diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml
index 56c8594..e594429 100644
--- a/.github/workflows/deploy.yml
+++ b/.github/workflows/deploy.yml
@@ -1,17 +1,85 @@
name: Deploy
on:
+ workflow_call:
+ inputs:
+ ref:
+ description: Git ref containing the Docs release
+ required: true
+ type: string
+ expectedVersion:
+ description: Release version expected from the deployed health endpoint
+ required: true
+ type: string
+ expectedCommit:
+ description: Commit expected from the deployed health endpoint
+ required: true
+ type: string
workflow_dispatch:
- release:
- types: [published]
+ inputs:
+ ref:
+ description: Git ref to deploy
+ required: true
+ default: main
+ type: string
+ expectedVersion:
+ description: Optional release version; inferred from version tags or reported as unreleased
+ required: false
+ type: string
+ expectedCommit:
+ description: Optional commit assertion; defaults to the selected ref's commit
+ required: false
+ type: string
+
+concurrency:
+ group: fsharp-viewengine-production
+ cancel-in-progress: false
+
jobs:
deploy:
name: Deploy
runs-on: ubuntu-latest
+ environment:
+ name: production
+ url: https://fsharpviewengine.meiermade.com
permissions:
contents: read
id-token: write
+ outputs:
+ version: ${{ steps.release.outputs.version }}
+ commit: ${{ steps.release.outputs.commit }}
steps:
- uses: actions/checkout@v5
+ with:
+ ref: ${{ inputs.ref }}
+ - name: Resolve release metadata
+ id: release
+ env:
+ REQUESTED_VERSION: ${{ inputs.expectedVersion }}
+ EXPECTED_COMMIT: ${{ inputs.expectedCommit }}
+ DEPLOY_REF: ${{ inputs.ref }}
+ run: |
+ version="$REQUESTED_VERSION"
+ if [[ -z "$version" ]]; then
+ if [[ "$DEPLOY_REF" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
+ version="${DEPLOY_REF#v}"
+ else
+ version="unreleased"
+ fi
+ fi
+
+ commit="$EXPECTED_COMMIT"
+ if [[ -z "$commit" ]]; then
+ commit="$(git rev-parse HEAD)"
+ fi
+
+ actual_commit="$(git rev-parse HEAD)"
+ if [[ "$commit" != "$actual_commit" ]]; then
+ echo "Expected commit $commit, but $DEPLOY_REF resolved to $actual_commit." >&2
+ exit 1
+ fi
+
+ echo "version=$version" >> "$GITHUB_OUTPUT"
+ echo "commit=$commit" >> "$GITHUB_OUTPUT"
- name: Setup Node
uses: actions/setup-node@v5
with:
@@ -21,7 +89,7 @@ jobs:
with:
install_components: gke-gcloud-auth-plugin
- name: Install Packages
- run: npm install
+ run: npm ci
working-directory: ./pulumi
- name: Authenticate Pulumi
uses: pulumi/auth-actions@v1
@@ -31,7 +99,71 @@ jobs:
scope: user:meiermade
- name: Deploy
uses: pulumi/actions@v6
+ env:
+ RELEASE_VERSION: ${{ steps.release.outputs.version }}
+ RELEASE_COMMIT: ${{ steps.release.outputs.commit }}
with:
work-dir: ./pulumi
command: up
stack-name: prod
+
+ e2e:
+ name: E2E
+ needs: deploy
+ runs-on: ubuntu-latest
+ timeout-minutes: 15
+ permissions:
+ contents: read
+ steps:
+ - uses: actions/checkout@v5
+ with:
+ ref: ${{ needs.deploy.outputs.commit }}
+ - name: Setup Node
+ uses: actions/setup-node@v5
+ with:
+ node-version: 24
+ cache: npm
+ cache-dependency-path: e2e/package-lock.json
+ - name: Install E2E packages
+ run: npm ci
+ working-directory: ./e2e
+ - name: Install Firefox
+ run: npx playwright install --with-deps firefox
+ working-directory: ./e2e
+ - name: Wait for deployed Docs release
+ env:
+ EXPECTED_VERSION: ${{ needs.deploy.outputs.version }}
+ EXPECTED_COMMIT: ${{ needs.deploy.outputs.commit }}
+ run: |
+ for _ in $(seq 1 60); do
+ health="$(curl --fail --silent --show-error https://fsharpviewengine.meiermade.com/health || true)"
+ status="$(jq -r '.status // empty' <<< "$health" 2>/dev/null || true)"
+ version="$(jq -r '.version // empty' <<< "$health" 2>/dev/null || true)"
+ commit="$(jq -r '.commit // empty' <<< "$health" 2>/dev/null || true)"
+
+ if [[ "$status" == "ok" && "$version" == "$EXPECTED_VERSION" && "$commit" == "$EXPECTED_COMMIT" ]]; then
+ exit 0
+ fi
+
+ echo "Waiting for Docs $EXPECTED_VERSION at $EXPECTED_COMMIT; found version=${version:-unknown}, commit=${commit:-unknown}."
+ sleep 5
+ done
+
+ echo "FSharp.ViewEngine Docs did not report the expected release." >&2
+ exit 1
+ - name: Test deployed Docs
+ run: npm run test:published
+ working-directory: ./e2e
+ env:
+ DOCS_E2E_BASE_URL: https://fsharpviewengine.meiermade.com
+ DOCS_EXPECTED_VERSION: ${{ needs.deploy.outputs.version }}
+ DOCS_EXPECTED_COMMIT: ${{ needs.deploy.outputs.commit }}
+ - name: Upload E2E diagnostics
+ if: failure()
+ uses: actions/upload-artifact@v4
+ with:
+ name: fsharp-viewengine-deploy-e2e
+ path: |
+ e2e/playwright-report
+ e2e/test-results
+ if-no-files-found: ignore
diff --git a/.github/workflows/preview.yml b/.github/workflows/preview.yml
index 5407c22..b34a25c 100644
--- a/.github/workflows/preview.yml
+++ b/.github/workflows/preview.yml
@@ -1,11 +1,11 @@
name: Preview
on:
pull_request:
- paths-ignore:
- - '**/*.md'
+ branches:
+ - main
jobs:
- test:
- name: Test
+ unit:
+ name: Unit
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
@@ -20,20 +20,68 @@ jobs:
run: dotnet tool restore
working-directory: ./sln
- name: Install Packages
- run: dotnet paket install
+ run: |
+ dotnet paket install
+ dotnet restore FSharp.ViewEngine.slnx
+ working-directory: ./sln
+ - name: Build solution
+ run: dotnet build FSharp.ViewEngine.slnx --configuration Release --no-restore
working-directory: ./sln
- name: Test
run: ./fake.sh Test
working-directory: ./sln
+ - name: Pack
+ run: dotnet pack src/FSharp.ViewEngine/FSharp.ViewEngine.fsproj --configuration Release --no-restore --output "$RUNNER_TEMP/package" /p:PackageVersion=0.0.0-preview
+ working-directory: ./sln
+ - name: Verify package compatibility
+ env:
+ PACKAGE_PATH: ${{ runner.temp }}/package/FSharp.ViewEngine.0.0.0-preview.nupkg
+ run: ./fake.sh VerifyPackage --single-target
+ working-directory: ./sln
+
+ e2e:
+ name: E2E
+ runs-on: ubuntu-latest
+ timeout-minutes: 20
+ steps:
+ - uses: actions/checkout@v5
+ - name: Setup Node
+ uses: actions/setup-node@v5
+ with:
+ node-version: 24
+ cache: npm
+ cache-dependency-path: e2e/package-lock.json
+ - name: Install E2E packages
+ run: npm ci
+ working-directory: ./e2e
+ - name: Install Firefox
+ run: npx playwright install --with-deps firefox
+ working-directory: ./e2e
+ - name: Test production Docs image
+ run: npm test -- --project=firefox
+ working-directory: ./e2e
+ - name: Upload E2E diagnostics
+ if: failure()
+ uses: actions/upload-artifact@v4
+ with:
+ name: fsharp-viewengine-preview-e2e
+ path: |
+ e2e/playwright-report
+ e2e/test-results
+ /tmp/fsharp-viewengine-e2e
+ if-no-files-found: ignore
+
preview:
- name: Preview
+ # Keep this terminal job named Test because it is the protected branch check.
+ name: Test
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write
pull-requests: write
needs:
- - test
+ - unit
+ - e2e
steps:
- uses: actions/checkout@v5
- name: Setup Node
@@ -45,7 +93,7 @@ jobs:
with:
install_components: gke-gcloud-auth-plugin
- name: Install Packages
- run: npm install
+ run: npm ci
working-directory: ./pulumi
- name: Authenticate Pulumi
uses: pulumi/auth-actions@v1
@@ -55,6 +103,9 @@ jobs:
scope: user:meiermade
- name: Preview
uses: pulumi/actions@v6
+ env:
+ RELEASE_VERSION: preview
+ RELEASE_COMMIT: ${{ github.event.pull_request.head.sha }}
with:
work-dir: ./pulumi
command: preview
diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
index cbb79f9..6cd0d2c 100644
--- a/.github/workflows/publish.yml
+++ b/.github/workflows/publish.yml
@@ -1,19 +1,41 @@
name: Publish
on:
- release:
- branches:
- - main
- types:
- - published
+ workflow_dispatch:
+ inputs:
+ version:
+ description: Optional calendar version override (YYYY.M.MINOR)
+ required: false
+ type: string
+
+permissions:
+ contents: read
+ id-token: write
+
+concurrency:
+ group: fsharp-viewengine-release
+ cancel-in-progress: false
+
jobs:
- publish:
- name: Publish
+ package:
+ name: Package
runs-on: ubuntu-latest
permissions:
- contents: read
- id-token: write
+ contents: write
+ outputs:
+ tag: ${{ steps.release.outputs.tag }}
+ version: ${{ steps.release.outputs.version }}
+ commit: ${{ steps.release.outputs.commit }}
steps:
+ - name: Require main branch
+ run: |
+ if [[ "$GITHUB_REF" != "refs/heads/main" ]]; then
+ echo "Releases must run from main, not $GITHUB_REF." >&2
+ exit 1
+ fi
- uses: actions/checkout@v5
+ with:
+ fetch-depth: 0
+ fetch-tags: true
- name: Setup dotnet
uses: actions/setup-dotnet@v5
with:
@@ -25,14 +47,117 @@ jobs:
run: dotnet tool restore
working-directory: ./sln
- name: Install Packages
- run: dotnet paket install
+ run: |
+ dotnet paket install
+ dotnet restore FSharp.ViewEngine.slnx
+ working-directory: ./sln
+ - name: Prepare release
+ env:
+ RELEASE_METADATA_PATH: ${{ runner.temp }}/release-metadata.json
+ RELEASE_VERSION_OVERRIDE: ${{ inputs.version }}
+ run: ./fake.sh PrepareRelease
+ working-directory: ./sln
+ - name: Read release metadata
+ id: release
+ env:
+ RELEASE_METADATA_PATH: ${{ runner.temp }}/release-metadata.json
+ run: |
+ {
+ echo "tag=$(jq -r '.tag' "$RELEASE_METADATA_PATH")"
+ echo "version=$(jq -r '.version' "$RELEASE_METADATA_PATH")"
+ echo "commit=$(jq -r '.commit' "$RELEASE_METADATA_PATH")"
+ } >> "$GITHUB_OUTPUT"
+ - name: Build, test, pack, and verify release
+ env:
+ PACKAGE_VERSION: ${{ steps.release.outputs.version }}
+ run: ./fake.sh VerifyPackage
working-directory: ./sln
+ - name: Record package checksums
+ run: sha256sum ./*.nupkg ./*.snupkg > SHA256SUMS
+ working-directory: ./nugets
+ - name: Upload verified package
+ uses: actions/upload-artifact@v4
+ with:
+ name: fsharp-viewengine-nuget
+ path: |
+ nugets/*.nupkg
+ nugets/*.snupkg
+ nugets/SHA256SUMS
+ if-no-files-found: error
+ - name: Create release tag
+ env:
+ RELEASE_METADATA_PATH: ${{ runner.temp }}/release-metadata.json
+ run: ./fake.sh TagRelease
+ working-directory: ./sln
+
+ deploy-and-verify:
+ name: Deploy and verify Docs
+ needs: package
+ uses: ./.github/workflows/deploy.yml
+ with:
+ ref: ${{ needs.package.outputs.tag }}
+ expectedVersion: ${{ needs.package.outputs.version }}
+ expectedCommit: ${{ needs.package.outputs.commit }}
+ secrets: inherit
+
+ publish:
+ name: Publish NuGet
+ needs:
+ - package
+ - deploy-and-verify
+ runs-on: ubuntu-latest
+ permissions:
+ contents: write
+ id-token: write
+ steps:
+ - name: Setup dotnet
+ uses: actions/setup-dotnet@v5
+ with:
+ dotnet-version: 10.0.x
+ - name: Download verified package
+ uses: actions/download-artifact@v5
+ with:
+ name: fsharp-viewengine-nuget
+ path: ${{ runner.temp }}/package
+ - name: Verify package checksums
+ run: sha256sum --check SHA256SUMS
+ working-directory: ${{ runner.temp }}/package
- name: Authenticate Pulumi
uses: pulumi/auth-actions@v1
with:
organization: meiermade
requested-token-type: urn:pulumi:token-type:access_token:personal
scope: user:meiermade
- - name: Publish
- run: pulumi env run fsharpviewengine/nuget -- ./fake.sh PushNugets
- working-directory: ./sln
+ - name: Publish verified package
+ env:
+ PACKAGE_PATH: ${{ runner.temp }}/package/FSharp.ViewEngine.${{ needs.package.outputs.version }}.nupkg
+ run: |
+ pulumi env run fsharpviewengine/nuget -- \
+ bash -euo pipefail -c \
+ "dotnet nuget push \"$PACKAGE_PATH\" --source https://api.nuget.org/v3/index.json --api-key \"\$NUGET_API_KEY\" --skip-duplicate"
+ - name: Wait for NuGet availability
+ env:
+ VERSION: ${{ needs.package.outputs.version }}
+ run: |
+ package_url="https://api.nuget.org/v3-flatcontainer/fsharp.viewengine/$VERSION/fsharp.viewengine.$VERSION.nupkg"
+
+ for _ in $(seq 1 60); do
+ if curl --fail --silent --show-error --output /dev/null "$package_url"; then
+ echo "FSharp.ViewEngine $VERSION is available from NuGet."
+ exit 0
+ fi
+ sleep 10
+ done
+
+ echo "FSharp.ViewEngine $VERSION was not available from NuGet after 10 minutes." >&2
+ exit 1
+ - name: Create GitHub Release
+ env:
+ GH_TOKEN: ${{ github.token }}
+ TAG: ${{ needs.package.outputs.tag }}
+ run: |
+ if gh release view "$TAG" >/dev/null 2>&1; then
+ echo "GitHub Release $TAG already exists."
+ else
+ gh release create "$TAG" --verify-tag --title "$TAG" --generate-notes
+ fi
diff --git a/.gitignore b/.gitignore
index 5c3f262..7fbeb67 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,3 +2,8 @@
.vscode
.claude
.pi*
+.plans/
+node_modules/
+playwright-report/
+test-results/
+BenchmarkDotNet.Artifacts/
diff --git a/README.md b/README.md
index 67bcc58..c2dc1f2 100644
--- a/README.md
+++ b/README.md
@@ -16,7 +16,7 @@ FSharp.ViewEngine combines ideas from several F# view engines into a clean, unif
- **Computation expression syntax** (like Oxpecker.ViewEngine and Bolero) for building elements
- **Feliz-style single sequence** of attributes and child elements — no separate attribute and children lists
-- **Attributes prefixed with underscore** by convention (like Giraffe.ViewEngine, e.g. `_class`, `_id`, `_hxGet`), giving clean syntax and nice syntax highlighting
+- **Attributes prefixed with underscore** by convention (like Giraffe.ViewEngine, e.g. `_class`, `_id`, `_dataOn`), giving clean syntax and nice syntax highlighting
- **Mixed yielding** in computation expressions — you can yield strings, elements, and attributes in any order without needing a special `_children` attribute
The result is a DSL that is as minimal and fast as possible while remaining expressive and type-safe.
@@ -35,14 +35,18 @@ dotnet package add FSharp.ViewEngine
dotnet paket add FSharp.ViewEngine
```
+## Runtime compatibility
+
+The package ships a single `net8.0` compatibility asset and is tested on supported .NET 8, .NET 9, and .NET 10 runtimes. NuGet automatically selects the `net8.0` asset for compatible newer runtimes.
+
+Portable symbols are published separately with Source Link metadata, so supported debuggers can retrieve the matching source from GitHub without increasing the main package size.
+
## Usage
```fsharp
open FSharp.ViewEngine
open type Html
-open type Htmx
-open type Alpine
open type Datastar
-open type Tailwind
+open type TailwindElements
html {
_lang "en"
@@ -52,17 +56,21 @@ html {
link { _href "/css/compiled.css"; _rel "stylesheet" }
}
body {
- _xData "{showContent: false}"
+ _dataSignals "{showContent: false}"
_class "bg-gray-50"
div {
_id "page"
_class [ "flex"; "flex-col" ]
- h1 { _hxGet "/hello"; _hxTarget "#page"; "Hello" }
- h1 { _hxGet "/world"; _hxTarget "#page"; "World" }
+ h1 { "Hello from FSharp.ViewEngine" }
+ button {
+ _dataOn ("click", "$showContent = !$showContent")
+ "Toggle content"
+ }
}
br
div {
- _xShow "showContent"
+ _dataShow "$showContent"
+ _style "display: none"
h2 { "Content" }
p { "Some content" }
ul {
@@ -82,13 +90,13 @@ html {
-
+
-
Hello
- World
+ Hello from FSharp.ViewEngine
+
-
+
Content
Some content
@@ -101,33 +109,100 @@ html {
```
## Benchmarks
-Ran on February 6, 2026 with BenchmarkDotNet MediumRun only.
+Measured on August 6, 2026 with BenchmarkDotNet 0.15.8 on .NET SDK 10.0.201 / runtime 10.0.5, macOS 26.4.1, Apple M5 Max Arm64. The process-isolated `MediumRun` configuration uses two launches, ten warmups, fifteen measured iterations, and a 100 ms iteration target. The shorter target avoids multi-gigabyte per-iteration allocation pressure in the fastest render-only workloads while retaining repeated measurements.
-Command:
-```
-cd sln && dotnet run -c Release --project src/Benchmarks/Benchmarks.fsproj
+The suite covers comparison-engine build/render behavior plus attribute encoding, 0/1/2/8 attribute and child shapes, array/list/sequence loops, and small, representative, deeply nested, and large workloads. Every run prints its environment, resolved dependency versions, and job configuration.
+
+```shell
+cd sln
+
+# Run the complete measurement suite.
+./fake.sh Benchmark
+
+# List or target benchmark cases with standard BenchmarkDotNet filters.
+./fake.sh Benchmark --list flat
+./fake.sh Benchmark --filter '*AttributeEncodingBenchmarks*'
+
+# Execute every case, or a filtered subset, once as a validation smoke run.
+./fake.sh BenchmarkSmoke
+./fake.sh BenchmarkSmoke --filter '*AttributeEncodingBenchmarks*'
```
-BuildAndRender (mean, lower is better):
-| Method | Mean | Allocated |
-|-------------- |----------:|----------:|
-| ViewEngineApi | 5.763 μs | 11.4 KB |
-| OxpeckerApi | 7.562 μs | 12.88 KB |
-| GiraffeApi | 7.925 μs | 23.95 KB |
-| FelizApi | 11.053 μs | 25.87 KB |
+Results are representative measurements, not CI regression thresholds. Means and managed allocations are shown below; lower is better.
-RenderOnly:
-| Method | Mean | Allocated |
-|-------------- |---------:|----------:|
-| ViewEngineApi | 2.464 μs | 2.94 KB |
-| OxpeckerApi | 2.796 μs | 2.94 KB |
-| GiraffeApi | 3.176 μs | 12.77 KB |
-| FelizApi | 6.151 μs | 14.2 KB |
+### View-engine comparisons
+
+Build and render:
-BuildOnly:
| Method | Mean | Allocated |
|-------------- |---------:|----------:|
-| ViewEngineApi | 2.153 μs | 8.46 KB |
-| OxpeckerApi | 5.275 μs | 9.95 KB |
-| GiraffeApi | 7.323 μs | 11.17 KB |
-| FelizApi | 7.707 μs | 11.66 KB |
+| ViewEngineApi | 1.585 μs | 11.39 KB |
+| OxpeckerApi | 2.147 μs | 12.88 KB |
+| GiraffeApi | 2.649 μs | 23.94 KB |
+| FelizApi | 3.723 μs | 25.87 KB |
+
+Render only:
+
+| Method | Mean | Allocated |
+|-------------- |-----------:|----------:|
+| ViewEngineApi | 833.5 ns | 2.93 KB |
+| OxpeckerApi | 911.4 ns | 2.93 KB |
+| GiraffeApi | 989.6 ns | 12.77 KB |
+| FelizApi | 1,872.9 ns | 14.2 KB |
+
+Build only:
+
+| Method | Mean | Allocated |
+|-------------- |-----------:|----------:|
+| ViewEngineApi | 670.1 ns | 8.46 KB |
+| OxpeckerApi | 1,181.0 ns | 9.95 KB |
+| GiraffeApi | 1,654.9 ns | 11.17 KB |
+| FelizApi | 1,782.9 ns | 11.66 KB |
+
+### FSharp.ViewEngine workloads
+
+Attribute encoding:
+
+| Value | Mean | Allocated |
+|-------- |---------:|----------:|
+| Plain | 36.17 ns | 280 B |
+| Encoded | 81.92 ns | 496 B |
+
+Inline and overflow storage boundaries:
+
+| Shape | Count | Mean | Allocated |
+|----------- |------:|----------:|----------:|
+| Attributes | 0 | 26.43 ns | 200 B |
+| Attributes | 1 | 33.16 ns | 216 B |
+| Attributes | 2 | 41.23 ns | 240 B |
+| Attributes | 8 | 108.42 ns | 744 B |
+| Children | 0 | 18.47 ns | 160 B |
+| Children | 1 | 35.08 ns | 320 B |
+| Children | 2 | 52.22 ns | 488 B |
+| Children | 8 | 187.57 ns | 1,648 B |
+
+Equivalent collection inputs:
+
+| Collection | Mean | Allocated |
+|----------- |---------:|----------:|
+| Array | 451.7 ns | 3.45 KB |
+| List | 437.7 ns | 3.45 KB |
+| Sequence | 482.8 ns | 3.53 KB |
+
+Document workloads:
+
+| Workload | Build and render | Build/render allocation | Render only | Render allocation |
+|-------------------- |-----------------:|------------------------:|------------:|------------------:|
+| Small fragment | 72.92 ns | 680 B | 51.05 ns | 296 B |
+| Representative page | 1,538.00 ns | 11,664 B | 813.40 ns | 3,000 B |
+| Deeply nested | 2,288.68 ns | 12,096 B | 1,069.54 ns | 3,256 B |
+| Large response | 228,746.00 ns | 1,252,539 B | 77,196.10 ns | 283,768 B |
+
+### Profiling findings
+
+- Build-only CPU samples are dominated by `TagBuilder.Run` and generated computation-expression `Invoke` methods, but allocation samples contain DOM nodes and overflow collections rather than F# closure objects.
+- Render-only allocation samples are almost entirely the required returned `System.String`.
+- Optimized ARM64 JIT output retains indirect virtual calls for child `HtmlElement.Render` dispatch, but profiling does not show dispatch as a dominant cost relative to string creation and GC work.
+- General sequence input adds about 80 bytes and modest runtime overhead; current results do not justify array/list-specific `For` overloads.
+- The 0/1/2 inline attribute and child storage optimization remains justified by the allocation results.
+- The thread-static `StringBuilder` pool now retains at most one builder with capacity no greater than 256K characters. The bound prevents unbounded per-thread retention without adding allocation or timing regressions to the representative 142K-character large response.
diff --git a/compose.yml b/compose.yml
new file mode 100644
index 0000000..94caa22
--- /dev/null
+++ b/compose.yml
@@ -0,0 +1,13 @@
+name: fsharp-viewengine-e2e
+
+services:
+ docs:
+ build:
+ context: ./sln
+ environment:
+ DEBUG: 'false'
+ SERVER_URL: http://0.0.0.0:5000
+ init: true
+ ports:
+ - 127.0.0.1:${E2E_SERVER_PORT:-5054}:5000
+ stop_grace_period: 15s
diff --git a/e2e/package-lock.json b/e2e/package-lock.json
new file mode 100644
index 0000000..7e6fc12
--- /dev/null
+++ b/e2e/package-lock.json
@@ -0,0 +1,76 @@
+{
+ "name": "fsharp-viewengine-e2e",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "fsharp-viewengine-e2e",
+ "devDependencies": {
+ "@playwright/test": "1.62.1"
+ }
+ },
+ "node_modules/@playwright/test": {
+ "version": "1.62.1",
+ "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.62.1.tgz",
+ "integrity": "sha512-DTcUc8qii+cpHvtOwggMtBRMjKZHXYWdw8syRYu2vtzuq4Wxphqq4NfCs5Zt44L6mA8rfDfj+PHnxFc/FeK6mQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "playwright": "1.62.1"
+ },
+ "bin": {
+ "playwright": "cli.js"
+ },
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.2",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
+ "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/playwright": {
+ "version": "1.62.1",
+ "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.62.1.tgz",
+ "integrity": "sha512-0M+L3LAD8/nm554LOla9Ayx0j0tmFZ0FBcoQ7F1VuVHpM/XpiC8RcDzBQB8W5+hA8L22THxELzeF+2WcUzvcLg==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "playwright-core": "1.62.1"
+ },
+ "bin": {
+ "playwright": "cli.js"
+ },
+ "engines": {
+ "node": ">=20"
+ },
+ "optionalDependencies": {
+ "fsevents": "2.3.2"
+ }
+ },
+ "node_modules/playwright-core": {
+ "version": "1.62.1",
+ "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.62.1.tgz",
+ "integrity": "sha512-wPYSwEBJY9GHraISXqyqtx0na0LpO3XEX7jNDhntbex7tzUS7kLnZsOlFruFJB4Hi/rhDMjXGqHewDZ68nYZVw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "playwright-core": "cli.js"
+ },
+ "engines": {
+ "node": ">=20"
+ }
+ }
+ }
+}
diff --git a/e2e/package.json b/e2e/package.json
new file mode 100644
index 0000000..e6b1548
--- /dev/null
+++ b/e2e/package.json
@@ -0,0 +1,12 @@
+{
+ "name": "fsharp-viewengine-e2e",
+ "private": true,
+ "type": "module",
+ "scripts": {
+ "test": "playwright test",
+ "test:published": "E2E_START_LOCAL=0 playwright test --project=firefox --retries=0"
+ },
+ "devDependencies": {
+ "@playwright/test": "1.62.1"
+ }
+}
diff --git a/e2e/playwright.config.ts b/e2e/playwright.config.ts
new file mode 100644
index 0000000..6b88e23
--- /dev/null
+++ b/e2e/playwright.config.ts
@@ -0,0 +1,39 @@
+import { defineConfig, devices } from '@playwright/test'
+import path from 'node:path'
+import { fileURLToPath } from 'node:url'
+
+const __dirname = path.dirname(fileURLToPath(import.meta.url))
+const startLocal = process.env.E2E_START_LOCAL !== '0'
+const port = process.env.E2E_SERVER_PORT ?? '5054'
+const baseURL = process.env.DOCS_E2E_BASE_URL ?? (startLocal ? `http://127.0.0.1:${port}` : 'https://fsharpviewengine.meiermade.com')
+
+export default defineConfig({
+ testDir: './tests',
+ outputDir: './test-results',
+ timeout: 30_000,
+ expect: { timeout: 10_000 },
+ fullyParallel: false,
+ forbidOnly: !!process.env.CI,
+ workers: 1,
+ retries: process.env.CI ? 2 : 0,
+ reporter: process.env.CI ? [['list'], ['html', { open: 'never' }]] : 'list',
+ use: {
+ baseURL,
+ trace: 'retain-on-failure',
+ video: 'retain-on-failure',
+ screenshot: 'only-on-failure',
+ },
+ projects: [{ name: 'firefox', use: { ...devices['Desktop Firefox'] } }],
+ webServer: startLocal
+ ? {
+ command: 'bash scripts/start-local.sh',
+ cwd: __dirname,
+ url: `${baseURL}/health`,
+ timeout: process.env.CI ? 600_000 : 300_000,
+ reuseExistingServer: process.env.E2E_REUSE_EXISTING_SERVER === '1',
+ gracefulShutdown: { signal: 'SIGTERM', timeout: 15_000 },
+ stdout: 'pipe',
+ stderr: 'pipe',
+ }
+ : undefined,
+})
diff --git a/e2e/scripts/start-local.sh b/e2e/scripts/start-local.sh
new file mode 100755
index 0000000..4fef30e
--- /dev/null
+++ b/e2e/scripts/start-local.sh
@@ -0,0 +1,36 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+e2e_dir="$(cd "$script_dir/.." && pwd)"
+repo_dir="$(cd "$e2e_dir/.." && pwd)"
+server_port="${E2E_SERVER_PORT:-5054}"
+project_name="fsharp-viewengine-e2e-${server_port}"
+log_dir="/tmp/fsharp-viewengine-e2e"
+log_file="$log_dir/compose.log"
+
+mkdir -p "$log_dir"
+export E2E_SERVER_PORT="$server_port"
+
+compose() {
+ docker compose \
+ --project-name "$project_name" \
+ --project-directory "$repo_dir" \
+ --file "$repo_dir/compose.yml" \
+ "$@"
+}
+
+cleanup() {
+ local exit_code=$?
+ trap - EXIT INT TERM
+ set +e
+ compose logs --no-color >"$log_file" 2>&1
+ compose down --volumes --remove-orphans
+ exit "$exit_code"
+}
+
+trap cleanup EXIT INT TERM
+
+compose up --build --detach --force-recreate --remove-orphans
+compose logs --follow --no-color docs &
+wait $!
diff --git a/e2e/tests/docs.spec.ts b/e2e/tests/docs.spec.ts
new file mode 100644
index 0000000..725f2e3
--- /dev/null
+++ b/e2e/tests/docs.spec.ts
@@ -0,0 +1,134 @@
+import { expect, test, type Page } from '@playwright/test'
+
+const productionOrigin = 'https://fsharpviewengine.meiermade.com'
+
+const routes = [
+ { path: '/', heading: 'FSharp.ViewEngine' },
+ { path: '/installation', heading: 'Installation' },
+ { path: '/custom', heading: 'Custom Elements & Attributes' },
+ { path: '/usage', heading: 'Usage' },
+ { path: '/extensions/alpine', heading: 'Alpine.js' },
+ { path: '/extensions/datastar', heading: 'Datastar' },
+ { path: '/extensions/htmx', heading: 'HTMX' },
+ { path: '/extensions/svg', heading: 'SVG' },
+ { path: '/extensions/tailwind-elements', heading: 'Tailwind Plus Elements' },
+ { path: '/benchmarks', heading: 'Benchmarks' },
+ { path: '/changelog', heading: 'Changelog' },
+]
+
+function captureBrowserErrors(page: Page) {
+ const errors: string[] = []
+ page.on('pageerror', error => errors.push(error.message))
+ page.on('console', message => {
+ if (message.type() === 'error') errors.push(message.text())
+ })
+ return errors
+}
+
+test.describe('public documentation routes', () => {
+ for (const route of routes) {
+ test(`GET ${route.path} renders`, async ({ page }) => {
+ const browserErrors = captureBrowserErrors(page)
+ const response = await page.goto(route.path, { waitUntil: 'domcontentloaded' })
+
+ expect(response?.status(), `${route.path} status`).toBe(200)
+ const serverHtml = await response!.text()
+ expect(serverHtml, `${route.path} server-rendered article`).toContain('')
+ expect(serverHtml, `${route.path} complete HTML document`).toContain('')
+ await expect(page.getByRole('heading', { level: 1, name: route.heading, exact: true })).toBeVisible()
+ await expect(page.locator('article')).toBeVisible()
+ const canonicalURL = route.path === '/' ? productionOrigin : `${productionOrigin}${route.path}`
+ await expect(page.locator('link[rel="canonical"]')).toHaveAttribute('href', canonicalURL)
+ expect(await page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth)).toBe(true)
+ expect(browserErrors, `${route.path} browser errors`).toEqual([])
+ })
+ }
+})
+
+test('health and pinned application assets are available', async ({ request }) => {
+ const health = await request.get('/health')
+ expect(health.status()).toBe(200)
+ const healthBody = await health.json()
+ expect(healthBody).toMatchObject({ status: 'ok' })
+ expect(healthBody.version).toBeTruthy()
+ expect(healthBody.commit).toBeTruthy()
+
+ if (process.env.DOCS_EXPECTED_VERSION) {
+ expect(healthBody.version).toBe(process.env.DOCS_EXPECTED_VERSION)
+ }
+ if (process.env.DOCS_EXPECTED_COMMIT) {
+ expect(healthBody.commit).toBe(process.env.DOCS_EXPECTED_COMMIT)
+ }
+
+ const css = await request.get('/css/output.css')
+ expect(css.status()).toBe(200)
+ expect(await css.text()).toContain('tailwindcss v4.3.3')
+
+ const datastar = await request.get('/scripts/datastar.1.0.2.js')
+ expect(datastar.status()).toBe(200)
+ expect(await datastar.text()).toContain('Datastar v1.0.2')
+
+ const alpine = await request.get('/scripts/alpinejs.3.15.12.min.js')
+ expect(alpine.status()).toBe(404)
+})
+
+test('mobile navigation opens, closes, and does not overflow', async ({ page }) => {
+ await page.setViewportSize({ width: 390, height: 844 })
+ await page.goto('/', { waitUntil: 'domcontentloaded' })
+
+ const drawer = page.locator('[data-show="$mobileNavOpen"]')
+ await expect(drawer).toBeHidden()
+
+ await page.getByRole('button', { name: 'Open navigation' }).click()
+ await expect(drawer).toBeVisible()
+ await page.keyboard.press('Escape')
+ await expect(drawer).toBeHidden()
+
+ await page.getByRole('button', { name: 'Open navigation' }).click()
+ await expect(drawer).toBeVisible()
+ await page.locator('#mobile-navigation-backdrop').click({ position: { x: 380, y: 400 } })
+ await expect(drawer).toBeHidden()
+
+ await page.getByRole('button', { name: 'Open navigation' }).click()
+ await page.getByRole('button', { name: 'Close navigation' }).click()
+ await expect(drawer).toBeHidden()
+ expect(await page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth)).toBe(true)
+})
+
+test('benchmark tables remain readable without page overflow on mobile', async ({ page }) => {
+ await page.setViewportSize({ width: 390, height: 844 })
+ await page.goto('/benchmarks', { waitUntil: 'domcontentloaded' })
+
+ const comparison = page.getByRole('figure', { name: 'Build and render comparison' })
+ await expect(comparison).toBeVisible()
+ await expect(comparison).toContainText('FSharp.ViewEngine')
+ await expect(comparison).toContainText('1.35× as long')
+ await expect(comparison.locator('[style^="width:"]')).toHaveCount(4)
+ await expect(page.getByRole('table')).toHaveCount(7)
+ expect(await page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth)).toBe(true)
+})
+
+test('theme selection persists across navigation', async ({ page }) => {
+ await page.goto('/', { waitUntil: 'domcontentloaded' })
+ const themeButton = page.getByRole('button', { name: 'Choose color theme' })
+ const darkButton = page.getByRole('button', { name: 'Dark', exact: true })
+
+ await themeButton.click()
+ await expect(darkButton).toBeVisible()
+ await page.getByRole('heading', { level: 1, name: 'FSharp.ViewEngine' }).click()
+ await expect(darkButton).toBeHidden()
+
+ await themeButton.click()
+ await darkButton.click()
+
+ await expect(page.locator('html')).toHaveClass(/dark/)
+ expect(await page.evaluate(() => localStorage.getItem('theme'))).toBe('dark')
+
+ await page.goto('/installation', { waitUntil: 'domcontentloaded' })
+ await expect(page.locator('html')).toHaveClass(/dark/)
+})
+
+test('removed Tailwind documentation route returns 404', async ({ request }) => {
+ const response = await request.get('/extensions/tailwind')
+ expect(response.status()).toBe(404)
+})
diff --git a/pulumi/package-lock.json b/pulumi/package-lock.json
index 7dec7fe..b1f3438 100644
--- a/pulumi/package-lock.json
+++ b/pulumi/package-lock.json
@@ -6,20 +6,29 @@
"": {
"name": "fsharp-view-engine",
"dependencies": {
- "@pulumi/cloudflare": "^6.14.0",
- "@pulumi/docker-build": "^0.0.15",
- "@pulumi/kubernetes": "^4.28",
- "@pulumi/pulumi": "^3.229"
+ "@pulumi/cloudflare": "^6.19.0",
+ "@pulumi/docker-build": "^0.0.22",
+ "@pulumi/kubernetes": "^4.33.0",
+ "@pulumi/pulumi": "^3.256.0"
},
"devDependencies": {
- "@types/node": "^24.12.2",
+ "@types/node": "^24.13.3",
"typescript": "^5.9.3"
}
},
+ "node_modules/@gar/promise-retry": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/@gar/promise-retry/-/promise-retry-1.0.3.tgz",
+ "integrity": "sha512-GmzA9ckNokPypTg10pgpeHNQe7ph+iIKKmhKu3Ob9ANkswreCx7R3cKmY781K8QK3AqVL3xVh9A42JvIAbkkSA==",
+ "license": "MIT",
+ "engines": {
+ "node": "^20.17.0 || >=22.9.0"
+ }
+ },
"node_modules/@grpc/grpc-js": {
- "version": "1.14.3",
- "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.14.3.tgz",
- "integrity": "sha512-Iq8QQQ/7X3Sac15oB6p0FmUg/klxQvXLeileoqrTRGJYLV+/9tubbr9ipz0GKHjmXVsgFPo/+W+2cA8eNcR+XA==",
+ "version": "1.14.4",
+ "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.14.4.tgz",
+ "integrity": "sha512-k9Dj3DV/itK9D06Y8f190Qgop7/Ui+D0njFV3LHMPwPT75DpXLQohE9Wmz0QElrJnzsjB7KPWiKJbOl7IPDArQ==",
"license": "Apache-2.0",
"dependencies": {
"@grpc/proto-loader": "^0.8.0",
@@ -30,14 +39,14 @@
}
},
"node_modules/@grpc/proto-loader": {
- "version": "0.8.0",
- "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.8.0.tgz",
- "integrity": "sha512-rc1hOQtjIWGxcxpb9aHAfLpIctjEnsDehj0DAiVfBlmT84uvR0uUtN2hEi/ecvWVjXUGf5qPF4qEgiLOx1YIMQ==",
+ "version": "0.8.1",
+ "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.8.1.tgz",
+ "integrity": "sha512-wtF6h+DY6M3YaDBPAmvuuA6jV8Sif9MjtOI5euKFWRgCDl5PeDpPsHR9u2l6St5ceY8AZgoNDww5+HvEsXFsGg==",
"license": "Apache-2.0",
"dependencies": {
"lodash.camelcase": "^4.3.0",
"long": "^5.0.0",
- "protobufjs": "^7.5.3",
+ "protobufjs": "^7.5.5",
"yargs": "^17.7.2"
},
"bin": {
@@ -364,303 +373,471 @@
}
},
"node_modules/@opentelemetry/api": {
- "version": "1.9.0",
- "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz",
- "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==",
+ "version": "1.9.1",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.1.tgz",
+ "integrity": "sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q==",
"license": "Apache-2.0",
"engines": {
"node": ">=8.0.0"
}
},
"node_modules/@opentelemetry/api-logs": {
- "version": "0.57.2",
- "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.57.2.tgz",
- "integrity": "sha512-uIX52NnTM0iBh84MShlpouI7UKqkZ7MrUszTmaypHBu4r7NofznSnQRfJ+uUeDtQDj6w8eFGg5KBLDAwAPz1+A==",
+ "version": "0.220.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.220.0.tgz",
+ "integrity": "sha512-CmVa4ImJ+ynfrPMNaAXHET6Bhb44SwzmfyVJFq9ni2jgXJR/l7C6gfVFddNmHP+ZOkP9cf4f9DBe68qVLTHc9w==",
"license": "Apache-2.0",
"dependencies": {
"@opentelemetry/api": "^1.3.0"
},
"engines": {
- "node": ">=14"
+ "node": ">=8.0.0"
}
},
"node_modules/@opentelemetry/context-async-hooks": {
- "version": "1.30.1",
- "resolved": "https://registry.npmjs.org/@opentelemetry/context-async-hooks/-/context-async-hooks-1.30.1.tgz",
- "integrity": "sha512-s5vvxXPVdjqS3kTLKMeBMvop9hbWkwzBpu+mUO2M7sZtlkyDJGwFe33wRKnbaYDo8ExRVBIIdwIGrqpxHuKttA==",
+ "version": "2.10.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/context-async-hooks/-/context-async-hooks-2.10.0.tgz",
+ "integrity": "sha512-bvyMcgLEkozzSzpEEEo1OMoeQ97bxj6Qs2uN3mPrSdDvObMI1myffD/BPqcLlzZO9//d1SqQA/WPw7Cz2AiqhA==",
"license": "Apache-2.0",
"engines": {
- "node": ">=14"
+ "node": "^18.19.0 || >=20.6.0"
},
"peerDependencies": {
"@opentelemetry/api": ">=1.0.0 <1.10.0"
}
},
"node_modules/@opentelemetry/core": {
- "version": "1.30.1",
- "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.30.1.tgz",
- "integrity": "sha512-OOCM2C/QIURhJMuKaekP3TRBxBKxG/TWWA0TL2J6nXUtDnuCtccy49LUJF8xPFXMX+0LMcxFpCo8M9cGY1W6rQ==",
+ "version": "2.10.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.10.0.tgz",
+ "integrity": "sha512-/wNZ8twnEQQA4HoHu22+vcsdru6pWPWxW+7w+FlxT6Id7PE/WIbZmVKkte+PF72e0F2dnImFeHD2syyE1Mw6MQ==",
"license": "Apache-2.0",
"dependencies": {
- "@opentelemetry/semantic-conventions": "1.28.0"
+ "@opentelemetry/semantic-conventions": "^1.29.0"
},
"engines": {
- "node": ">=14"
+ "node": "^18.19.0 || >=20.6.0"
},
"peerDependencies": {
"@opentelemetry/api": ">=1.0.0 <1.10.0"
}
},
"node_modules/@opentelemetry/exporter-trace-otlp-grpc": {
- "version": "0.57.2",
- "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-grpc/-/exporter-trace-otlp-grpc-0.57.2.tgz",
- "integrity": "sha512-gHU1vA3JnHbNxEXg5iysqCWxN9j83d7/epTYBZflqQnTyCC4N7yZXn/dMM+bEmyhQPGjhCkNZLx4vZuChH1PYw==",
+ "version": "0.220.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-grpc/-/exporter-trace-otlp-grpc-0.220.0.tgz",
+ "integrity": "sha512-bv1xmNhmNwIM6MdUBw4yYuJeVcEViVLk3uD69vOQMwueHBnfyl/u0HnBlB1FNY/Te0UOzJzvcbyR8wN6b+iGbA==",
"license": "Apache-2.0",
"dependencies": {
- "@grpc/grpc-js": "^1.7.1",
- "@opentelemetry/core": "1.30.1",
- "@opentelemetry/otlp-exporter-base": "0.57.2",
- "@opentelemetry/otlp-grpc-exporter-base": "0.57.2",
- "@opentelemetry/otlp-transformer": "0.57.2",
- "@opentelemetry/resources": "1.30.1",
- "@opentelemetry/sdk-trace-base": "1.30.1"
+ "@grpc/grpc-js": "^1.14.3",
+ "@opentelemetry/otlp-exporter-base": "0.220.0",
+ "@opentelemetry/otlp-grpc-exporter-base": "0.220.0",
+ "@opentelemetry/otlp-transformer": "0.220.0",
+ "@opentelemetry/sdk-trace": "2.9.0"
},
"engines": {
- "node": ">=14"
+ "node": "^18.19.0 || >=20.6.0"
},
"peerDependencies": {
"@opentelemetry/api": "^1.3.0"
}
},
"node_modules/@opentelemetry/exporter-zipkin": {
- "version": "1.30.1",
- "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-zipkin/-/exporter-zipkin-1.30.1.tgz",
- "integrity": "sha512-6S2QIMJahIquvFaaxmcwpvQQRD/YFaMTNoIxrfPIPOeITN+a8lfEcPDxNxn8JDAaxkg+4EnXhz8upVDYenoQjA==",
+ "version": "2.10.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-zipkin/-/exporter-zipkin-2.10.0.tgz",
+ "integrity": "sha512-7gsvgf0UDoJ4l9ObrwBmz5G/ZogiPk+lq+g5GpLp24YQF/vPM/BSsnOfcLnfinast5ASUgLo78uSC/ObjlnXgg==",
"license": "Apache-2.0",
"dependencies": {
- "@opentelemetry/core": "1.30.1",
- "@opentelemetry/resources": "1.30.1",
- "@opentelemetry/sdk-trace-base": "1.30.1",
- "@opentelemetry/semantic-conventions": "1.28.0"
+ "@opentelemetry/core": "2.10.0",
+ "@opentelemetry/resources": "2.10.0",
+ "@opentelemetry/sdk-trace": "2.10.0",
+ "@opentelemetry/semantic-conventions": "^1.29.0"
},
"engines": {
- "node": ">=14"
+ "node": "^18.19.0 || >=20.6.0"
},
"peerDependencies": {
"@opentelemetry/api": "^1.0.0"
}
},
+ "node_modules/@opentelemetry/exporter-zipkin/node_modules/@opentelemetry/sdk-trace": {
+ "version": "2.10.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace/-/sdk-trace-2.10.0.tgz",
+ "integrity": "sha512-MfQGq3GRmTh5fM/y+OjaO0vj6+luCB1XO2gfXCalKCfgKw0eHL++sm75DNweC6ohlp+aFvACqeE0fYayqdRaoQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/core": "2.10.0",
+ "@opentelemetry/resources": "2.10.0",
+ "@opentelemetry/semantic-conventions": "^1.29.0"
+ },
+ "engines": {
+ "node": "^18.19.0 || >=20.6.0"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": ">=1.3.0 <1.10.0"
+ }
+ },
"node_modules/@opentelemetry/instrumentation": {
- "version": "0.57.2",
- "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.57.2.tgz",
- "integrity": "sha512-BdBGhQBh8IjZ2oIIX6F2/Q3LKm/FDDKi6ccYKcBTeilh6SNdNKveDOLk73BkSJjQLJk6qe4Yh+hHw1UPhCDdrg==",
+ "version": "0.220.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.220.0.tgz",
+ "integrity": "sha512-xQx3E2WxP1mDvKzxLxX+CTCtNLa560YJZ3087qYHerl2YmiKpv7AH+dAy7vmx+eVrZ5BwhfWUAVoKOoxCNHcpw==",
"license": "Apache-2.0",
"dependencies": {
- "@opentelemetry/api-logs": "0.57.2",
- "@types/shimmer": "^1.2.0",
- "import-in-the-middle": "^1.8.1",
- "require-in-the-middle": "^7.1.1",
- "semver": "^7.5.2",
- "shimmer": "^1.2.1"
+ "@opentelemetry/api-logs": "0.220.0",
+ "import-in-the-middle": "^3.0.0",
+ "require-in-the-middle": "^8.0.0"
},
"engines": {
- "node": ">=14"
+ "node": "^18.19.0 || >=20.6.0"
},
"peerDependencies": {
"@opentelemetry/api": "^1.3.0"
}
},
"node_modules/@opentelemetry/instrumentation-grpc": {
- "version": "0.57.2",
- "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-grpc/-/instrumentation-grpc-0.57.2.tgz",
- "integrity": "sha512-TR6YQA67cLSZzdxbf2SrbADJy2Y8eBW1+9mF15P0VK2MYcpdoUSmQTF1oMkBwa3B9NwqDFA2fq7wYTTutFQqaQ==",
+ "version": "0.220.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-grpc/-/instrumentation-grpc-0.220.0.tgz",
+ "integrity": "sha512-U1EF8KKu52XwH2ybUkjVDmaVQZGf3mXirRSw1KJQrOV5aymgJgkPJV7+kRPqawZe0rpVc/BK+pPSyMWuQoyJJQ==",
"license": "Apache-2.0",
"dependencies": {
- "@opentelemetry/instrumentation": "0.57.2",
- "@opentelemetry/semantic-conventions": "1.28.0"
+ "@opentelemetry/instrumentation": "0.220.0",
+ "@opentelemetry/semantic-conventions": "^1.29.0"
},
"engines": {
- "node": ">=14"
+ "node": "^18.19.0 || >=20.6.0"
},
"peerDependencies": {
"@opentelemetry/api": "^1.3.0"
}
},
"node_modules/@opentelemetry/otlp-exporter-base": {
- "version": "0.57.2",
- "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.57.2.tgz",
- "integrity": "sha512-XdxEzL23Urhidyebg5E6jZoaiW5ygP/mRjxLHixogbqwDy2Faduzb5N0o/Oi+XTIJu+iyxXdVORjXax+Qgfxag==",
+ "version": "0.220.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.220.0.tgz",
+ "integrity": "sha512-CXYo8UD5Mn9YbgebO2EL4wejtA+gxLmLiu6HCk2KH2BR7XhFN6/6p1UlCb23DYCjeYkndevLHuejCCN1yx4+OQ==",
"license": "Apache-2.0",
"dependencies": {
- "@opentelemetry/core": "1.30.1",
- "@opentelemetry/otlp-transformer": "0.57.2"
+ "@opentelemetry/core": "2.9.0",
+ "@opentelemetry/otlp-transformer": "0.220.0"
},
"engines": {
- "node": ">=14"
+ "node": "^18.19.0 || >=20.6.0"
},
"peerDependencies": {
"@opentelemetry/api": "^1.3.0"
}
},
+ "node_modules/@opentelemetry/otlp-exporter-base/node_modules/@opentelemetry/core": {
+ "version": "2.9.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.9.0.tgz",
+ "integrity": "sha512-m2nckMT80NnmjTYSPjJQObBJ+8dgkoajEOUbznL8AHZ3T3yHRk2P7gI1PhEBc1+lOnrYE9UWrWHqJDsmqjmNbw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/semantic-conventions": "^1.29.0"
+ },
+ "engines": {
+ "node": "^18.19.0 || >=20.6.0"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": ">=1.0.0 <1.10.0"
+ }
+ },
"node_modules/@opentelemetry/otlp-grpc-exporter-base": {
- "version": "0.57.2",
- "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-grpc-exporter-base/-/otlp-grpc-exporter-base-0.57.2.tgz",
- "integrity": "sha512-USn173KTWy0saqqRB5yU9xUZ2xdgb1Rdu5IosJnm9aV4hMTuFFRTUsQxbgc24QxpCHeoKzzCSnS/JzdV0oM2iQ==",
+ "version": "0.220.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-grpc-exporter-base/-/otlp-grpc-exporter-base-0.220.0.tgz",
+ "integrity": "sha512-/eIkBPMBTIvM3x/0mDX4aJeSkYifYClnBPr68PL1h5LV4VQv4+SV6CGrpiZ4fIWDnobVmhTWCm1J/QRdAWUfvA==",
"license": "Apache-2.0",
"dependencies": {
- "@grpc/grpc-js": "^1.7.1",
- "@opentelemetry/core": "1.30.1",
- "@opentelemetry/otlp-exporter-base": "0.57.2",
- "@opentelemetry/otlp-transformer": "0.57.2"
+ "@grpc/grpc-js": "^1.14.3",
+ "@opentelemetry/core": "2.9.0",
+ "@opentelemetry/otlp-exporter-base": "0.220.0",
+ "@opentelemetry/otlp-transformer": "0.220.0"
},
"engines": {
- "node": ">=14"
+ "node": "^18.19.0 || >=20.6.0"
},
"peerDependencies": {
"@opentelemetry/api": "^1.3.0"
}
},
+ "node_modules/@opentelemetry/otlp-grpc-exporter-base/node_modules/@opentelemetry/core": {
+ "version": "2.9.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.9.0.tgz",
+ "integrity": "sha512-m2nckMT80NnmjTYSPjJQObBJ+8dgkoajEOUbznL8AHZ3T3yHRk2P7gI1PhEBc1+lOnrYE9UWrWHqJDsmqjmNbw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/semantic-conventions": "^1.29.0"
+ },
+ "engines": {
+ "node": "^18.19.0 || >=20.6.0"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": ">=1.0.0 <1.10.0"
+ }
+ },
"node_modules/@opentelemetry/otlp-transformer": {
- "version": "0.57.2",
- "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-transformer/-/otlp-transformer-0.57.2.tgz",
- "integrity": "sha512-48IIRj49gbQVK52jYsw70+Jv+JbahT8BqT2Th7C4H7RCM9d0gZ5sgNPoMpWldmfjvIsSgiGJtjfk9MeZvjhoig==",
+ "version": "0.220.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-transformer/-/otlp-transformer-0.220.0.tgz",
+ "integrity": "sha512-lXGrv7KXZ0gNH9SVNUaa6vv6phVYGvJxfXAlMbzbakiXru75f5MZl8Z7oqiMMQD77riVHJCFlQvbZs/VVN2/4A==",
"license": "Apache-2.0",
"dependencies": {
- "@opentelemetry/api-logs": "0.57.2",
- "@opentelemetry/core": "1.30.1",
- "@opentelemetry/resources": "1.30.1",
- "@opentelemetry/sdk-logs": "0.57.2",
- "@opentelemetry/sdk-metrics": "1.30.1",
- "@opentelemetry/sdk-trace-base": "1.30.1",
- "protobufjs": "^7.3.0"
+ "@opentelemetry/api-logs": "0.220.0",
+ "@opentelemetry/core": "2.9.0",
+ "@opentelemetry/resources": "2.9.0",
+ "@opentelemetry/sdk-logs": "0.220.0",
+ "@opentelemetry/sdk-metrics": "2.9.0",
+ "@opentelemetry/sdk-trace": "2.9.0"
},
"engines": {
- "node": ">=14"
+ "node": "^18.19.0 || >=20.6.0"
},
"peerDependencies": {
"@opentelemetry/api": "^1.3.0"
}
},
- "node_modules/@opentelemetry/propagator-b3": {
- "version": "1.30.1",
- "resolved": "https://registry.npmjs.org/@opentelemetry/propagator-b3/-/propagator-b3-1.30.1.tgz",
- "integrity": "sha512-oATwWWDIJzybAZ4pO76ATN5N6FFbOA1otibAVlS8v90B4S1wClnhRUk7K+2CHAwN1JKYuj4jh/lpCEG5BAqFuQ==",
+ "node_modules/@opentelemetry/otlp-transformer/node_modules/@opentelemetry/core": {
+ "version": "2.9.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.9.0.tgz",
+ "integrity": "sha512-m2nckMT80NnmjTYSPjJQObBJ+8dgkoajEOUbznL8AHZ3T3yHRk2P7gI1PhEBc1+lOnrYE9UWrWHqJDsmqjmNbw==",
"license": "Apache-2.0",
"dependencies": {
- "@opentelemetry/core": "1.30.1"
+ "@opentelemetry/semantic-conventions": "^1.29.0"
},
"engines": {
- "node": ">=14"
+ "node": "^18.19.0 || >=20.6.0"
},
"peerDependencies": {
"@opentelemetry/api": ">=1.0.0 <1.10.0"
}
},
- "node_modules/@opentelemetry/propagator-jaeger": {
- "version": "1.30.1",
- "resolved": "https://registry.npmjs.org/@opentelemetry/propagator-jaeger/-/propagator-jaeger-1.30.1.tgz",
- "integrity": "sha512-Pj/BfnYEKIOImirH76M4hDaBSx6HyZ2CXUqk+Kj02m6BB80c/yo4BdWkn/1gDFfU+YPY+bPR2U0DKBfdxCKwmg==",
+ "node_modules/@opentelemetry/otlp-transformer/node_modules/@opentelemetry/resources": {
+ "version": "2.9.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.9.0.tgz",
+ "integrity": "sha512-jyA5MBLQ+Dkl3+JsZkUoUvL7yHvU64kLsvpXKarWm6347Sl1t1bXFTFykUePNpT5WH5pm9a2Qtt03iIYQhZ1Fg==",
"license": "Apache-2.0",
"dependencies": {
- "@opentelemetry/core": "1.30.1"
+ "@opentelemetry/core": "2.9.0",
+ "@opentelemetry/semantic-conventions": "^1.29.0"
},
"engines": {
- "node": ">=14"
+ "node": "^18.19.0 || >=20.6.0"
},
"peerDependencies": {
- "@opentelemetry/api": ">=1.0.0 <1.10.0"
+ "@opentelemetry/api": ">=1.3.0 <1.10.0"
}
},
"node_modules/@opentelemetry/resources": {
- "version": "1.30.1",
- "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-1.30.1.tgz",
- "integrity": "sha512-5UxZqiAgLYGFjS4s9qm5mBVo433u+dSPUFWVWXmLAD4wB65oMCoXaJP1KJa9DIYYMeHu3z4BZcStG3LC593cWA==",
+ "version": "2.10.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.10.0.tgz",
+ "integrity": "sha512-q6MMm2zhggzsHVNbabYwut+a6nbuQQe3URUoxaojM/8K1IBfwwPzvxIjNi2/lI1TFe+fMHMW9MWhrtDLEXEnkA==",
"license": "Apache-2.0",
"dependencies": {
- "@opentelemetry/core": "1.30.1",
- "@opentelemetry/semantic-conventions": "1.28.0"
+ "@opentelemetry/core": "2.10.0",
+ "@opentelemetry/semantic-conventions": "^1.29.0"
},
"engines": {
- "node": ">=14"
+ "node": "^18.19.0 || >=20.6.0"
},
"peerDependencies": {
- "@opentelemetry/api": ">=1.0.0 <1.10.0"
+ "@opentelemetry/api": ">=1.3.0 <1.10.0"
}
},
"node_modules/@opentelemetry/sdk-logs": {
- "version": "0.57.2",
- "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.57.2.tgz",
- "integrity": "sha512-TXFHJ5c+BKggWbdEQ/inpgIzEmS2BGQowLE9UhsMd7YYlUfBQJ4uax0VF/B5NYigdM/75OoJGhAV3upEhK+3gg==",
+ "version": "0.220.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.220.0.tgz",
+ "integrity": "sha512-WywcTkQtv2iNmt+6y5Kcd4rzvx9bLVsBa2Nwcmg01IUaBTkTow3W4d9KE5vNBpEDtb9tp21WcRBY/lANRrApYA==",
"license": "Apache-2.0",
"dependencies": {
- "@opentelemetry/api-logs": "0.57.2",
- "@opentelemetry/core": "1.30.1",
- "@opentelemetry/resources": "1.30.1"
+ "@opentelemetry/api-logs": "0.220.0",
+ "@opentelemetry/core": "2.9.0",
+ "@opentelemetry/resources": "2.9.0",
+ "@opentelemetry/semantic-conventions": "^1.29.0"
},
"engines": {
- "node": ">=14"
+ "node": "^18.19.0 || >=20.6.0"
},
"peerDependencies": {
"@opentelemetry/api": ">=1.4.0 <1.10.0"
}
},
+ "node_modules/@opentelemetry/sdk-logs/node_modules/@opentelemetry/core": {
+ "version": "2.9.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.9.0.tgz",
+ "integrity": "sha512-m2nckMT80NnmjTYSPjJQObBJ+8dgkoajEOUbznL8AHZ3T3yHRk2P7gI1PhEBc1+lOnrYE9UWrWHqJDsmqjmNbw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/semantic-conventions": "^1.29.0"
+ },
+ "engines": {
+ "node": "^18.19.0 || >=20.6.0"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": ">=1.0.0 <1.10.0"
+ }
+ },
+ "node_modules/@opentelemetry/sdk-logs/node_modules/@opentelemetry/resources": {
+ "version": "2.9.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.9.0.tgz",
+ "integrity": "sha512-jyA5MBLQ+Dkl3+JsZkUoUvL7yHvU64kLsvpXKarWm6347Sl1t1bXFTFykUePNpT5WH5pm9a2Qtt03iIYQhZ1Fg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/core": "2.9.0",
+ "@opentelemetry/semantic-conventions": "^1.29.0"
+ },
+ "engines": {
+ "node": "^18.19.0 || >=20.6.0"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": ">=1.3.0 <1.10.0"
+ }
+ },
"node_modules/@opentelemetry/sdk-metrics": {
- "version": "1.30.1",
- "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-1.30.1.tgz",
- "integrity": "sha512-q9zcZ0Okl8jRgmy7eNW3Ku1XSgg3sDLa5evHZpCwjspw7E8Is4K/haRPDJrBcX3YSn/Y7gUvFnByNYEKQNbNog==",
+ "version": "2.9.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.9.0.tgz",
+ "integrity": "sha512-Xx8RGS4H5XEBl01WuCreMIpiah9cCXMbSkeuIePPdD2cUpq/vUzYmj8E/MK1OsbOc93FuAD4jfn2WOacKwLn7Q==",
"license": "Apache-2.0",
"dependencies": {
- "@opentelemetry/core": "1.30.1",
- "@opentelemetry/resources": "1.30.1"
+ "@opentelemetry/core": "2.9.0",
+ "@opentelemetry/resources": "2.9.0"
},
"engines": {
- "node": ">=14"
+ "node": "^18.19.0 || >=20.6.0"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": ">=1.9.0 <1.10.0"
+ }
+ },
+ "node_modules/@opentelemetry/sdk-metrics/node_modules/@opentelemetry/core": {
+ "version": "2.9.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.9.0.tgz",
+ "integrity": "sha512-m2nckMT80NnmjTYSPjJQObBJ+8dgkoajEOUbznL8AHZ3T3yHRk2P7gI1PhEBc1+lOnrYE9UWrWHqJDsmqjmNbw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/semantic-conventions": "^1.29.0"
+ },
+ "engines": {
+ "node": "^18.19.0 || >=20.6.0"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": ">=1.0.0 <1.10.0"
+ }
+ },
+ "node_modules/@opentelemetry/sdk-metrics/node_modules/@opentelemetry/resources": {
+ "version": "2.9.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.9.0.tgz",
+ "integrity": "sha512-jyA5MBLQ+Dkl3+JsZkUoUvL7yHvU64kLsvpXKarWm6347Sl1t1bXFTFykUePNpT5WH5pm9a2Qtt03iIYQhZ1Fg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/core": "2.9.0",
+ "@opentelemetry/semantic-conventions": "^1.29.0"
+ },
+ "engines": {
+ "node": "^18.19.0 || >=20.6.0"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": ">=1.3.0 <1.10.0"
+ }
+ },
+ "node_modules/@opentelemetry/sdk-trace": {
+ "version": "2.9.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace/-/sdk-trace-2.9.0.tgz",
+ "integrity": "sha512-sGA19HvtrrSKYsseHphluH6j3p6Xa3fqc7c7y8f/7mYWejc1lyDFcpSdD1kYa50HCLUeEo4zA5bW0pniaPszuw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/core": "2.9.0",
+ "@opentelemetry/resources": "2.9.0",
+ "@opentelemetry/semantic-conventions": "^1.29.0"
+ },
+ "engines": {
+ "node": "^18.19.0 || >=20.6.0"
},
"peerDependencies": {
"@opentelemetry/api": ">=1.3.0 <1.10.0"
}
},
"node_modules/@opentelemetry/sdk-trace-base": {
- "version": "1.30.1",
- "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-1.30.1.tgz",
- "integrity": "sha512-jVPgBbH1gCy2Lb7X0AVQ8XAfgg0pJ4nvl8/IiQA6nxOsPvS+0zMJaFSs2ltXe0J6C8dqjcnpyqINDJmU30+uOg==",
+ "version": "2.10.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.10.0.tgz",
+ "integrity": "sha512-GuYQQT7QD2EeO8lcZLRQzcbOyhqAzL+6WWTKTU9mSUBYBazkEDl+VrQcXQhbB08OWM9anD1aHleVadzulpOaUQ==",
"license": "Apache-2.0",
"dependencies": {
- "@opentelemetry/core": "1.30.1",
- "@opentelemetry/resources": "1.30.1",
- "@opentelemetry/semantic-conventions": "1.28.0"
+ "@opentelemetry/core": "2.10.0",
+ "@opentelemetry/resources": "2.10.0",
+ "@opentelemetry/sdk-trace": "2.10.0",
+ "@opentelemetry/semantic-conventions": "^1.29.0"
},
"engines": {
- "node": ">=14"
+ "node": "^18.19.0 || >=20.6.0"
},
"peerDependencies": {
- "@opentelemetry/api": ">=1.0.0 <1.10.0"
+ "@opentelemetry/api": ">=1.3.0 <1.10.0"
+ }
+ },
+ "node_modules/@opentelemetry/sdk-trace-base/node_modules/@opentelemetry/sdk-trace": {
+ "version": "2.10.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace/-/sdk-trace-2.10.0.tgz",
+ "integrity": "sha512-MfQGq3GRmTh5fM/y+OjaO0vj6+luCB1XO2gfXCalKCfgKw0eHL++sm75DNweC6ohlp+aFvACqeE0fYayqdRaoQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/core": "2.10.0",
+ "@opentelemetry/resources": "2.10.0",
+ "@opentelemetry/semantic-conventions": "^1.29.0"
+ },
+ "engines": {
+ "node": "^18.19.0 || >=20.6.0"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": ">=1.3.0 <1.10.0"
}
},
"node_modules/@opentelemetry/sdk-trace-node": {
- "version": "1.30.1",
- "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-node/-/sdk-trace-node-1.30.1.tgz",
- "integrity": "sha512-cBjYOINt1JxXdpw1e5MlHmFRc5fgj4GW/86vsKFxJCJ8AL4PdVtYH41gWwl4qd4uQjqEL1oJVrXkSy5cnduAnQ==",
+ "version": "2.10.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-node/-/sdk-trace-node-2.10.0.tgz",
+ "integrity": "sha512-GZK/G6oZyBLGlH1pUgeDch7D91KoHd2uotUGIkWCPi9GI5T9X0p4L7nNAMDR1BQjkRYoDqo+ddfVx9t5Uhys+Q==",
"license": "Apache-2.0",
"dependencies": {
- "@opentelemetry/context-async-hooks": "1.30.1",
- "@opentelemetry/core": "1.30.1",
- "@opentelemetry/propagator-b3": "1.30.1",
- "@opentelemetry/propagator-jaeger": "1.30.1",
- "@opentelemetry/sdk-trace-base": "1.30.1",
- "semver": "^7.5.2"
+ "@opentelemetry/context-async-hooks": "2.10.0",
+ "@opentelemetry/core": "2.10.0",
+ "@opentelemetry/sdk-trace-base": "2.10.0"
},
"engines": {
- "node": ">=14"
+ "node": "^18.19.0 || >=20.6.0"
},
"peerDependencies": {
"@opentelemetry/api": ">=1.0.0 <1.10.0"
}
},
+ "node_modules/@opentelemetry/sdk-trace/node_modules/@opentelemetry/core": {
+ "version": "2.9.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.9.0.tgz",
+ "integrity": "sha512-m2nckMT80NnmjTYSPjJQObBJ+8dgkoajEOUbznL8AHZ3T3yHRk2P7gI1PhEBc1+lOnrYE9UWrWHqJDsmqjmNbw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/semantic-conventions": "^1.29.0"
+ },
+ "engines": {
+ "node": "^18.19.0 || >=20.6.0"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": ">=1.0.0 <1.10.0"
+ }
+ },
+ "node_modules/@opentelemetry/sdk-trace/node_modules/@opentelemetry/resources": {
+ "version": "2.9.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.9.0.tgz",
+ "integrity": "sha512-jyA5MBLQ+Dkl3+JsZkUoUvL7yHvU64kLsvpXKarWm6347Sl1t1bXFTFykUePNpT5WH5pm9a2Qtt03iIYQhZ1Fg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/core": "2.9.0",
+ "@opentelemetry/semantic-conventions": "^1.29.0"
+ },
+ "engines": {
+ "node": "^18.19.0 || >=20.6.0"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": ">=1.3.0 <1.10.0"
+ }
+ },
"node_modules/@opentelemetry/semantic-conventions": {
- "version": "1.28.0",
- "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.28.0.tgz",
- "integrity": "sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA==",
+ "version": "1.43.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.43.0.tgz",
+ "integrity": "sha512-eSYWTm620tTk45EKSedaUL8MFYI8hW164hIXsgIHyxu3VobUB3fFCu5t0hQby6OoWRPsG1KkKUG2M5UadiLiVg==",
"license": "Apache-2.0",
"engines": {
"node": ">=14"
@@ -679,25 +856,24 @@
"license": "BSD-3-Clause"
},
"node_modules/@protobufjs/codegen": {
- "version": "2.0.4",
- "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz",
- "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==",
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.5.tgz",
+ "integrity": "sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==",
"license": "BSD-3-Clause"
},
"node_modules/@protobufjs/eventemitter": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz",
- "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==",
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.1.tgz",
+ "integrity": "sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg==",
"license": "BSD-3-Clause"
},
"node_modules/@protobufjs/fetch": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz",
- "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==",
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.1.tgz",
+ "integrity": "sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw==",
"license": "BSD-3-Clause",
"dependencies": {
- "@protobufjs/aspromise": "^1.1.1",
- "@protobufjs/inquire": "^1.1.0"
+ "@protobufjs/aspromise": "^1.1.1"
}
},
"node_modules/@protobufjs/float": {
@@ -706,12 +882,6 @@
"integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==",
"license": "BSD-3-Clause"
},
- "node_modules/@protobufjs/inquire": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz",
- "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==",
- "license": "BSD-3-Clause"
- },
"node_modules/@protobufjs/path": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz",
@@ -725,24 +895,24 @@
"license": "BSD-3-Clause"
},
"node_modules/@protobufjs/utf8": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz",
- "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==",
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.2.tgz",
+ "integrity": "sha512-b1UQwcEZ4yCnMCD8DAL1VlbvBJE9/IX4FTIp7BG1xYpf29SLazLSrqUkj4w7Y5y7cCVP6E5tcqqcI0xemPkHug==",
"license": "BSD-3-Clause"
},
"node_modules/@pulumi/cloudflare": {
- "version": "6.14.0",
- "resolved": "https://registry.npmjs.org/@pulumi/cloudflare/-/cloudflare-6.14.0.tgz",
- "integrity": "sha512-YKH9x7UsNg5iI92g9hVL893MzF8VS4W7nANFvr1TN99mQczPKNi0Z1abTBxyZ4DcB/JWi5zq+va7rJuKkuVr6Q==",
+ "version": "6.19.0",
+ "resolved": "https://registry.npmjs.org/@pulumi/cloudflare/-/cloudflare-6.19.0.tgz",
+ "integrity": "sha512-AP76ceRwCIws362nm7UpfvDvzI4zPaGThli0i3qsKqjektk1qNapyflA9g0LvpI2e6VEWAdTbkIK7WSucg00Sg==",
"license": "Apache-2.0",
"dependencies": {
"@pulumi/pulumi": "^3.142.0"
}
},
"node_modules/@pulumi/docker-build": {
- "version": "0.0.15",
- "resolved": "https://registry.npmjs.org/@pulumi/docker-build/-/docker-build-0.0.15.tgz",
- "integrity": "sha512-abtz4zCbePBkpj73M7mJWqXktM9muEAUOxRu8PKErbsqv5M3NfALCMULsDxA1a9vEImHa6ZC9rAe1liXbsinFg==",
+ "version": "0.0.22",
+ "resolved": "https://registry.npmjs.org/@pulumi/docker-build/-/docker-build-0.0.22.tgz",
+ "integrity": "sha512-DcnwoDv7kBM6MBhuEfkiATDwKDqGCNPICxUt7gbYSdpb/UuT+6K7pNMQu7mCNZV7feqZh9EJ3P20UXCq1R+YQg==",
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
@@ -750,9 +920,9 @@
}
},
"node_modules/@pulumi/kubernetes": {
- "version": "4.28.0",
- "resolved": "https://registry.npmjs.org/@pulumi/kubernetes/-/kubernetes-4.28.0.tgz",
- "integrity": "sha512-tdoUN7blJv1rrwr+2Pc55CP/+L1sMkH3z5Y7DI2yfNiGgdeaOHYia+hYsnLgR4HtBSfIfuBVWxGkZwGWOgUmOQ==",
+ "version": "4.33.0",
+ "resolved": "https://registry.npmjs.org/@pulumi/kubernetes/-/kubernetes-4.33.0.tgz",
+ "integrity": "sha512-tSMUDEWNl1pqOrANVqQRao5KRKB9waghbjpC29MUqCaqiVLZe6V0biVGC0TRDMy3hKXdP2VjiLAZ7DMnqts7rw==",
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
@@ -762,43 +932,39 @@
}
},
"node_modules/@pulumi/pulumi": {
- "version": "3.229.0",
- "resolved": "https://registry.npmjs.org/@pulumi/pulumi/-/pulumi-3.229.0.tgz",
- "integrity": "sha512-SoGAonHOrGP/NQQivq3gMz0nlebVUYOf6TgpsPHqvFARsHz4JIR+/fpFm/8JW+Ev5o6tvb6g04a+STmpdxoaOg==",
+ "version": "3.256.0",
+ "resolved": "https://registry.npmjs.org/@pulumi/pulumi/-/pulumi-3.256.0.tgz",
+ "integrity": "sha512-EK1TQieOmxTxbIpX5EKTPpC1WYappH3ASEZzN6Q9gs7rZjuZCATXI9Am8nszmlRdtPlAiVSW8NfQNKCnaNk34A==",
"license": "Apache-2.0",
"dependencies": {
"@grpc/grpc-js": "^1.10.1",
"@logdna/tail-file": "^2.0.6",
"@npmcli/arborist": "^9.0.0",
"@opentelemetry/api": "^1.9",
- "@opentelemetry/exporter-trace-otlp-grpc": "^0.57",
- "@opentelemetry/exporter-zipkin": "^1.30",
- "@opentelemetry/instrumentation": "^0.57",
- "@opentelemetry/instrumentation-grpc": "^0.57",
- "@opentelemetry/resources": "^1.30",
- "@opentelemetry/sdk-trace-base": "^1.30",
- "@opentelemetry/sdk-trace-node": "^1.30",
+ "@opentelemetry/core": "^2.9",
+ "@opentelemetry/exporter-trace-otlp-grpc": "^0.220",
+ "@opentelemetry/exporter-zipkin": "^2.9",
+ "@opentelemetry/instrumentation": "^0.220",
+ "@opentelemetry/instrumentation-grpc": "^0.220",
+ "@opentelemetry/resources": "^2.9",
+ "@opentelemetry/sdk-trace-base": "^2.9",
+ "@opentelemetry/sdk-trace-node": "^2.9",
+ "@opentelemetry/semantic-conventions": "^1.42",
"@types/google-protobuf": "^3.15.5",
"@types/semver": "^7.5.6",
- "@types/tmp": "^0.2.6",
"execa": "^5.1.0",
- "fdir": "^6.5.0",
"google-protobuf": "^3.21.4",
- "got": "^11.8.6",
"ini": "^2.0.0",
- "js-yaml": "^3.14.2",
+ "js-yaml": "^4.0.0",
"minimist": "^1.2.6",
"normalize-package-data": "^6.0.0",
- "package-directory": "^8.1.0",
- "picomatch": "^4.0.0",
"require-from-string": "^2.0.1",
"semver": "^7.5.2",
"source-map-support": "^0.5.6",
- "tmp": "^0.2.4",
"upath": "^1.1.0"
},
"engines": {
- "node": ">=20"
+ "node": ">=22"
},
"peerDependencies": {
"ts-node": ">= 7.0.1 < 12",
@@ -826,44 +992,44 @@
}
},
"node_modules/@sigstore/core": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/@sigstore/core/-/core-3.1.0.tgz",
- "integrity": "sha512-o5cw1QYhNQ9IroioJxpzexmPjfCe7gzafd2RY3qnMpxr4ZEja+Jad/U8sgFpaue6bOaF+z7RVkyKVV44FN+N8A==",
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/@sigstore/core/-/core-3.2.1.tgz",
+ "integrity": "sha512-qRsxPnCrbC/puegGxKuynfnxgLiHqWStrSjxkoB4YKqq3Z3s4cyZyj42ZdWFAEblNP65C+rBH8EuREHIXoi83g==",
"license": "Apache-2.0",
"engines": {
"node": "^20.17.0 || >=22.9.0"
}
},
"node_modules/@sigstore/protobuf-specs": {
- "version": "0.5.0",
- "resolved": "https://registry.npmjs.org/@sigstore/protobuf-specs/-/protobuf-specs-0.5.0.tgz",
- "integrity": "sha512-MM8XIwUjN2bwvCg1QvrMtbBmpcSHrkhFSCu1D11NyPvDQ25HEc4oG5/OcQfd/Tlf/OxmKWERDj0zGE23jQaMwA==",
+ "version": "0.5.1",
+ "resolved": "https://registry.npmjs.org/@sigstore/protobuf-specs/-/protobuf-specs-0.5.1.tgz",
+ "integrity": "sha512-/ScWUhhoFasJsSRGTVBwId1loQjjnjAfE4djL6ZhrXRpNCmPTnUKF5Jokd58ILseOMjzET3UrMOtJPS9sYeI0g==",
"license": "Apache-2.0",
"engines": {
"node": "^18.17.0 || >=20.5.0"
}
},
"node_modules/@sigstore/sign": {
- "version": "4.1.0",
- "resolved": "https://registry.npmjs.org/@sigstore/sign/-/sign-4.1.0.tgz",
- "integrity": "sha512-Vx1RmLxLGnSUqx/o5/VsCjkuN5L7y+vxEEwawvc7u+6WtX2W4GNa7b9HEjmcRWohw/d6BpATXmvOwc78m+Swdg==",
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/@sigstore/sign/-/sign-4.1.1.tgz",
+ "integrity": "sha512-Hf4xglukg0XXQ2RiD5vSoLjdPe8OBUPA8XeVjUObheuDcWdYWrnH/BNmxZCzkAy68MzmNCxXLeurJvs6hcP2OQ==",
"license": "Apache-2.0",
"dependencies": {
+ "@gar/promise-retry": "^1.0.2",
"@sigstore/bundle": "^4.0.0",
- "@sigstore/core": "^3.1.0",
+ "@sigstore/core": "^3.2.0",
"@sigstore/protobuf-specs": "^0.5.0",
- "make-fetch-happen": "^15.0.3",
- "proc-log": "^6.1.0",
- "promise-retry": "^2.0.1"
+ "make-fetch-happen": "^15.0.4",
+ "proc-log": "^6.1.0"
},
"engines": {
"node": "^20.17.0 || >=22.9.0"
}
},
"node_modules/@sigstore/tuf": {
- "version": "4.0.1",
- "resolved": "https://registry.npmjs.org/@sigstore/tuf/-/tuf-4.0.1.tgz",
- "integrity": "sha512-OPZBg8y5Vc9yZjmWCHrlWPMBqW5yd8+wFNl+thMdtcWz3vjVSoJQutF8YkrzI0SLGnkuFof4HSsWUhXrf219Lw==",
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/@sigstore/tuf/-/tuf-4.0.2.tgz",
+ "integrity": "sha512-TCAzTy0xzdP79EnxSjq9KQ3eaR7+FmudLC6eRKknVKZbV7ZNlGLClAAQb/HMNJ5n2OBNk2GT1tEmU0xuPr+SLQ==",
"license": "Apache-2.0",
"dependencies": {
"@sigstore/protobuf-specs": "^0.5.0",
@@ -874,43 +1040,19 @@
}
},
"node_modules/@sigstore/verify": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/@sigstore/verify/-/verify-3.1.0.tgz",
- "integrity": "sha512-mNe0Iigql08YupSOGv197YdHpPPr+EzDZmfCgMc7RPNaZTw5aLN01nBl6CHJOh3BGtnMIj83EeN4butBchc8Ag==",
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/@sigstore/verify/-/verify-3.1.1.tgz",
+ "integrity": "sha512-qv7+G3J2cc6wwFj3yKvXOamzqhMwSk1ogPGmhpS8iXllcPrJaIIBA+4HbttlHVu1pqWTdmaCH/WE7UOC51kdoA==",
"license": "Apache-2.0",
"dependencies": {
"@sigstore/bundle": "^4.0.0",
- "@sigstore/core": "^3.1.0",
+ "@sigstore/core": "^3.2.1",
"@sigstore/protobuf-specs": "^0.5.0"
},
"engines": {
"node": "^20.17.0 || >=22.9.0"
}
},
- "node_modules/@sindresorhus/is": {
- "version": "4.6.0",
- "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz",
- "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==",
- "license": "MIT",
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/sindresorhus/is?sponsor=1"
- }
- },
- "node_modules/@szmarczak/http-timer": {
- "version": "4.0.6",
- "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz",
- "integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==",
- "license": "MIT",
- "dependencies": {
- "defer-to-connect": "^2.0.0"
- },
- "engines": {
- "node": ">=10"
- }
- },
"node_modules/@tufjs/canonical-json": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@tufjs/canonical-json/-/canonical-json-2.0.0.tgz",
@@ -933,55 +1075,19 @@
"node": "^20.17.0 || >=22.9.0"
}
},
- "node_modules/@types/cacheable-request": {
- "version": "6.0.3",
- "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz",
- "integrity": "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==",
- "license": "MIT",
- "dependencies": {
- "@types/http-cache-semantics": "*",
- "@types/keyv": "^3.1.4",
- "@types/node": "*",
- "@types/responselike": "^1.0.0"
- }
- },
"node_modules/@types/google-protobuf": {
"version": "3.15.12",
"resolved": "https://registry.npmjs.org/@types/google-protobuf/-/google-protobuf-3.15.12.tgz",
"integrity": "sha512-40um9QqwHjRS92qnOaDpL7RmDK15NuZYo9HihiJRbYkMQZlWnuH8AdvbMy8/o6lgLmKbDUKa+OALCltHdbOTpQ==",
"license": "MIT"
},
- "node_modules/@types/http-cache-semantics": {
- "version": "4.2.0",
- "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz",
- "integrity": "sha512-L3LgimLHXtGkWikKnsPg0/VFx9OGZaC+eN1u4r+OB1XRqH3meBIAVC2zr1WdMH+RHmnRkqliQAOHNJ/E0j/e0Q==",
- "license": "MIT"
- },
- "node_modules/@types/keyv": {
- "version": "3.1.4",
- "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz",
- "integrity": "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==",
- "license": "MIT",
- "dependencies": {
- "@types/node": "*"
- }
- },
"node_modules/@types/node": {
- "version": "24.12.2",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.2.tgz",
- "integrity": "sha512-A1sre26ke7HDIuY/M23nd9gfB+nrmhtYyMINbjI1zHJxYteKR6qSMX56FsmjMcDb3SMcjJg5BiRRgOCC/yBD0g==",
+ "version": "24.13.3",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.3.tgz",
+ "integrity": "sha512-Dh8vAsV36ig5wa9OX4pXvMc9D3Veibfw2wix0CUwYODLD8nkj9UsLjASr49nPg+2eKzxhBV+v7L8pXvT4e639Q==",
"license": "MIT",
"dependencies": {
- "undici-types": "~7.16.0"
- }
- },
- "node_modules/@types/responselike": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.3.tgz",
- "integrity": "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==",
- "license": "MIT",
- "dependencies": {
- "@types/node": "*"
+ "undici-types": "~7.18.0"
}
},
"node_modules/@types/semver": {
@@ -990,18 +1096,6 @@
"integrity": "sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA==",
"license": "MIT"
},
- "node_modules/@types/shimmer": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/@types/shimmer/-/shimmer-1.2.0.tgz",
- "integrity": "sha512-UE7oxhQLLd9gub6JKIAhDq06T0F6FnztwMNRvYgjeQSBeMc1ZG/tA47EwfduvkuQS8apbkM/lpLpWsaCeYsXVg==",
- "license": "MIT"
- },
- "node_modules/@types/tmp": {
- "version": "0.2.6",
- "resolved": "https://registry.npmjs.org/@types/tmp/-/tmp-0.2.6.tgz",
- "integrity": "sha512-chhaNf2oKHlRkDGt+tiKE2Z5aJ6qalm7Z9rlLdBwmOiAAf09YQvvoLXjWK4HWPF1xU/fqvMgfNfpVoBscA/tKA==",
- "license": "MIT"
- },
"node_modules/abbrev": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-4.0.0.tgz",
@@ -1011,27 +1105,6 @@
"node": "^20.17.0 || >=22.9.0"
}
},
- "node_modules/acorn": {
- "version": "8.16.0",
- "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz",
- "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
- "license": "MIT",
- "bin": {
- "acorn": "bin/acorn"
- },
- "engines": {
- "node": ">=0.4.0"
- }
- },
- "node_modules/acorn-import-attributes": {
- "version": "1.9.5",
- "resolved": "https://registry.npmjs.org/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz",
- "integrity": "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==",
- "license": "MIT",
- "peerDependencies": {
- "acorn": "^8"
- }
- },
"node_modules/agent-base": {
"version": "7.1.4",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
@@ -1041,15 +1114,36 @@
"node": ">= 14"
}
},
- "node_modules/argparse": {
- "version": "1.0.10",
- "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
- "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
+ "node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"license": "MIT",
"dependencies": {
- "sprintf-js": "~1.0.2"
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
+ "node_modules/argparse": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
+ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
+ "license": "Python-2.0"
+ },
"node_modules/balanced-match": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
@@ -1076,15 +1170,15 @@
}
},
"node_modules/brace-expansion": {
- "version": "5.0.5",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz",
- "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==",
+ "version": "5.0.9",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz",
+ "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==",
"license": "MIT",
"dependencies": {
"balanced-match": "^4.0.2"
},
"engines": {
- "node": "18 || 20 || >=22"
+ "node": "20 || >=22"
}
},
"node_modules/buffer-from": {
@@ -1132,48 +1226,6 @@
"url": "https://github.com/sponsors/isaacs"
}
},
- "node_modules/cacheable-lookup": {
- "version": "5.0.4",
- "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz",
- "integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==",
- "license": "MIT",
- "engines": {
- "node": ">=10.6.0"
- }
- },
- "node_modules/cacheable-request": {
- "version": "7.0.4",
- "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.4.tgz",
- "integrity": "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==",
- "license": "MIT",
- "dependencies": {
- "clone-response": "^1.0.2",
- "get-stream": "^5.1.0",
- "http-cache-semantics": "^4.0.0",
- "keyv": "^4.0.0",
- "lowercase-keys": "^2.0.0",
- "normalize-url": "^6.0.1",
- "responselike": "^2.0.0"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/cacheable-request/node_modules/get-stream": {
- "version": "5.2.0",
- "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz",
- "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==",
- "license": "MIT",
- "dependencies": {
- "pump": "^3.0.0"
- },
- "engines": {
- "node": ">=8"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
"node_modules/chownr": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz",
@@ -1184,9 +1236,9 @@
}
},
"node_modules/cjs-module-lexer": {
- "version": "1.4.3",
- "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz",
- "integrity": "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==",
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-2.2.0.tgz",
+ "integrity": "sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ==",
"license": "MIT"
},
"node_modules/cliui": {
@@ -1203,91 +1255,6 @@
"node": ">=12"
}
},
- "node_modules/cliui/node_modules/ansi-regex": {
- "version": "5.0.1",
- "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
- "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
- "license": "MIT",
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/cliui/node_modules/ansi-styles": {
- "version": "4.3.0",
- "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
- "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
- "license": "MIT",
- "dependencies": {
- "color-convert": "^2.0.1"
- },
- "engines": {
- "node": ">=8"
- },
- "funding": {
- "url": "https://github.com/chalk/ansi-styles?sponsor=1"
- }
- },
- "node_modules/cliui/node_modules/emoji-regex": {
- "version": "8.0.0",
- "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
- "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
- "license": "MIT"
- },
- "node_modules/cliui/node_modules/string-width": {
- "version": "4.2.3",
- "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
- "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
- "license": "MIT",
- "dependencies": {
- "emoji-regex": "^8.0.0",
- "is-fullwidth-code-point": "^3.0.0",
- "strip-ansi": "^6.0.1"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/cliui/node_modules/strip-ansi": {
- "version": "6.0.1",
- "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
- "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
- "license": "MIT",
- "dependencies": {
- "ansi-regex": "^5.0.1"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/cliui/node_modules/wrap-ansi": {
- "version": "7.0.0",
- "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
- "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
- "license": "MIT",
- "dependencies": {
- "ansi-styles": "^4.0.0",
- "string-width": "^4.1.0",
- "strip-ansi": "^6.0.0"
- },
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
- }
- },
- "node_modules/clone-response": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz",
- "integrity": "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==",
- "license": "MIT",
- "dependencies": {
- "mimic-response": "^1.0.0"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
"node_modules/cmd-shim": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/cmd-shim/-/cmd-shim-8.0.0.tgz",
@@ -1388,41 +1355,11 @@
}
}
},
- "node_modules/decompress-response": {
- "version": "6.0.0",
- "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
- "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==",
- "license": "MIT",
- "dependencies": {
- "mimic-response": "^3.1.0"
- },
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/decompress-response/node_modules/mimic-response": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz",
- "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==",
- "license": "MIT",
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/defer-to-connect": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz",
- "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==",
- "license": "MIT",
- "engines": {
- "node": ">=10"
- }
+ "node_modules/emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "license": "MIT"
},
"node_modules/encoding": {
"version": "0.1.13",
@@ -1434,15 +1371,6 @@
"iconv-lite": "^0.6.2"
}
},
- "node_modules/end-of-stream": {
- "version": "1.4.5",
- "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz",
- "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==",
- "license": "MIT",
- "dependencies": {
- "once": "^1.4.0"
- }
- },
"node_modules/env-paths": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz",
@@ -1458,6 +1386,12 @@
"integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==",
"license": "MIT"
},
+ "node_modules/es-module-lexer": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.1.tgz",
+ "integrity": "sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==",
+ "license": "MIT"
+ },
"node_modules/escalade": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
@@ -1467,19 +1401,6 @@
"node": ">=6"
}
},
- "node_modules/esprima": {
- "version": "4.0.1",
- "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
- "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==",
- "license": "BSD-2-Clause",
- "bin": {
- "esparse": "bin/esparse.js",
- "esvalidate": "bin/esvalidate.js"
- },
- "engines": {
- "node": ">=4"
- }
- },
"node_modules/execa": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz",
@@ -1526,18 +1447,6 @@
}
}
},
- "node_modules/find-up-simple": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/find-up-simple/-/find-up-simple-1.0.1.tgz",
- "integrity": "sha512-afd4O7zpqHeRyg4PfDQsXmlDe2PfdHtJt6Akt8jOWaApLOZk5JXs6VMR29lz03pRe9mpykrRCYIYxaJYcfpncQ==",
- "license": "MIT",
- "engines": {
- "node": ">=18"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
"node_modules/foreground-child": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz",
@@ -1578,15 +1487,6 @@
"node": "^14.17.0 || ^16.13.0 || >=18.0.0"
}
},
- "node_modules/function-bind": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
- "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
- "license": "MIT",
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
"node_modules/get-caller-file": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
@@ -1637,49 +1537,12 @@
"integrity": "sha512-MnG7N936zcKTco4Jd2PX2U96Kf9PxygAPKBug+74LHzmHXmceN16MmRcdgZv+DGef/S9YvQAfRsNCn4cjf9yyQ==",
"license": "(BSD-3-Clause AND Apache-2.0)"
},
- "node_modules/got": {
- "version": "11.8.6",
- "resolved": "https://registry.npmjs.org/got/-/got-11.8.6.tgz",
- "integrity": "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==",
- "license": "MIT",
- "dependencies": {
- "@sindresorhus/is": "^4.0.0",
- "@szmarczak/http-timer": "^4.0.5",
- "@types/cacheable-request": "^6.0.1",
- "@types/responselike": "^1.0.0",
- "cacheable-lookup": "^5.0.3",
- "cacheable-request": "^7.0.2",
- "decompress-response": "^6.0.0",
- "http2-wrapper": "^1.0.0-beta.5.2",
- "lowercase-keys": "^2.0.0",
- "p-cancelable": "^2.0.0",
- "responselike": "^2.0.0"
- },
- "engines": {
- "node": ">=10.19.0"
- },
- "funding": {
- "url": "https://github.com/sindresorhus/got?sponsor=1"
- }
- },
"node_modules/graceful-fs": {
"version": "4.2.11",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
"license": "ISC"
},
- "node_modules/hasown": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
- "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
- "license": "MIT",
- "dependencies": {
- "function-bind": "^1.1.2"
- },
- "engines": {
- "node": ">= 0.4"
- }
- },
"node_modules/hosted-git-info": {
"version": "9.0.2",
"resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-9.0.2.tgz",
@@ -1711,19 +1574,6 @@
"node": ">= 14"
}
},
- "node_modules/http2-wrapper": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz",
- "integrity": "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==",
- "license": "MIT",
- "dependencies": {
- "quick-lru": "^5.1.1",
- "resolve-alpn": "^1.0.0"
- },
- "engines": {
- "node": ">=10.19.0"
- }
- },
"node_modules/https-proxy-agent": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
@@ -1772,15 +1622,17 @@
}
},
"node_modules/import-in-the-middle": {
- "version": "1.15.0",
- "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-1.15.0.tgz",
- "integrity": "sha512-bpQy+CrsRmYmoPMAE/0G33iwRqwW4ouqdRg8jgbH3aKuCtOc8lxgmYXg2dMM92CRiGP660EtBcymH/eVUpCSaA==",
+ "version": "3.3.3",
+ "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-3.3.3.tgz",
+ "integrity": "sha512-AiohS3H80sXO6owEltjGX+glb7qXaDhBoJb9XcQVH4UI207xu/bDLUcadVKp7Qe576reg9yr/PXZjV5qx8gfbA==",
"license": "Apache-2.0",
"dependencies": {
- "acorn": "^8.14.0",
- "acorn-import-attributes": "^1.9.5",
- "cjs-module-lexer": "^1.2.2",
- "module-details-from-path": "^1.0.3"
+ "cjs-module-lexer": "^2.2.0",
+ "es-module-lexer": "^2.2.0",
+ "module-details-from-path": "^1.0.4"
+ },
+ "engines": {
+ "node": ">=18"
}
},
"node_modules/imurmurhash": {
@@ -1802,29 +1654,14 @@
}
},
"node_modules/ip-address": {
- "version": "10.1.0",
- "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz",
- "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==",
+ "version": "10.4.0",
+ "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.4.0.tgz",
+ "integrity": "sha512-oSK96Grm3aP6OrS263xVxbNDGVL7rzBtYdpGqlDG8iQdoenDoTs/nkki+DflYbAEE8Xl6o5YxhxlrKvI3nqKXQ==",
"license": "MIT",
"engines": {
"node": ">= 12"
}
},
- "node_modules/is-core-module": {
- "version": "2.16.1",
- "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz",
- "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==",
- "license": "MIT",
- "dependencies": {
- "hasown": "^2.0.2"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
"node_modules/is-fullwidth-code-point": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
@@ -1871,24 +1708,27 @@
}
},
"node_modules/js-yaml": {
- "version": "3.14.2",
- "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz",
- "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==",
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.1.tgz",
+ "integrity": "sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/puzrin"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/nodeca"
+ }
+ ],
"license": "MIT",
"dependencies": {
- "argparse": "^1.0.7",
- "esprima": "^4.0.0"
+ "argparse": "^2.0.1"
},
"bin": {
"js-yaml": "bin/js-yaml.js"
}
},
- "node_modules/json-buffer": {
- "version": "3.0.1",
- "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
- "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
- "license": "MIT"
- },
"node_modules/json-parse-even-better-errors": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-5.0.0.tgz",
@@ -1928,15 +1768,6 @@
"integrity": "sha512-OYTthRfSh55WOItVqwpefPtNt2VdKsq5AnAK6apdtR6yCH8pr0CmSr710J0Mf+WdQy7K/OzMy7K2MgAfdQURDw==",
"license": "MIT"
},
- "node_modules/keyv": {
- "version": "4.5.4",
- "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
- "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
- "license": "MIT",
- "dependencies": {
- "json-buffer": "3.0.1"
- }
- },
"node_modules/lodash.camelcase": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz",
@@ -1949,15 +1780,6 @@
"integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==",
"license": "Apache-2.0"
},
- "node_modules/lowercase-keys": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz",
- "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==",
- "license": "MIT",
- "engines": {
- "node": ">=8"
- }
- },
"node_modules/lru-cache": {
"version": "11.2.5",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.5.tgz",
@@ -1968,12 +1790,14 @@
}
},
"node_modules/make-fetch-happen": {
- "version": "15.0.3",
- "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-15.0.3.tgz",
- "integrity": "sha512-iyyEpDty1mwW3dGlYXAJqC/azFn5PPvgKVwXayOGBSmKLxhKZ9fg4qIan2ePpp1vJIwfFiO34LAPZgq9SZW9Aw==",
+ "version": "15.0.6",
+ "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-15.0.6.tgz",
+ "integrity": "sha512-Je0fLJ0F5atA7F+eIlLzk+Wkcl57JDf4kf+EW8xiP5E31xOQxkIxTbgf1Oi1Lw9tRI9UEMRdI5Vz2xTzoNU1Jw==",
"license": "ISC",
"dependencies": {
+ "@gar/promise-retry": "^1.0.0",
"@npmcli/agent": "^4.0.0",
+ "@npmcli/redact": "^4.0.0",
"cacache": "^20.0.1",
"http-cache-semantics": "^4.1.1",
"minipass": "^7.0.2",
@@ -1982,7 +1806,6 @@
"minipass-pipeline": "^1.2.4",
"negotiator": "^1.0.0",
"proc-log": "^6.0.0",
- "promise-retry": "^2.0.1",
"ssri": "^13.0.0"
},
"engines": {
@@ -2004,15 +1827,6 @@
"node": ">=6"
}
},
- "node_modules/mimic-response": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz",
- "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==",
- "license": "MIT",
- "engines": {
- "node": ">=4"
- }
- },
"node_modules/minimatch": {
"version": "10.2.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz",
@@ -2269,18 +2083,6 @@
"integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
"license": "ISC"
},
- "node_modules/normalize-url": {
- "version": "6.1.0",
- "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz",
- "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==",
- "license": "MIT",
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
"node_modules/npm-bundled": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-5.0.0.tgz",
@@ -2388,15 +2190,6 @@
"node": ">=8"
}
},
- "node_modules/once": {
- "version": "1.4.0",
- "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
- "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
- "license": "ISC",
- "dependencies": {
- "wrappy": "1"
- }
- },
"node_modules/onetime": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz",
@@ -2412,15 +2205,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
- "node_modules/p-cancelable": {
- "version": "2.1.1",
- "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz",
- "integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==",
- "license": "MIT",
- "engines": {
- "node": ">=8"
- }
- },
"node_modules/p-map": {
"version": "7.0.4",
"resolved": "https://registry.npmjs.org/p-map/-/p-map-7.0.4.tgz",
@@ -2433,21 +2217,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
- "node_modules/package-directory": {
- "version": "8.1.0",
- "resolved": "https://registry.npmjs.org/package-directory/-/package-directory-8.1.0.tgz",
- "integrity": "sha512-qHKRW0pw3lYdZMQVkjDBqh8HlamH/LCww2PH7OWEp4Qrt3SFeYMNpnJrQzlSnGrDD5zGR51XqBh7FnNCdVNEHA==",
- "license": "MIT",
- "dependencies": {
- "find-up-simple": "^1.0.0"
- },
- "engines": {
- "node": ">=18"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
"node_modules/package-json-from-dist": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
@@ -2508,12 +2277,6 @@
"node": ">=8"
}
},
- "node_modules/path-parse": {
- "version": "1.0.7",
- "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
- "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
- "license": "MIT"
- },
"node_modules/path-scurry": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz",
@@ -2605,51 +2368,28 @@
}
},
"node_modules/protobufjs": {
- "version": "7.5.4",
- "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.4.tgz",
- "integrity": "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==",
+ "version": "7.6.5",
+ "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.6.5.tgz",
+ "integrity": "sha512-/FPD0nUc9jH6rfFjji9IBqOz4pcSE3CsT1m7Ep6Mdb0LxSUMj8hgl6GomOvZzpNpAqqGaXA0P3VSrZLFzIhQrw==",
"hasInstallScript": true,
"license": "BSD-3-Clause",
"dependencies": {
"@protobufjs/aspromise": "^1.1.2",
"@protobufjs/base64": "^1.1.2",
- "@protobufjs/codegen": "^2.0.4",
- "@protobufjs/eventemitter": "^1.1.0",
- "@protobufjs/fetch": "^1.1.0",
+ "@protobufjs/codegen": "^2.0.5",
+ "@protobufjs/eventemitter": "^1.1.1",
+ "@protobufjs/fetch": "^1.1.1",
"@protobufjs/float": "^1.0.2",
- "@protobufjs/inquire": "^1.1.0",
"@protobufjs/path": "^1.1.2",
"@protobufjs/pool": "^1.1.0",
- "@protobufjs/utf8": "^1.1.0",
+ "@protobufjs/utf8": "^1.1.1",
"@types/node": ">=13.7.0",
- "long": "^5.0.0"
+ "long": "^5.3.2"
},
"engines": {
"node": ">=12.0.0"
}
},
- "node_modules/pump": {
- "version": "3.0.3",
- "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz",
- "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==",
- "license": "MIT",
- "dependencies": {
- "end-of-stream": "^1.1.0",
- "once": "^1.3.1"
- }
- },
- "node_modules/quick-lru": {
- "version": "5.1.1",
- "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz",
- "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==",
- "license": "MIT",
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
"node_modules/read-cmd-shim": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/read-cmd-shim/-/read-cmd-shim-6.0.0.tgz",
@@ -2678,55 +2418,16 @@
}
},
"node_modules/require-in-the-middle": {
- "version": "7.5.2",
- "resolved": "https://registry.npmjs.org/require-in-the-middle/-/require-in-the-middle-7.5.2.tgz",
- "integrity": "sha512-gAZ+kLqBdHarXB64XpAe2VCjB7rIRv+mU8tfRWziHRJ5umKsIHN2tLLv6EtMw7WCdP19S0ERVMldNvxYCHnhSQ==",
+ "version": "8.0.1",
+ "resolved": "https://registry.npmjs.org/require-in-the-middle/-/require-in-the-middle-8.0.1.tgz",
+ "integrity": "sha512-QT7FVMXfWOYFbeRBF6nu+I6tr2Tf3u0q8RIEjNob/heKY/nh7drD/k7eeMFmSQgnTtCzLDcCu/XEnpW2wk4xCQ==",
"license": "MIT",
"dependencies": {
"debug": "^4.3.5",
- "module-details-from-path": "^1.0.3",
- "resolve": "^1.22.8"
- },
- "engines": {
- "node": ">=8.6.0"
- }
- },
- "node_modules/resolve": {
- "version": "1.22.11",
- "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz",
- "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==",
- "license": "MIT",
- "dependencies": {
- "is-core-module": "^2.16.1",
- "path-parse": "^1.0.7",
- "supports-preserve-symlinks-flag": "^1.0.0"
- },
- "bin": {
- "resolve": "bin/resolve"
+ "module-details-from-path": "^1.0.3"
},
"engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/resolve-alpn": {
- "version": "1.2.1",
- "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz",
- "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==",
- "license": "MIT"
- },
- "node_modules/responselike": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz",
- "integrity": "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==",
- "license": "MIT",
- "dependencies": {
- "lowercase-keys": "^2.0.0"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
+ "node": ">=9.3.0 || >=8.10.0 <9.0.0"
}
},
"node_modules/retry": {
@@ -2779,9 +2480,9 @@
}
},
"node_modules/shell-quote": {
- "version": "1.8.3",
- "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz",
- "integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==",
+ "version": "1.10.0",
+ "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.10.0.tgz",
+ "integrity": "sha512-w1aiOKwKuRgtwAReIIj89puqg+I7GvX4IbLrvmhXbzQsj1+Zwi4VO3+fa6ZF91TWSjIxoEkKnMeHcLEODK5ZXA==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
@@ -2790,12 +2491,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/shimmer": {
- "version": "1.2.1",
- "resolved": "https://registry.npmjs.org/shimmer/-/shimmer-1.2.1.tgz",
- "integrity": "sha512-sQTKC1Re/rM6XyFM6fIAGHRPVGvyXfgzIDvzoq608vM+jeyVD0Tu1E6Np0Kc2zAIFWIj963V2800iF/9LPieQw==",
- "license": "BSD-2-Clause"
- },
"node_modules/signal-exit": {
"version": "3.0.7",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
@@ -2803,17 +2498,17 @@
"license": "ISC"
},
"node_modules/sigstore": {
- "version": "4.1.0",
- "resolved": "https://registry.npmjs.org/sigstore/-/sigstore-4.1.0.tgz",
- "integrity": "sha512-/fUgUhYghuLzVT/gaJoeVehLCgZiUxPCPMcyVNY0lIf/cTCz58K/WTI7PefDarXxp9nUKpEwg1yyz3eSBMTtgA==",
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/sigstore/-/sigstore-4.1.1.tgz",
+ "integrity": "sha512-endqECJkfhozrXMK5ngu/UAA0xVcVEFdnHJCElGaExypjW+HK5i6zu3NteLoaX/iFbRUbC3+DjttQs0GARr+5w==",
"license": "Apache-2.0",
"dependencies": {
"@sigstore/bundle": "^4.0.0",
- "@sigstore/core": "^3.1.0",
+ "@sigstore/core": "^3.2.1",
"@sigstore/protobuf-specs": "^0.5.0",
- "@sigstore/sign": "^4.1.0",
- "@sigstore/tuf": "^4.0.1",
- "@sigstore/verify": "^3.1.0"
+ "@sigstore/sign": "^4.1.1",
+ "@sigstore/tuf": "^4.0.2",
+ "@sigstore/verify": "^3.1.1"
},
"engines": {
"node": "^20.17.0 || >=22.9.0"
@@ -2908,12 +2603,6 @@
"integrity": "sha512-4PRT4nh1EImPbt2jASOKHX7PB7I+e4IWNLvkKFDxNhJlfjbYlleYQh285Z/3mPTHSAK/AvdMmw5BNNuYH8ShgQ==",
"license": "CC0-1.0"
},
- "node_modules/sprintf-js": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
- "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==",
- "license": "BSD-3-Clause"
- },
"node_modules/ssri": {
"version": "13.0.0",
"resolved": "https://registry.npmjs.org/ssri/-/ssri-13.0.0.tgz",
@@ -2926,6 +2615,32 @@
"node": "^20.17.0 || >=22.9.0"
}
},
+ "node_modules/string-width": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "license": "MIT",
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/strip-final-newline": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz",
@@ -2935,22 +2650,10 @@
"node": ">=6"
}
},
- "node_modules/supports-preserve-symlinks-flag": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
- "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
"node_modules/tar": {
- "version": "7.5.13",
- "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.13.tgz",
- "integrity": "sha512-tOG/7GyXpFevhXVh8jOPJrmtRpOTsYqUIkVdVooZYJS/z8WhfQUX8RJILmeuJNinGAMSu1veBr4asSHFt5/hng==",
+ "version": "7.5.22",
+ "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.22.tgz",
+ "integrity": "sha512-MFO/QzvtAOmJbkhOaCTvbGcFN9L9b+JunIsDwaKljSOdcLMea3NJ1k9Usz/rjdfSXTq4dfzfeS7W4p4YOAAHeA==",
"license": "BlueOak-1.0.0",
"dependencies": {
"@isaacs/fs-minipass": "^4.0.0",
@@ -2979,15 +2682,6 @@
"url": "https://github.com/sponsors/SuperchupuDev"
}
},
- "node_modules/tmp": {
- "version": "0.2.5",
- "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz",
- "integrity": "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==",
- "license": "MIT",
- "engines": {
- "node": ">=14.14"
- }
- },
"node_modules/treeverse": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/treeverse/-/treeverse-3.0.0.tgz",
@@ -3026,9 +2720,9 @@
}
},
"node_modules/undici-types": {
- "version": "7.16.0",
- "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
- "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==",
+ "version": "7.18.2",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz",
+ "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==",
"license": "MIT"
},
"node_modules/unique-filename": {
@@ -3114,11 +2808,22 @@
"node": "^20.17.0 || >=22.9.0"
}
},
- "node_modules/wrappy": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
- "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
- "license": "ISC"
+ "node_modules/wrap-ansi": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
+ "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.0.0",
+ "string-width": "^4.1.0",
+ "strip-ansi": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+ }
},
"node_modules/write-file-atomic": {
"version": "7.0.0",
@@ -3164,9 +2869,9 @@
}
},
"node_modules/yargs": {
- "version": "17.7.2",
- "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
- "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==",
+ "version": "17.7.3",
+ "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.3.tgz",
+ "integrity": "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==",
"license": "MIT",
"dependencies": {
"cliui": "^8.0.1",
@@ -3189,47 +2894,6 @@
"engines": {
"node": ">=12"
}
- },
- "node_modules/yargs/node_modules/ansi-regex": {
- "version": "5.0.1",
- "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
- "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
- "license": "MIT",
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/yargs/node_modules/emoji-regex": {
- "version": "8.0.0",
- "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
- "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
- "license": "MIT"
- },
- "node_modules/yargs/node_modules/string-width": {
- "version": "4.2.3",
- "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
- "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
- "license": "MIT",
- "dependencies": {
- "emoji-regex": "^8.0.0",
- "is-fullwidth-code-point": "^3.0.0",
- "strip-ansi": "^6.0.1"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/yargs/node_modules/strip-ansi": {
- "version": "6.0.1",
- "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
- "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
- "license": "MIT",
- "dependencies": {
- "ansi-regex": "^5.0.1"
- },
- "engines": {
- "node": ">=8"
- }
}
}
}
diff --git a/pulumi/package.json b/pulumi/package.json
index 016d3d5..ac5057f 100644
--- a/pulumi/package.json
+++ b/pulumi/package.json
@@ -2,13 +2,13 @@
"name": "fsharp-view-engine",
"main": "index.ts",
"devDependencies": {
- "@types/node": "^24.12.2",
+ "@types/node": "^24.13.3",
"typescript": "^5.9.3"
},
"dependencies": {
- "@pulumi/cloudflare": "^6.14.0",
- "@pulumi/docker-build": "^0.0.15",
- "@pulumi/kubernetes": "^4.28",
- "@pulumi/pulumi": "^3.229"
+ "@pulumi/cloudflare": "^6.19.0",
+ "@pulumi/docker-build": "^0.0.22",
+ "@pulumi/kubernetes": "^4.33.0",
+ "@pulumi/pulumi": "^3.256.0"
}
}
diff --git a/pulumi/src/config.ts b/pulumi/src/config.ts
index 136bae0..11b1f2e 100644
--- a/pulumi/src/config.ts
+++ b/pulumi/src/config.ts
@@ -5,6 +5,11 @@ export const rootDir = path.dirname(path.dirname(__dirname))
export const identifier = 'fsharpviewengine'
+export const releaseConfig = {
+ version: process.env.RELEASE_VERSION || 'development',
+ commit: process.env.RELEASE_COMMIT || 'local',
+}
+
const rawDockerConfig = new pulumi.Config('docker')
export const dockerConfig = {
@@ -18,7 +23,7 @@ export const cloudflareConfig = {
accountId: rawCloudflareConfig.require('accountId'),
apiToken: rawCloudflareConfig.requireSecret('apiToken'),
zoneName: rawCloudflareConfig.require('zoneName'),
- cloudflaredVersion: '2026.2.0'
+ cloudflaredVersion: '2026.7.3'
}
const rawK8sConfig = new pulumi.Config('k8s')
diff --git a/pulumi/src/k8s/deployment.ts b/pulumi/src/k8s/deployment.ts
index aa47b3d..2b77f9e 100644
--- a/pulumi/src/k8s/deployment.ts
+++ b/pulumi/src/k8s/deployment.ts
@@ -62,6 +62,10 @@ const deployment = new k8s.apps.v1.Deployment(config.identifier, {
securityContext: containerSecurityContext,
imagePullPolicy: 'IfNotPresent',
envFrom: [ { configMapRef: { name: appConfigMap.metadata.name } } ],
+ env: [
+ { name: 'RELEASE_VERSION', value: config.releaseConfig.version },
+ { name: 'RELEASE_COMMIT', value: config.releaseConfig.commit },
+ ],
resources: {
requests: { cpu: '25m', memory: '64Mi' },
limits: { cpu: '250m', memory: '256Mi' },
diff --git a/sln/Dockerfile b/sln/Dockerfile
index 8259f90..3861000 100644
--- a/sln/Dockerfile
+++ b/sln/Dockerfile
@@ -4,7 +4,7 @@ WORKDIR /app
# install tailwindcss v4 CLI (match builder platform)
RUN ARCH=$(uname -m | sed 's/aarch64/arm64/' | sed 's/x86_64/x64/') \
- && curl -fsSLo /usr/local/bin/tailwindcss "https://github.com/tailwindlabs/tailwindcss/releases/download/v4.2.1/tailwindcss-linux-${ARCH}" \
+ && curl -fsSLo /usr/local/bin/tailwindcss "https://github.com/tailwindlabs/tailwindcss/releases/download/v4.3.3/tailwindcss-linux-${ARCH}" \
&& chmod +x /usr/local/bin/tailwindcss
# restore tools
diff --git a/sln/FSharp.ViewEngine.slnx b/sln/FSharp.ViewEngine.slnx
index 8a66231..5161675 100644
--- a/sln/FSharp.ViewEngine.slnx
+++ b/sln/FSharp.ViewEngine.slnx
@@ -3,6 +3,7 @@
+
diff --git a/sln/fake.sh b/sln/fake.sh
index d25bbc3..2119f65 100755
--- a/sln/fake.sh
+++ b/sln/fake.sh
@@ -1 +1 @@
-dotnet run --project ./src/Build/Build.fsproj -- --target $1
+dotnet run --project ./src/Build/Build.fsproj -- --target "$@"
diff --git a/sln/paket.dependencies b/sln/paket.dependencies
index 7f39ae1..615279f 100644
--- a/sln/paket.dependencies
+++ b/sln/paket.dependencies
@@ -2,16 +2,11 @@ source https://api.nuget.org/v3/index.json
storage: none
nuget Fake.Core.Target
-nuget Giraffe
-nuget JetBrains.Annotations
-nuget Markdig
-nuget Expecto
-nuget BenchmarkDotNet 0.14.0
-nuget FSharp.Core
-nuget Giraffe.ViewEngine
-nuget Feliz.ViewEngine
-nuget Oxpecker.ViewEngine
-nuget Serilog
+nuget Fake.Tools.Git 6.1.4
+nuget Giraffe 8.3.0
+nuget Expecto 11.1.0
+nuget FSharp.Core 10.0.102
+nuget Serilog 4.4.0
nuget Serilog.AspNetCore
nuget Serilog.Sinks.Console
nuget Serilog.Sinks.OpenTelemetry
diff --git a/sln/paket.lock b/sln/paket.lock
index 03f63e7..7a571d3 100644
--- a/sln/paket.lock
+++ b/sln/paket.lock
@@ -1,27 +1,10 @@
STORAGE: NONE
NUGET
remote: https://api.nuget.org/v3/index.json
- BenchmarkDotNet (0.14)
- BenchmarkDotNet.Annotations (>= 0.14) - restriction: >= netstandard2.0
- CommandLineParser (>= 2.9.1) - restriction: >= netstandard2.0
- Gee.External.Capstone (>= 2.3) - restriction: >= netstandard2.0
- Iced (>= 1.17) - restriction: >= netstandard2.0
- Microsoft.CodeAnalysis.CSharp (>= 4.1) - restriction: >= netstandard2.0
- Microsoft.Diagnostics.Runtime (>= 2.2.332302) - restriction: >= netstandard2.0
- Microsoft.Diagnostics.Tracing.TraceEvent (>= 3.1.8) - restriction: >= netstandard2.0
- Microsoft.DotNet.PlatformAbstractions (>= 3.1.6) - restriction: >= netstandard2.0
- Microsoft.Win32.Registry (>= 5.0) - restriction: && (< net6.0) (>= netstandard2.0)
- Perfolizer (0.3.17) - restriction: >= netstandard2.0
- System.Management (>= 5.0) - restriction: >= netstandard2.0
- System.Numerics.Vectors (>= 4.5) - restriction: && (< net6.0) (>= netstandard2.0)
- System.Reflection.Emit (>= 4.7) - restriction: && (< net6.0) (>= netstandard2.0)
- System.Reflection.Emit.Lightweight (>= 4.7) - restriction: && (< net6.0) (>= netstandard2.0)
- System.Threading.Tasks.Extensions (>= 4.5.4) - restriction: && (< net6.0) (>= netstandard2.0)
- BenchmarkDotNet.Annotations (0.15.8) - restriction: >= netstandard2.0
- CommandLineParser (2.9.1) - restriction: >= netstandard2.0
- Expecto (10.2.3)
- FSharp.Core (>= 7.0.200) - restriction: >= net6.0
- Mono.Cecil (>= 0.11.4 < 1.0) - restriction: >= net6.0
+ Expecto (11.1)
+ FSharp.Core (>= 7.0.200) - restriction: >= netstandard2.0
+ Mono.Cecil (>= 0.11.6 < 1.0) - restriction: >= netstandard2.0
+ System.Threading.Tasks.Extensions (>= 4.5.4) - restriction: && (< net8.0) (>= netstandard2.0)
Fake.Core.CommandLineParsing (6.1.4) - restriction: >= netstandard2.0
FParsec (>= 1.1.1) - restriction: >= netstandard2.0
FSharp.Core (>= 8.0.400) - restriction: >= netstandard2.0
@@ -40,6 +23,8 @@ NUGET
Fake.IO.FileSystem (>= 6.1.4) - restriction: >= netstandard2.0
FSharp.Core (>= 8.0.400) - restriction: >= netstandard2.0
System.Collections.Immutable (>= 8.0) - restriction: >= netstandard2.0
+ Fake.Core.SemVer (6.1.4) - restriction: >= netstandard2.0
+ FSharp.Core (>= 8.0.400) - restriction: >= netstandard2.0
Fake.Core.String (6.1.4) - restriction: >= netstandard2.0
FSharp.Core (>= 8.0.400) - restriction: >= netstandard2.0
Fake.Core.Target (6.1.4)
@@ -60,8 +45,14 @@ NUGET
Fake.Core.String (>= 6.1.4) - restriction: >= netstandard2.0
Fake.Core.Trace (>= 6.1.4) - restriction: >= netstandard2.0
FSharp.Core (>= 8.0.400) - restriction: >= netstandard2.0
- Feliz.ViewEngine (1.0.3)
- FSharp.Core (>= 4.7) - restriction: >= netstandard2.0
+ Fake.Tools.Git (6.1.4)
+ Fake.Core.Environment (>= 6.1.4) - restriction: >= netstandard2.0
+ Fake.Core.Process (>= 6.1.4) - restriction: >= netstandard2.0
+ Fake.Core.SemVer (>= 6.1.4) - restriction: >= netstandard2.0
+ Fake.Core.String (>= 6.1.4) - restriction: >= netstandard2.0
+ Fake.Core.Trace (>= 6.1.4) - restriction: >= netstandard2.0
+ Fake.IO.FileSystem (>= 6.1.4) - restriction: >= netstandard2.0
+ FSharp.Core (>= 8.0.400) - restriction: >= netstandard2.0
FParsec (1.1.1) - restriction: >= netstandard2.0
FSharp.Core (>= 4.3.4) - restriction: || (>= net45) (>= netstandard2.0)
System.ValueTuple (>= 4.4) - restriction: >= net45
@@ -72,14 +63,13 @@ NUGET
FSharp.SystemTextJson (1.4.36) - restriction: >= net6.0
FSharp.Core (>= 4.7) - restriction: >= netstandard2.0
System.Text.Json (>= 6.0.10) - restriction: >= netstandard2.0
- Gee.External.Capstone (2.3) - restriction: >= netstandard2.0
- Giraffe (8.2)
+ Giraffe (8.3)
FSharp.Core (>= 6.0) - restriction: >= net6.0
FSharp.SystemTextJson (>= 1.3.13) - restriction: >= net6.0
Giraffe.ViewEngine (>= 1.4) - restriction: >= net6.0
Microsoft.IO.RecyclableMemoryStream (>= 3.0.1) - restriction: >= net6.0
System.Text.Json (>= 8.0.6) - restriction: >= net6.0
- Giraffe.ViewEngine (1.4)
+ Giraffe.ViewEngine (1.4) - restriction: >= net6.0
FSharp.Core (>= 5.0) - restriction: >= netstandard2.0
Google.Protobuf (3.34.1) - restriction: || (&& (>= net462) (< netstandard2.0)) (&& (< net462) (>= netstandard2.0)) (>= net471) (>= net6.0)
System.Memory (>= 4.5.3) - restriction: || (>= net45) (&& (< net5.0) (>= netstandard2.0)) (&& (>= netstandard1.1) (< netstandard2.0))
@@ -94,11 +84,6 @@ NUGET
Grpc.Net.Common (2.76) - restriction: || (&& (>= net462) (< netstandard2.0)) (&& (< net462) (>= netstandard2.0)) (>= net471) (>= net6.0)
Grpc.Core.Api (>= 2.76) - restriction: >= netstandard2.0
Microsoft.Bcl.AsyncInterfaces (>= 8.0) - restriction: && (>= netstandard2.0) (< netstandard2.1)
- Iced (1.21) - restriction: >= netstandard2.0
- JetBrains.Annotations (2025.2.4)
- System.Runtime (>= 4.1) - restriction: && (< net20) (>= netstandard1.0) (< netstandard2.0)
- Markdig (0.44)
- System.Memory (>= 4.6.3) - restriction: || (>= net462) (&& (>= netstandard2.0) (< netstandard2.1))
Microsoft.AspNetCore.Hosting.Abstractions (2.3.9) - restriction: || (>= net462) (&& (< net8.0) (>= netstandard2.1)) (&& (>= netstandard2.0) (< netstandard2.1))
Microsoft.AspNetCore.Hosting.Server.Abstractions (>= 2.3) - restriction: >= netstandard2.0
Microsoft.AspNetCore.Http.Abstractions (>= 2.3) - restriction: >= netstandard2.0
@@ -112,49 +97,9 @@ NUGET
Microsoft.AspNetCore.Http.Features (5.0.17) - restriction: || (&& (>= net462) (>= netstandard2.0)) (&& (< net8.0) (>= netstandard2.1)) (&& (>= netstandard2.0) (< netstandard2.1))
Microsoft.Extensions.Primitives (>= 5.0.1) - restriction: || (>= net461) (>= netstandard2.0)
System.IO.Pipelines (>= 5.0.2) - restriction: || (>= net461) (>= netstandard2.0)
- Microsoft.Bcl.AsyncInterfaces (10.0.5) - restriction: || (&& (>= net462) (>= net6.0)) (&& (>= net462) (< netstandard2.0)) (&& (< net462) (>= netstandard2.0) (< netstandard2.1)) (>= net471) (&& (>= net6.0) (< net8.0)) (&& (>= net6.0) (< netstandard2.1)) (&& (< net8.0) (>= netstandard2.0))
+ Microsoft.Bcl.AsyncInterfaces (10.0.5) - restriction: || (&& (>= net462) (>= net6.0)) (&& (>= net462) (< netstandard2.0)) (&& (< net462) (>= netstandard2.0) (< netstandard2.1)) (>= net471) (&& (>= net6.0) (< net8.0)) (&& (>= net6.0) (< netstandard2.1))
System.Threading.Tasks.Extensions (>= 4.6.3) - restriction: || (>= net462) (&& (>= netstandard2.0) (< netstandard2.1))
Microsoft.Bcl.HashCode (6.0) - restriction: || (&& (>= net462) (>= net6.0)) (&& (>= net462) (< netstandard2.0)) (>= net471)
- Microsoft.CodeAnalysis.Analyzers (3.11) - restriction: >= netstandard2.0
- Microsoft.CodeAnalysis.Common (5.0) - restriction: >= netstandard2.0
- Microsoft.CodeAnalysis.Analyzers (>= 3.11) - restriction: >= netstandard2.0
- System.Buffers (>= 4.6) - restriction: && (< net8.0) (>= netstandard2.0)
- System.Collections.Immutable (>= 9.0) - restriction: || (&& (>= net8.0) (< net9.0)) (&& (< net8.0) (>= netstandard2.0))
- System.Memory (>= 4.6) - restriction: && (< net8.0) (>= netstandard2.0)
- System.Numerics.Vectors (>= 4.6) - restriction: && (< net8.0) (>= netstandard2.0)
- System.Reflection.Metadata (>= 9.0) - restriction: || (&& (>= net8.0) (< net9.0)) (&& (< net8.0) (>= netstandard2.0))
- System.Runtime.CompilerServices.Unsafe (>= 6.1) - restriction: && (< net8.0) (>= netstandard2.0)
- System.Text.Encoding.CodePages (>= 8.0) - restriction: && (< net8.0) (>= netstandard2.0)
- System.Threading.Tasks.Extensions (>= 4.6) - restriction: && (< net8.0) (>= netstandard2.0)
- Microsoft.CodeAnalysis.CSharp (5.0) - restriction: >= netstandard2.0
- Microsoft.CodeAnalysis.Analyzers (>= 3.11) - restriction: >= netstandard2.0
- Microsoft.CodeAnalysis.Common (5.0) - restriction: >= netstandard2.0
- System.Buffers (>= 4.6) - restriction: && (< net8.0) (>= netstandard2.0)
- System.Collections.Immutable (>= 9.0) - restriction: || (&& (>= net8.0) (< net9.0)) (&& (< net8.0) (>= netstandard2.0))
- System.Memory (>= 4.6) - restriction: && (< net8.0) (>= netstandard2.0)
- System.Numerics.Vectors (>= 4.6) - restriction: && (< net8.0) (>= netstandard2.0)
- System.Reflection.Metadata (>= 9.0) - restriction: || (&& (>= net8.0) (< net9.0)) (&& (< net8.0) (>= netstandard2.0))
- System.Runtime.CompilerServices.Unsafe (>= 6.1) - restriction: && (< net8.0) (>= netstandard2.0)
- System.Text.Encoding.CodePages (>= 8.0) - restriction: && (< net8.0) (>= netstandard2.0)
- System.Threading.Tasks.Extensions (>= 4.6) - restriction: && (< net8.0) (>= netstandard2.0)
- Microsoft.Diagnostics.NETCore.Client (0.2.661903) - restriction: >= netstandard2.0
- Microsoft.Bcl.AsyncInterfaces (>= 9.0.8) - restriction: && (< net8.0) (>= netstandard2.0)
- Microsoft.Extensions.Logging.Abstractions (>= 8.0.3) - restriction: >= netstandard2.0
- System.Buffers (>= 4.5.1) - restriction: && (< net8.0) (>= netstandard2.0)
- Microsoft.Diagnostics.Runtime (3.1.512801) - restriction: >= netstandard2.0
- Microsoft.Diagnostics.NETCore.Client (>= 0.2.410101) - restriction: >= netstandard2.0
- System.Collections.Immutable (>= 6.0) - restriction: && (< net6.0) (>= netstandard2.0)
- System.Runtime.CompilerServices.Unsafe (>= 6.0) - restriction: && (< net6.0) (>= netstandard2.0)
- Microsoft.Diagnostics.Tracing.TraceEvent (3.1.29) - restriction: >= netstandard2.0
- Microsoft.Diagnostics.NETCore.Client (>= 0.2.510501) - restriction: >= netstandard2.0
- Microsoft.Win32.Registry (>= 5.0) - restriction: >= netstandard2.0
- System.Collections.Immutable (>= 9.0.8) - restriction: >= netstandard2.0
- System.Reflection.Metadata (>= 9.0.8) - restriction: >= netstandard2.0
- System.Reflection.TypeExtensions (>= 4.7) - restriction: >= netstandard2.0
- System.Runtime.CompilerServices.Unsafe (>= 6.1.2) - restriction: >= netstandard2.0
- System.Text.Json (>= 9.0.8) - restriction: >= netstandard2.0
- Microsoft.DotNet.PlatformAbstractions (3.1.6) - restriction: >= netstandard2.0
- System.Runtime.InteropServices.RuntimeInformation (>= 4.0) - restriction: || (>= net45) (&& (>= netstandard1.3) (< netstandard2.0))
Microsoft.Extensions.Configuration (10.0.5) - restriction: || (>= net462) (>= netstandard2.0)
Microsoft.Extensions.Configuration.Abstractions (>= 10.0.5) - restriction: || (>= net462) (>= netstandard2.0)
Microsoft.Extensions.Primitives (>= 10.0.5) - restriction: || (>= net462) (>= netstandard2.0)
@@ -204,7 +149,6 @@ NUGET
System.Buffers (>= 4.6.1) - restriction: || (>= net462) (&& (< net8.0) (>= netstandard2.0))
System.Diagnostics.DiagnosticSource (>= 10.0.2) - restriction: || (&& (< net10.0) (>= net9.0)) (>= net462) (&& (>= net8.0) (< net9.0)) (&& (< net8.0) (>= netstandard2.0))
System.Memory (>= 4.6.3) - restriction: || (>= net462) (&& (< net8.0) (>= netstandard2.0))
- Microsoft.Extensions.ObjectPool (10.0.2) - restriction: >= net10.0
Microsoft.Extensions.Options (10.0.2) - restriction: || (>= net462) (>= netstandard2.0)
Microsoft.Extensions.DependencyInjection.Abstractions (>= 10.0.2) - restriction: || (>= net462) (>= netstandard2.0)
Microsoft.Extensions.Primitives (>= 10.0.2) - restriction: || (>= net462) (>= netstandard2.0)
@@ -214,20 +158,8 @@ NUGET
System.Memory (>= 4.6.3) - restriction: || (>= net462) (&& (< net8.0) (>= netstandard2.0))
System.Runtime.CompilerServices.Unsafe (>= 6.1.2) - restriction: || (>= net462) (&& (< net8.0) (>= netstandard2.0))
Microsoft.IO.RecyclableMemoryStream (3.0.1) - restriction: >= net6.0
- Microsoft.NETCore.Platforms (7.0.4) - restriction: || (&& (>= monoandroid) (>= netcoreapp2.0) (< netstandard1.3)) (&& (>= monoandroid) (>= netcoreapp2.1) (< netstandard1.3)) (&& (< monoandroid) (< net20) (>= netstandard1.0) (< netstandard1.2) (< win8) (< wp8)) (&& (< monoandroid) (< net20) (>= netstandard1.2) (< netstandard1.3) (< win8) (< wpa81)) (&& (< monoandroid) (< net20) (>= netstandard1.3) (< netstandard1.5) (< win8) (< wpa81)) (&& (< monoandroid) (>= netcoreapp2.0) (< netcoreapp2.1)) (&& (>= monotouch) (>= netcoreapp2.0)) (&& (>= monotouch) (>= netcoreapp2.1)) (&& (< monotouch) (< net20) (>= netstandard1.5) (< netstandard2.0) (< win8) (< wpa81) (< xamarintvos) (< xamarinwatchos)) (&& (>= net461) (>= netcoreapp2.0)) (&& (>= net461) (>= netcoreapp2.1)) (&& (>= netcoreapp2.0) (< netcoreapp2.1) (>= xamarinios)) (&& (>= netcoreapp2.0) (< netcoreapp2.1) (>= xamarinmac)) (&& (>= netcoreapp2.0) (< netcoreapp2.1) (>= xamarintvos)) (&& (>= netcoreapp2.0) (< netcoreapp2.1) (>= xamarinwatchos)) (&& (>= netcoreapp2.0) (>= uap10.1)) (&& (< netcoreapp2.0) (>= netcoreapp2.1)) (&& (>= netcoreapp2.1) (< netcoreapp3.0)) (&& (>= netcoreapp2.1) (>= uap10.1))
- Microsoft.NETCore.Targets (5.0) - restriction: || (&& (< monoandroid) (< net20) (>= netstandard1.0) (< netstandard1.2) (< win8) (< wp8)) (&& (< monoandroid) (< net20) (>= netstandard1.2) (< netstandard1.3) (< win8) (< wpa81)) (&& (< monoandroid) (< net20) (>= netstandard1.3) (< netstandard1.5) (< win8) (< wpa81)) (&& (< monotouch) (< net20) (>= netstandard1.5) (< netstandard2.0) (< win8) (< wpa81) (< xamarintvos) (< xamarinwatchos))
- Microsoft.Win32.Registry (5.0) - restriction: >= netstandard2.0
- System.Buffers (>= 4.5.1) - restriction: || (&& (>= monoandroid) (< netstandard1.3)) (>= monotouch) (&& (< net46) (< netcoreapp2.0) (>= netstandard2.0)) (>= xamarinios) (>= xamarinmac) (>= xamarintvos) (>= xamarinwatchos)
- System.Memory (>= 4.5.4) - restriction: || (&& (< monoandroid) (>= netcoreapp2.0) (< netcoreapp2.1) (< xamarinios) (< xamarinmac) (< xamarintvos) (< xamarinwatchos)) (&& (< net46) (< netcoreapp2.0) (>= netstandard2.0) (< xamarinios) (< xamarinmac) (< xamarintvos) (< xamarinwatchos)) (>= uap10.1)
- System.Security.AccessControl (>= 5.0) - restriction: || (&& (>= monoandroid) (< netstandard1.3)) (&& (< monoandroid) (>= netcoreapp2.0)) (>= monotouch) (&& (< net46) (< netcoreapp2.0) (>= netstandard2.0)) (>= net461) (>= netcoreapp2.1) (>= uap10.1) (>= xamarinios) (>= xamarinmac) (>= xamarintvos) (>= xamarinwatchos)
- System.Security.Principal.Windows (>= 5.0) - restriction: || (&& (>= monoandroid) (< netstandard1.3)) (&& (< monoandroid) (>= netcoreapp2.0)) (>= monotouch) (&& (< net46) (< netcoreapp2.0) (>= netstandard2.0)) (>= net461) (>= netcoreapp2.1) (>= uap10.1) (>= xamarinios) (>= xamarinmac) (>= xamarintvos) (>= xamarinwatchos)
- Mono.Cecil (0.11.6) - restriction: >= net6.0
- Oxpecker.ViewEngine (2.0)
- FSharp.Core (>= 10.0.100) - restriction: >= net10.0
- Microsoft.Extensions.ObjectPool (>= 10.0) - restriction: >= net10.0
- Perfolizer (0.3.17) - restriction: >= netstandard2.0
- System.Memory (>= 4.5.5) - restriction: && (>= netstandard2.0) (< netstandard2.1)
- Serilog (4.3.1)
+ Mono.Cecil (0.11.6) - restriction: >= netstandard2.0
+ Serilog (4.4)
System.Diagnostics.DiagnosticSource (>= 8.0.1) - restriction: || (&& (>= net462) (< netstandard2.0)) (&& (< net462) (< net6.0) (>= netstandard2.0)) (>= net471)
System.Threading.Channels (>= 8.0) - restriction: || (&& (>= net462) (< netstandard2.0)) (&& (< net462) (< net6.0) (>= netstandard2.0)) (>= net471)
System.ValueTuple (>= 4.5) - restriction: && (>= net462) (< netstandard2.0)
@@ -266,9 +198,8 @@ NUGET
Google.Protobuf (>= 3.30.1) - restriction: || (&& (>= net462) (< netstandard2.0)) (&& (< net462) (>= netstandard2.0)) (>= net471) (>= net6.0)
Grpc.Net.Client (>= 2.70) - restriction: || (&& (>= net462) (< netstandard2.0)) (&& (< net462) (>= netstandard2.0)) (>= net471) (>= net6.0)
Serilog (>= 4.2) - restriction: || (&& (>= net462) (< netstandard2.0)) (&& (< net462) (>= netstandard2.0)) (>= net471) (>= net6.0)
- System.Buffers (4.6.1) - restriction: || (&& (>= monoandroid) (< netstandard1.3) (>= netstandard2.0)) (&& (>= monotouch) (>= netstandard2.0)) (&& (< net46) (< netcoreapp2.0) (>= netstandard2.0)) (>= net462) (&& (>= net6.0) (< net8.0)) (&& (< net6.0) (>= netstandard2.0) (>= xamarintvos)) (&& (< net6.0) (>= netstandard2.0) (>= xamarinwatchos)) (&& (< net6.0) (>= xamarinios)) (&& (< net6.0) (>= xamarinmac)) (&& (< net8.0) (>= netstandard2.0)) (&& (< netcoreapp2.1) (>= netstandard2.0) (< netstandard2.1))
- System.CodeDom (10.0.2) - restriction: >= netstandard2.0
- System.Collections.Immutable (10.0.2) - restriction: || (&& (< net10.0) (>= net9.0)) (&& (>= net8.0) (< net9.0)) (>= netstandard2.0)
+ System.Buffers (4.6.1) - restriction: || (>= net462) (&& (>= net6.0) (< net8.0)) (&& (< net8.0) (>= netstandard2.0)) (&& (< netcoreapp2.1) (>= netstandard2.0) (< netstandard2.1))
+ System.Collections.Immutable (10.0.2) - restriction: >= netstandard2.0
System.Memory (>= 4.6.3) - restriction: || (>= net462) (&& (< net8.0) (>= netstandard2.0))
System.Runtime.CompilerServices.Unsafe (>= 6.1.2) - restriction: || (>= net462) (&& (< net8.0) (>= netstandard2.0))
System.ComponentModel.Annotations (5.0) - restriction: || (&& (>= net462) (>= netstandard2.1)) (&& (< net462) (>= netstandard2.0) (< netstandard2.1)) (&& (< net8.0) (>= netstandard2.1))
@@ -279,9 +210,7 @@ NUGET
System.Buffers (>= 4.6.1) - restriction: || (>= net462) (&& (< net8.0) (>= netstandard2.0))
System.Memory (>= 4.6.3) - restriction: || (>= net462) (&& (< net8.0) (>= netstandard2.0))
System.Threading.Tasks.Extensions (>= 4.6.3) - restriction: || (>= net462) (&& (< net8.0) (>= netstandard2.0))
- System.Management (10.0.2) - restriction: >= netstandard2.0
- System.CodeDom (>= 10.0.2) - restriction: >= netstandard2.0
- System.Memory (4.6.3) - restriction: || (&& (< monoandroid) (>= netcoreapp2.0) (< netcoreapp2.1) (< xamarinios) (< xamarinmac) (< xamarintvos) (< xamarinwatchos)) (&& (>= net45) (>= net6.0)) (&& (>= net45) (>= netstandard2.0)) (&& (< net46) (< netcoreapp2.0) (>= netstandard2.0) (< xamarinios) (< xamarinmac) (< xamarintvos) (< xamarinwatchos)) (>= net462) (&& (< net5.0) (>= net6.0)) (&& (< net5.0) (>= netstandard2.0)) (&& (>= net6.0) (< net8.0)) (&& (>= net6.0) (< netstandard2.0)) (&& (< net6.0) (>= netstandard2.0)) (&& (< net8.0) (>= netstandard2.0)) (&& (>= netstandard2.0) (< netstandard2.1)) (&& (>= netstandard2.0) (>= uap10.1))
+ System.Memory (4.6.3) - restriction: || (&& (>= net45) (>= net6.0)) (&& (>= net45) (>= netstandard2.0)) (>= net462) (&& (< net5.0) (>= net6.0)) (&& (< net5.0) (>= netstandard2.0)) (&& (>= net6.0) (< net8.0)) (&& (>= net6.0) (< netstandard2.0)) (&& (>= net6.0) (< netstandard2.1)) (&& (< net6.0) (>= netstandard2.0)) (&& (< net8.0) (>= netstandard2.0)) (&& (>= netstandard2.0) (< netstandard2.1))
System.Buffers (>= 4.6.1) - restriction: || (>= net462) (&& (< netcoreapp2.1) (>= netstandard2.0) (< netstandard2.1))
System.Numerics.Vectors (>= 4.6.1) - restriction: || (>= net462) (&& (< netcoreapp2.1) (>= netstandard2.0) (< netstandard2.1))
System.Runtime.CompilerServices.Unsafe (>= 6.1.2) - restriction: || (>= net462) (&& (< netcoreapp2.1) (>= netstandard2.0) (< netstandard2.1))
@@ -289,35 +218,15 @@ NUGET
Microsoft.Bcl.HashCode (>= 6.0) - restriction: >= net462
System.Buffers (>= 4.6.1) - restriction: >= net462
System.Memory (>= 4.6.3) - restriction: >= net462
- System.Numerics.Vectors (4.6.1) - restriction: || (>= net462) (&& (< net6.0) (>= netstandard2.0)) (&& (< net8.0) (>= netstandard2.0)) (&& (< netcoreapp2.1) (>= netstandard2.0) (< netstandard2.1))
+ System.Numerics.Vectors (4.6.1) - restriction: || (&& (>= net462) (< netstandard2.0)) (&& (< net462) (< netcoreapp2.1) (>= netstandard2.0) (< netstandard2.1)) (>= net471)
System.Reactive (6.1) - restriction: >= netstandard2.0
System.Threading.Tasks.Extensions (>= 4.5.4) - restriction: || (>= net472) (&& (< net6.0) (>= netstandard2.0)) (>= uap10.1)
- System.Reflection.Emit (4.7) - restriction: && (< net6.0) (>= netstandard2.0)
- System.Reflection.Emit.ILGeneration (>= 4.7) - restriction: || (&& (< monoandroid) (< monotouch) (< net45) (>= netstandard1.1) (< netstandard2.0) (< win8) (< wpa81) (< xamarintvos) (< xamarinwatchos)) (&& (< monoandroid) (< net45) (< netcoreapp2.0) (>= netstandard2.0) (< netstandard2.1) (< xamarintvos) (< xamarinwatchos)) (&& (< monoandroid) (< netstandard1.1) (>= portable-net45+win8+wpa81) (< win8)) (&& (< netstandard1.1) (>= win8)) (&& (< netstandard2.0) (>= wpa81)) (>= uap10.1)
- System.Reflection.Emit.ILGeneration (4.7) - restriction: || (&& (< monoandroid) (< net45) (< netcoreapp2.0) (>= netstandard2.0) (< netstandard2.1) (< xamarintvos) (< xamarinwatchos)) (&& (< monoandroid) (< netstandard1.1) (>= netstandard2.0) (< win8)) (&& (< netstandard1.1) (>= netstandard2.0) (>= win8)) (&& (>= netstandard2.0) (>= uap10.1))
- System.Reflection.Emit.Lightweight (4.7) - restriction: && (< net6.0) (>= netstandard2.0)
- System.Reflection.Emit.ILGeneration (>= 4.7) - restriction: || (&& (< monoandroid) (< monotouch) (< net45) (>= netstandard1.0) (< netstandard2.0) (< win8) (< wp8) (< wpa81) (< xamarintvos) (< xamarinwatchos)) (&& (< monoandroid) (< net45) (< netcoreapp2.0) (>= netstandard2.0) (< netstandard2.1) (< xamarintvos) (< xamarinwatchos)) (&& (< netstandard2.0) (>= wpa81)) (&& (>= portable-net45+win8+wp8+wpa81) (< portable-net45+wp8) (< win8)) (&& (< portable-net45+wp8) (>= win8)) (>= uap10.1)
- System.Reflection.Metadata (10.0.2) - restriction: || (&& (>= net8.0) (< net9.0)) (>= netstandard2.0)
- System.Collections.Immutable (>= 10.0.2) - restriction: || (&& (< net10.0) (>= net9.0)) (>= net462) (&& (>= net8.0) (< net9.0)) (&& (< net8.0) (>= netstandard2.0))
- System.Reflection.TypeExtensions (4.7) - restriction: >= netstandard2.0
- System.Runtime (4.3.1) - restriction: && (< net20) (>= netstandard1.0) (< netstandard2.0)
- Microsoft.NETCore.Platforms (>= 1.1.1) - restriction: || (&& (< monoandroid) (< net45) (>= netstandard1.0) (< netstandard1.2) (< win8) (< wp8)) (&& (< monoandroid) (< net45) (>= netstandard1.2) (< netstandard1.3) (< win8) (< wpa81)) (&& (< monoandroid) (< net45) (>= netstandard1.3) (< netstandard1.5) (< win8) (< wpa81)) (&& (< monotouch) (< net45) (>= netstandard1.5) (< win8) (< wpa81) (< xamarinios) (< xamarinmac) (< xamarintvos) (< xamarinwatchos))
- Microsoft.NETCore.Targets (>= 1.1.3) - restriction: || (&& (< monoandroid) (< net45) (>= netstandard1.0) (< netstandard1.2) (< win8) (< wp8)) (&& (< monoandroid) (< net45) (>= netstandard1.2) (< netstandard1.3) (< win8) (< wpa81)) (&& (< monoandroid) (< net45) (>= netstandard1.3) (< netstandard1.5) (< win8) (< wpa81)) (&& (< monotouch) (< net45) (>= netstandard1.5) (< win8) (< wpa81) (< xamarinios) (< xamarinmac) (< xamarintvos) (< xamarinwatchos))
- System.Runtime.CompilerServices.Unsafe (6.1.2) - restriction: || (&& (< net45) (>= net471)) (>= net462) (&& (< net5.0) (>= net6.0)) (&& (>= net6.0) (< net8.0)) (>= netstandard2.0)
- System.Runtime.InteropServices.RuntimeInformation (4.3) - restriction: && (>= net45) (>= netstandard2.0)
- System.Security.AccessControl (6.0.1) - restriction: || (&& (>= monoandroid) (< netstandard1.3) (>= netstandard2.0)) (&& (< monoandroid) (< net6.0) (>= netcoreapp2.0)) (&& (>= monotouch) (>= netstandard2.0)) (&& (< net46) (< netcoreapp2.0) (>= netstandard2.0)) (&& (>= net461) (>= netstandard2.0)) (&& (< net6.0) (>= netcoreapp2.1)) (&& (< net6.0) (>= netstandard2.0) (>= xamarintvos)) (&& (< net6.0) (>= netstandard2.0) (>= xamarinwatchos)) (&& (< net6.0) (>= xamarinios)) (&& (< net6.0) (>= xamarinmac)) (&& (>= netstandard2.0) (>= uap10.1))
- System.Security.Principal.Windows (>= 5.0) - restriction: || (>= net461) (&& (< net6.0) (>= netstandard2.0))
- System.Security.Principal.Windows (5.0) - restriction: || (&& (>= monoandroid) (< netstandard1.3) (>= netstandard2.0)) (&& (< monoandroid) (< net6.0) (>= netcoreapp2.0)) (&& (>= monotouch) (>= netstandard2.0)) (&& (< net46) (< netcoreapp2.0) (>= netstandard2.0)) (&& (>= net461) (>= netstandard2.0)) (&& (< net6.0) (>= netcoreapp2.1)) (&& (< net6.0) (>= netstandard2.0) (>= xamarintvos)) (&& (< net6.0) (>= netstandard2.0) (>= xamarinwatchos)) (&& (< net6.0) (>= xamarinios)) (&& (< net6.0) (>= xamarinmac)) (&& (>= netstandard2.0) (>= uap10.1))
- Microsoft.NETCore.Platforms (>= 5.0) - restriction: || (&& (>= netcoreapp2.0) (< netcoreapp2.1)) (&& (>= netcoreapp2.1) (< netcoreapp3.0))
- System.Text.Encoding.CodePages (10.0.2) - restriction: && (< net8.0) (>= netstandard2.0)
- System.Memory (>= 4.6.3) - restriction: || (>= net462) (&& (< net8.0) (>= netstandard2.0))
- System.Runtime.CompilerServices.Unsafe (>= 6.1.2) - restriction: || (>= net462) (&& (< net8.0) (>= netstandard2.0))
- System.ValueTuple (>= 4.6.1) - restriction: >= net462
+ System.Runtime.CompilerServices.Unsafe (6.1.2) - restriction: || (&& (< net45) (< net5.0) (>= netstandard2.0)) (&& (>= net462) (>= net6.0)) (&& (>= net462) (>= netstandard2.0)) (&& (>= net462) (< netstandard2.0)) (&& (< net462) (< net6.0) (>= netstandard2.0)) (>= net471) (&& (< net5.0) (>= net6.0)) (&& (>= net6.0) (< net8.0)) (&& (< net8.0) (>= netstandard2.0)) (&& (< netcoreapp2.1) (>= netstandard2.0) (< netstandard2.1))
System.Text.Encodings.Web (10.0.2) - restriction: || (&& (< net10.0) (>= net9.0)) (>= net462) (&& (>= net6.0) (< net8.0)) (&& (>= net8.0) (< net9.0)) (&& (< net8.0) (>= netstandard2.0)) (&& (< net8.0) (>= netstandard2.1)) (&& (>= netstandard2.0) (< netstandard2.1))
System.Buffers (>= 4.6.1) - restriction: || (>= net462) (&& (< net8.0) (>= netstandard2.0))
System.Memory (>= 4.6.3) - restriction: || (>= net462) (&& (< net8.0) (>= netstandard2.0))
System.Runtime.CompilerServices.Unsafe (>= 6.1.2) - restriction: || (>= net462) (&& (< net8.0) (>= netstandard2.0))
- System.Text.Json (10.0.2) - restriction: || (&& (< net10.0) (>= net9.0)) (>= net462) (&& (>= net8.0) (< net9.0)) (>= netstandard2.0)
+ System.Text.Json (10.0.2) - restriction: || (&& (< net10.0) (>= net9.0)) (>= net462) (>= net6.0) (&& (>= net8.0) (< net9.0)) (&& (< net8.0) (>= netstandard2.0))
Microsoft.Bcl.AsyncInterfaces (>= 10.0.2) - restriction: || (>= net462) (&& (< net8.0) (>= netstandard2.0))
System.Buffers (>= 4.6.1) - restriction: || (>= net462) (&& (< net8.0) (>= netstandard2.0))
System.IO.Pipelines (>= 10.0.2) - restriction: || (&& (< net10.0) (>= net9.0)) (>= net462) (&& (>= net8.0) (< net9.0)) (&& (< net8.0) (>= netstandard2.0))
@@ -328,6 +237,6 @@ NUGET
System.Threading.Channels (10.0.5) - restriction: || (&& (>= net462) (< netstandard2.0)) (&& (< net462) (< net6.0) (>= netstandard2.0)) (>= net471)
Microsoft.Bcl.AsyncInterfaces (>= 10.0.5) - restriction: || (>= net462) (&& (>= netstandard2.0) (< netstandard2.1))
System.Threading.Tasks.Extensions (>= 4.6.3) - restriction: || (>= net462) (&& (>= netstandard2.0) (< netstandard2.1))
- System.Threading.Tasks.Extensions (4.6.3) - restriction: || (&& (>= net462) (>= net6.0)) (&& (>= net462) (< netstandard2.0)) (&& (< net462) (>= netstandard2.0) (< netstandard2.1)) (>= net471) (&& (>= net6.0) (< net8.0)) (&& (< net6.0) (>= netstandard2.0)) (&& (< net8.0) (>= netstandard2.0))
+ System.Threading.Tasks.Extensions (4.6.3) - restriction: || (&& (>= net462) (>= net6.0)) (&& (>= net462) (< netstandard2.0)) (&& (< net462) (>= netstandard2.0) (< netstandard2.1)) (>= net471) (&& (>= net6.0) (< net8.0)) (&& (< net6.0) (>= netstandard2.0)) (&& (< net8.0) (>= netstandard2.0)) (&& (>= netstandard2.0) (>= uap10.1))
System.Runtime.CompilerServices.Unsafe (>= 6.1.2) - restriction: || (>= net462) (&& (< netcoreapp2.1) (>= netstandard2.0) (< netstandard2.1))
- System.ValueTuple (4.6.1) - restriction: >= net462
+ System.ValueTuple (4.6.1) - restriction: || (&& (>= net45) (>= netstandard2.0)) (>= net462)
diff --git a/sln/src/Benchmarks/Benchmarks.fs b/sln/src/Benchmarks/Benchmarks.fs
index 09c8f03..5c48e43 100644
--- a/sln/src/Benchmarks/Benchmarks.fs
+++ b/sln/src/Benchmarks/Benchmarks.fs
@@ -1,9 +1,5 @@
module Benchmarks
open BenchmarkDotNet.Attributes
-open BenchmarkDotNet.Configs
-open BenchmarkDotNet.Jobs
-open BenchmarkDotNet.Running
-open BenchmarkDotNet.Toolchains.InProcess.NoEmit
module ViewEngineApi =
open FSharp.ViewEngine
@@ -458,15 +454,3 @@ type BuildOnly() =
[]
member _.FelizApi() =
FelizApi.buildDocument()
-
-let runBenchmarks () =
- let medJob =
- Job.MediumRun
- .WithToolchain(InProcessNoEmitToolchain.Instance)
-
- let config =
- ManualConfig.Create(DefaultConfig.Instance)
- .AddJob(medJob)
- BenchmarkRunner.Run(config) |> ignore
- BenchmarkRunner.Run(config) |> ignore
- BenchmarkRunner.Run(config) |> ignore
diff --git a/sln/src/Benchmarks/Benchmarks.fsproj b/sln/src/Benchmarks/Benchmarks.fsproj
index 5cb7d76..ac7bc12 100644
--- a/sln/src/Benchmarks/Benchmarks.fsproj
+++ b/sln/src/Benchmarks/Benchmarks.fsproj
@@ -8,11 +8,16 @@
+
+
-
+
+
+
+
+
-
diff --git a/sln/src/Benchmarks/Profile.fs b/sln/src/Benchmarks/Profile.fs
index cd1c5f5..642b1b3 100644
--- a/sln/src/Benchmarks/Profile.fs
+++ b/sln/src/Benchmarks/Profile.fs
@@ -60,20 +60,20 @@ let run (mode: Mode) (api: Api) (durationMs: int) =
| Giraffe -> Benchmarks.GiraffeApi.buildDocument() |> ignore
| Feliz -> Benchmarks.FelizApi.buildDocument() |> ignore
- let renderOnly () =
+ let renderOnly =
match api with
| ViewEngine ->
let doc = Benchmarks.ViewEngineApi.buildDocument()
- doc |> FSharp.ViewEngine.Render.toHtmlDocString |> ignore
+ fun () -> doc |> FSharp.ViewEngine.Render.toHtmlDocString |> ignore
| Oxpecker ->
let doc = Benchmarks.OxpeckerApi.buildDocument()
- doc |> Oxpecker.ViewEngine.Render.toHtmlDocString |> ignore
+ fun () -> doc |> Oxpecker.ViewEngine.Render.toHtmlDocString |> ignore
| Giraffe ->
let doc = Benchmarks.GiraffeApi.buildDocument()
- doc |> Giraffe.ViewEngine.RenderView.AsString.htmlDocument |> ignore
+ fun () -> doc |> Giraffe.ViewEngine.RenderView.AsString.htmlDocument |> ignore
| Feliz ->
let doc = Benchmarks.FelizApi.buildDocument()
- doc |> Feliz.ViewEngine.Render.htmlDocument |> ignore
+ fun () -> doc |> Feliz.ViewEngine.Render.htmlDocument |> ignore
let invoke =
match mode with
@@ -106,5 +106,5 @@ let main args =
run mode api durationMs
0
else
- Benchmarks.runBenchmarks ()
+ Runner.runBenchmarks args
0
diff --git a/sln/src/Benchmarks/Runner.fs b/sln/src/Benchmarks/Runner.fs
new file mode 100644
index 0000000..083e292
--- /dev/null
+++ b/sln/src/Benchmarks/Runner.fs
@@ -0,0 +1,70 @@
+module Runner
+
+open System
+open System.Diagnostics
+open System.IO
+open System.Reflection
+open System.Runtime.InteropServices
+open System.Text.Json
+open BenchmarkDotNet.Configs
+open BenchmarkDotNet.Jobs
+open BenchmarkDotNet.Running
+open Perfolizer.Horology
+
+let private sdkVersion () =
+ let startInfo = ProcessStartInfo("dotnet", "--version")
+ startInfo.RedirectStandardOutput <- true
+ startInfo.UseShellExecute <- false
+ use sdkProcess = Process.Start(startInfo)
+ let output = sdkProcess.StandardOutput.ReadToEnd().Trim()
+ sdkProcess.WaitForExit()
+ if sdkProcess.ExitCode = 0 then output else "unknown"
+
+let private dependencyVersions () =
+ let tracked =
+ set [ "BenchmarkDotNet"; "FSharp.ViewEngine"; "Oxpecker.ViewEngine"; "Giraffe.ViewEngine"; "Feliz.ViewEngine" ]
+
+ let assemblyPath = Assembly.GetExecutingAssembly().Location
+ let dependencyManifest = Path.ChangeExtension(assemblyPath, ".deps.json")
+ use document = JsonDocument.Parse(File.ReadAllText dependencyManifest)
+
+ document.RootElement.GetProperty("libraries").EnumerateObject()
+ |> Seq.choose (fun dependency ->
+ match dependency.Name.Split('/') with
+ | [| name; version |] when tracked.Contains name ->
+ let kind = dependency.Value.GetProperty("type").GetString()
+ Some(name, (version, kind))
+ | _ -> None)
+ |> Map.ofSeq
+
+let private printMetadata jobName =
+ printfn "Benchmark environment"
+ printfn " SDK: %s" (sdkVersion ())
+ printfn " Runtime: %s" RuntimeInformation.FrameworkDescription
+ printfn " OS: %s" RuntimeInformation.OSDescription
+ printfn " Architecture: %A" RuntimeInformation.ProcessArchitecture
+ printfn " Job: %s; process-isolated default toolchain" jobName
+ printfn "Dependency versions"
+ for KeyValue(name, (version, kind)) in dependencyVersions () do
+ printfn " %s: %s (%s)" name version kind
+
+let runBenchmarks args =
+ Workloads.Validation.run ()
+
+ let smoke = args |> Array.contains "--smoke"
+ let suppliedArgs = args |> Array.filter ((<>) "--smoke")
+ let benchmarkArgs = if Array.isEmpty suppliedArgs then [| "--filter"; "*" |] else suppliedArgs
+ let measurementJob = Job.MediumRun.WithIterationTime(TimeInterval.FromMilliseconds 100.)
+ let job, jobName =
+ if smoke then Job.Dry, "Dry smoke"
+ else measurementJob, "MediumRun (100 ms iteration target)"
+ printMetadata jobName
+
+ let config =
+ ManualConfig.Create(DefaultConfig.Instance)
+ .AddJob(job)
+
+ BenchmarkSwitcher
+ .FromAssembly(typeof.Assembly)
+ .Run(benchmarkArgs, config)
+ |> ignore
diff --git a/sln/src/Benchmarks/Workloads.fs b/sln/src/Benchmarks/Workloads.fs
new file mode 100644
index 0000000..b5c0139
--- /dev/null
+++ b/sln/src/Benchmarks/Workloads.fs
@@ -0,0 +1,230 @@
+module Workloads
+
+open BenchmarkDotNet.Attributes
+open FSharp.ViewEngine
+open type Html
+
+module AttributeValues =
+ let plain = "plain attribute value"
+ let encoded = "{\"quoted\":\"\",\"ampersand\":\"A&B\",\"apostrophe\":\"'\"}"
+
+ let render value =
+ div {
+ _title value
+ "content"
+ }
+ |> Render.toString
+
+module Shapes =
+ let attributes count =
+ match count with
+ | 0 -> div { "content" }
+ | 1 -> div { _attr("data-1", "1"); "content" }
+ | 2 -> div { _attr("data-1", "1"); _attr("data-2", "2"); "content" }
+ | 8 ->
+ div {
+ _attr("data-1", "1")
+ _attr("data-2", "2")
+ _attr("data-3", "3")
+ _attr("data-4", "4")
+ _attr("data-5", "5")
+ _attr("data-6", "6")
+ _attr("data-7", "7")
+ _attr("data-8", "8")
+ "content"
+ }
+ | unsupported -> invalidArg (nameof count) $"Unsupported attribute count: {unsupported}"
+
+ let children count =
+ match count with
+ | 0 -> div { () }
+ | 1 -> div { span { "1" } }
+ | 2 -> div { span { "1" }; span { "2" } }
+ | 8 ->
+ div {
+ span { "1" }
+ span { "2" }
+ span { "3" }
+ span { "4" }
+ span { "5" }
+ span { "6" }
+ span { "7" }
+ span { "8" }
+ }
+ | unsupported -> invalidArg (nameof count) $"Unsupported child count: {unsupported}"
+
+module Collections =
+ let private valuesArray = Array.init 16 (fun index -> $"item-{index}")
+ let private valuesList = valuesArray |> Array.toList
+ let private valuesSequence = valuesArray |> Seq.map id
+
+ let fromArray () =
+ ul {
+ for value in valuesArray do
+ li { value }
+ }
+
+ let fromList () =
+ ul {
+ for value in valuesList do
+ li { value }
+ }
+
+ let fromSequence () =
+ ul {
+ for value in valuesSequence do
+ li { value }
+ }
+
+module Documents =
+ let private largeIndexes = Array.init 1_000 id
+
+ let small () =
+ section {
+ _class "card"
+ h2 { "Small fragment" }
+ p { "A representative fragment with attributes and encoded text: <>&." }
+ }
+
+ let representative () = Benchmarks.ViewEngineApi.buildDocument ()
+
+ let rec private nestedElement depth =
+ if depth = 0 then
+ span { "leaf" }
+ else
+ div {
+ _class "level"
+ nestedElement (depth - 1)
+ }
+
+ let deeplyNested () = nestedElement 64
+
+ let large () =
+ html {
+ _lang "en"
+ body {
+ main {
+ for index in largeIndexes do
+ article {
+ _id $"item-{index}"
+ h2 { $"Item {index}" }
+ p { "A repeated row with plain content." }
+ p { "Encoded content: & metadata." }
+ }
+ }
+ }
+ }
+
+module Validation =
+ let private occurrences (needle: string) (value: string) =
+ let rec countFrom offset count =
+ let index = value.IndexOf(needle, offset, System.StringComparison.Ordinal)
+ if index < 0 then count else countFrom (index + needle.Length) (count + 1)
+ countFrom 0 0
+
+ let run () =
+ let encoded = AttributeValues.render AttributeValues.encoded
+ if encoded.Contains("\"\"", System.StringComparison.Ordinal)
+ || not (encoded.Contains(""", System.StringComparison.Ordinal))
+ || not (encoded.Contains("<tag>", System.StringComparison.Ordinal))
+ || not (encoded.Contains("A&B", System.StringComparison.Ordinal)) then
+ failwith $"Encoded attribute benchmark does not exercise HTML encoding: {encoded}"
+
+ for count in [ 0; 1; 2; 8 ] do
+ let attributes = Shapes.attributes count |> Render.toString
+ if occurrences " data-" attributes <> count then
+ failwith $"Attribute shape {count} rendered an unexpected value: {attributes}"
+
+ let children = Shapes.children count |> Render.toString
+ if occurrences "" children <> count then
+ failwith $"Child shape {count} rendered an unexpected value: {children}"
+
+ let collectionOutputs =
+ [ Collections.fromArray (); Collections.fromList (); Collections.fromSequence () ]
+ |> List.map Render.toString
+
+ if collectionOutputs |> List.distinct |> List.length <> 1 then
+ failwith "Array, list, and sequence benchmarks must render equivalent output."
+
+ let nested = Documents.deeplyNested () |> Render.toString
+ if occurrences " 64 then
+ failwith "Deeply nested workload must contain exactly 64 nested div elements."
+
+ let large = Documents.large () |> Render.toHtmlDocString
+ if occurrences "
1_000 then
+ failwith "Large workload must contain exactly 1,000 article elements."
+
+[]
+type AttributeEncodingBenchmarks() =
+ []
+ member _.Plain() = AttributeValues.render AttributeValues.plain
+
+ []
+ member _.Encoded() = AttributeValues.render AttributeValues.encoded
+
+[]
+type AttributeShapeBenchmarks() =
+ []
+ member val Count = 0 with get, set
+
+ []
+ member this.BuildAndRender() = Shapes.attributes this.Count |> Render.toString
+
+[]
+type ChildShapeBenchmarks() =
+ []
+ member val Count = 0 with get, set
+
+ []
+ member this.BuildAndRender() = Shapes.children this.Count |> Render.toString
+
+[]
+type CollectionBenchmarks() =
+ []
+ member _.Array() = Collections.fromArray () |> Render.toString
+
+ []
+ member _.List() = Collections.fromList () |> Render.toString
+
+ []
+ member _.Sequence() = Collections.fromSequence () |> Render.toString
+
+[]
+type BuildAndRenderWorkloads() =
+ []
+ member _.SmallFragment() = Documents.small () |> Render.toString
+
+ []
+ member _.RepresentativePage() = Documents.representative () |> Render.toHtmlDocString
+
+ []
+ member _.DeeplyNested() = Documents.deeplyNested () |> Render.toString
+
+ []
+ member _.LargeResponse() = Documents.large () |> Render.toHtmlDocString
+
+[]
+type RenderOnlyWorkloads() =
+ let mutable small = Unchecked.defaultof
+ let mutable representative = Unchecked.defaultof
+ let mutable nested = Unchecked.defaultof
+ let mutable large = Unchecked.defaultof
+
+ []
+ member _.Setup() =
+ small <- Documents.small ()
+ representative <- Documents.representative ()
+ nested <- Documents.deeplyNested ()
+ large <- Documents.large ()
+
+ []
+ member _.SmallFragment() = small |> Render.toString
+
+ []
+ member _.RepresentativePage() = representative |> Render.toHtmlDocString
+
+ []
+ member _.DeeplyNested() = nested |> Render.toString
+
+ []
+ member _.LargeResponse() = large |> Render.toHtmlDocString
diff --git a/sln/src/Benchmarks/paket.references b/sln/src/Benchmarks/paket.references
deleted file mode 100644
index e8590a7..0000000
--- a/sln/src/Benchmarks/paket.references
+++ /dev/null
@@ -1,5 +0,0 @@
-BenchmarkDotNet
-FSharp.Core
-Giraffe.ViewEngine
-Feliz.ViewEngine
-Oxpecker.ViewEngine
diff --git a/sln/src/Build/Build.fsproj b/sln/src/Build/Build.fsproj
index 8f18310..c4947ee 100644
--- a/sln/src/Build/Build.fsproj
+++ b/sln/src/Build/Build.fsproj
@@ -6,6 +6,8 @@
$(NoWarn);NU1510
+
+
diff --git a/sln/src/Build/PackageVerification.fs b/sln/src/Build/PackageVerification.fs
new file mode 100644
index 0000000..5399d7c
--- /dev/null
+++ b/sln/src/Build/PackageVerification.fs
@@ -0,0 +1,190 @@
+module PackageVerification
+
+open System
+open System.IO
+open System.IO.Compression
+open System.Reflection.Metadata
+open System.Text.Json
+open System.Text.RegularExpressions
+open System.Xml.Linq
+
+let private sourceLinkKind = Guid("CC110556-A091-4D38-9FEC-25AB9A351A6A")
+
+let private fail message = raise (InvalidOperationException message)
+
+let private exactlyOne description values =
+ match values |> Seq.toList with
+ | [ value ] -> value
+ | values -> fail $"Expected exactly one {description}, found {values.Length}"
+
+let private entryFrameworks fileName (archive:ZipArchive) =
+ let pattern = Regex($"^lib/(net[0-9]+\\.[0-9]+)/{Regex.Escape fileName}$")
+
+ archive.Entries
+ |> Seq.choose (fun entry ->
+ let matched = pattern.Match entry.FullName
+ if matched.Success then Some matched.Groups[1].Value else None)
+ |> Seq.distinct
+ |> Seq.sort
+ |> String.concat " "
+
+let private entryText (entry:ZipArchiveEntry) =
+ use stream = entry.Open()
+ use reader = new StreamReader(stream)
+ reader.ReadToEnd()
+
+let private verifyPackageContents (archive:ZipArchive) =
+ let frameworks = entryFrameworks "FSharp.ViewEngine.dll" archive
+ if frameworks <> "net8.0" then
+ let found = if String.IsNullOrEmpty frameworks then "none" else frameworks
+ fail $"Expected only lib/net8.0, found: {found}"
+
+ if archive.Entries |> Seq.exists (fun entry -> entry.FullName.EndsWith(".pdb", StringComparison.OrdinalIgnoreCase)) then
+ fail "The main package must not contain PDB files"
+
+let private repositoryMetadata (archive:ZipArchive) =
+ let nuspecEntry =
+ archive.Entries
+ |> Seq.filter (fun entry -> entry.FullName.EndsWith(".nuspec", StringComparison.OrdinalIgnoreCase))
+ |> exactlyOne "NuSpec entry"
+
+ let document = nuspecEntry |> entryText |> XDocument.Parse
+ let repository =
+ document.Descendants()
+ |> Seq.filter (fun element -> element.Name.LocalName = "repository")
+ |> exactlyOne "repository element"
+
+ let attribute name =
+ match repository.Attribute(XName.Get name) with
+ | null -> ""
+ | value -> value.Value
+
+ let repositoryType = attribute "type"
+ let repositoryUrl = attribute "url"
+ let repositoryCommit = attribute "commit"
+
+ if repositoryType <> "git"
+ || repositoryUrl <> "https://github.com/meiermade/FSharp.ViewEngine"
+ || not (Regex.IsMatch(repositoryCommit, "^[0-9a-f]{40}$")) then
+ fail "Package repository metadata is missing its GitHub URL or commit"
+
+ repositoryCommit
+
+let private sourceLinkMappings (pdbEntry:ZipArchiveEntry) =
+ use entryStream = pdbEntry.Open()
+ use stream = new MemoryStream()
+ entryStream.CopyTo stream
+ stream.Position <- 0L
+ use provider = MetadataReaderProvider.FromPortablePdbStream stream
+ let reader = provider.GetMetadataReader()
+
+ reader.CustomDebugInformation
+ |> Seq.choose (fun handle ->
+ let information = reader.GetCustomDebugInformation handle
+ if reader.GetGuid(information.Kind) = sourceLinkKind then
+ Some(reader.GetBlobBytes information.Value)
+ else
+ None)
+ |> Seq.collect (fun json ->
+ use document = JsonDocument.Parse json
+ let mutable documents = Unchecked.defaultof
+ if document.RootElement.TryGetProperty("documents", &documents) then
+ documents.EnumerateObject()
+ |> Seq.choose (fun mapping ->
+ if mapping.Value.ValueKind = JsonValueKind.String then mapping.Value.GetString() |> Option.ofObj
+ else None)
+ |> Seq.toArray
+ else
+ Array.empty)
+ |> Seq.toList
+
+let private verifySymbols repositoryCommit symbolsPackagePath =
+ if not (File.Exists symbolsPackagePath) then
+ fail $"Missing symbol package: {symbolsPackagePath}"
+
+ use symbolsArchive = ZipFile.OpenRead symbolsPackagePath
+ let frameworks = entryFrameworks "FSharp.ViewEngine.pdb" symbolsArchive
+ if frameworks <> "net8.0" then
+ let found = if String.IsNullOrEmpty frameworks then "none" else frameworks
+ fail $"Expected only lib/net8.0 symbols, found: {found}"
+
+ let pdbEntry =
+ symbolsArchive.Entries
+ |> Seq.filter (fun entry -> entry.FullName = "lib/net8.0/FSharp.ViewEngine.pdb")
+ |> exactlyOne "portable PDB entry"
+
+ let sourceUrl = $"https://raw.githubusercontent.com/meiermade/FSharp.ViewEngine/{repositoryCommit}/"
+ let hasExpectedMapping =
+ pdbEntry
+ |> sourceLinkMappings
+ |> List.exists (fun mapping -> mapping.StartsWith(sourceUrl, StringComparison.Ordinal))
+
+ if not hasExpectedMapping then
+ fail "Portable PDB does not map Source Link to the packaged repository commit"
+
+let rec private containsProperty expectedName (element:JsonElement) =
+ match element.ValueKind with
+ | JsonValueKind.Object ->
+ element.EnumerateObject()
+ |> Seq.exists (fun property -> property.Name = expectedName || containsProperty expectedName property.Value)
+ | JsonValueKind.Array -> element.EnumerateArray() |> Seq.exists (containsProperty expectedName)
+ | _ -> false
+
+let private verifySelectedAsset framework projectDirectory =
+ let assetsPath = Path.Combine(projectDirectory, "obj", "project.assets.json")
+ use document = JsonDocument.Parse(File.ReadAllText assetsPath)
+
+ if not (containsProperty "lib/net8.0/FSharp.ViewEngine.dll" document.RootElement) then
+ fail $"{framework} did not select the net8.0 compatibility asset"
+
+let private packageVersion (packagePath:string) =
+ let matched = Regex.Match(Path.GetFileName packagePath, "^FSharp\\.ViewEngine\\.(.+)\\.nupkg$")
+ if matched.Success then matched.Groups[1].Value
+ else fail $"Unexpected package name: {Path.GetFileName packagePath}"
+
+let private testFrameworks () =
+ let configured =
+ match Environment.GetEnvironmentVariable "PACKAGE_TEST_FRAMEWORKS" with
+ | value when String.IsNullOrWhiteSpace value -> "net8.0 net9.0 net10.0"
+ | value -> value
+
+ Regex.Split(configured.Trim(), "\\s+")
+ |> Array.filter (String.IsNullOrWhiteSpace >> not)
+
+let private consumerProgram =
+ """open FSharp.ViewEngine
+open type Html
+
+let actual = div { _class "package-smoke"; "ok" } |> Render.toString
+if actual <> "ok
" then
+ failwith $"unexpected render: {actual}"
+
+printfn "FSharp.ViewEngine package works on %s" System.Runtime.InteropServices.RuntimeInformation.FrameworkDescription
+"""
+
+let verify runDotnet packagePath =
+ let packagePath = Path.GetFullPath packagePath
+ let packageDirectory = Path.GetDirectoryName packagePath
+ let version = packageVersion packagePath
+ let symbolsPackagePath = Path.ChangeExtension(packagePath, ".snupkg")
+
+ use packageArchive = ZipFile.OpenRead packagePath
+ verifyPackageContents packageArchive
+ let repositoryCommit = repositoryMetadata packageArchive
+ verifySymbols repositoryCommit symbolsPackagePath
+
+ let workDirectory = Path.Combine(Path.GetTempPath(), $"fsharp-viewengine-package.{Guid.NewGuid():N}")
+ Directory.CreateDirectory workDirectory |> ignore
+
+ try
+ for framework in testFrameworks () do
+ let projectDirectory = Path.Combine(workDirectory, framework)
+ runDotnet workDirectory [ "new"; "console"; "--language"; "F#"; "--framework"; framework; "--output"; projectDirectory; "--no-restore" ]
+ File.WriteAllText(Path.Combine(projectDirectory, "Program.fs"), consumerProgram)
+
+ runDotnet projectDirectory [ "add"; "package"; "FSharp.ViewEngine"; "--version"; version; "--source"; packageDirectory; "--no-restore" ]
+ runDotnet projectDirectory [ "restore"; "--source"; packageDirectory; "--source"; "https://api.nuget.org/v3/index.json" ]
+ verifySelectedAsset framework projectDirectory
+ runDotnet projectDirectory [ "run"; "--framework"; framework; "--no-restore" ]
+ finally
+ if Directory.Exists workDirectory then Directory.Delete(workDirectory, true)
diff --git a/sln/src/Build/Program.fs b/sln/src/Build/Program.fs
index 838cfc4..9a52be0 100644
--- a/sln/src/Build/Program.fs
+++ b/sln/src/Build/Program.fs
@@ -1,9 +1,11 @@
+open System.Net
+open System.Net.Sockets
+open System.Text.RegularExpressions
open Fake.Core
open Fake.Core.TargetOperators
open Fake.IO
open Fake.IO.FileSystemOperators
open Fake.IO.Globbing.Operators
-open System.Text.RegularExpressions
System.Environment.GetCommandLineArgs()
|> Array.tail
@@ -20,6 +22,13 @@ let rootDir = Path.getDirectory slnDir
let nugetsDir = rootDir > "nugets"
let testsDir = srcDir > "Tests"
let docsDir = srcDir > "Docs"
+let docsTestsDir = srcDir > "Docs.Tests"
+let benchmarksDir = srcDir > "Benchmarks"
+let releaseRepository = Environment.environVarOrDefault "RELEASE_REPOSITORY" rootDir
+let releaseMetadataPath =
+ Environment.environVarOrDefault
+ "RELEASE_METADATA_PATH"
+ (__SOURCE_DIRECTORY__ > "obj" > "release-metadata.json")
let exec workDir cmd args =
CreateProcess.fromRawCommand cmd args
@@ -29,9 +38,9 @@ let exec workDir cmd args =
|> Async.AwaitTask
|> Async.Ignore
-let execEnv env workDir cmd args =
+let execEnv key value workDir cmd args =
CreateProcess.fromRawCommand cmd args
- |> CreateProcess.withEnvironmentMap env
+ |> CreateProcess.setEnvironmentVariable key value
|> CreateProcess.withWorkingDirectory workDir
|> CreateProcess.ensureExitCode
|> Proc.start
@@ -41,10 +50,30 @@ let execEnv env workDir cmd args =
let dotnet workdir args = exec workdir "dotnet" args
let tailwindcss args = exec docsDir "tailwindcss" args
+let availableLocalPort () =
+ use listener = new TcpListener(IPAddress.Loopback, 0)
+ listener.Start()
+ (listener.LocalEndpoint :?> IPEndPoint).Port
+
let getVersion () =
- let tag = Environment.environVarOrFail "GITHUB_REF_NAME"
- let m = Regex.Match(tag, @"^v(\d+\.\d+\.\d+)$")
- if m.Success then m.Groups[1].Value else failwith $"invalid tag: {tag}"
+ let value =
+ match Environment.environVarOrNone "PACKAGE_VERSION" with
+ | Some version -> version
+ | None -> Environment.environVarOrFail "GITHUB_REF_NAME"
+
+ let matched = Regex.Match(value, @"^v?(\d+\.\d+\.\d+)$")
+ if matched.Success then matched.Groups[1].Value else failwith $"invalid package version: {value}"
+
+Target.create "PrepareRelease" <| fun _ ->
+ let versionOverride = Environment.environVarOrNone "RELEASE_VERSION_OVERRIDE"
+ let metadata = Release.prepare releaseRepository releaseMetadataPath versionOverride
+ Trace.trace $"Prepared {metadata.tag} for {metadata.commit}"
+ Trace.trace $"Release metadata: {releaseMetadataPath}"
+
+Target.create "TagRelease" <| fun _ ->
+ let metadata = Release.readMetadata releaseMetadataPath
+ Release.tag releaseRepository metadata
+ Trace.trace $"Release tag {metadata.tag} points to {metadata.commit}"
Target.create "CleanNugets" <| fun _ -> Shell.cleanDir nugetsDir
@@ -55,6 +84,9 @@ Target.create "Test" <| fun _ ->
|> Async.RunSynchronously
)
+ dotnet docsTestsDir ["run"]
+ |> Async.RunSynchronously
+
Target.create "Pack" (fun _ ->
let project = srcDir > "FSharp.ViewEngine" > "FSharp.ViewEngine.fsproj"
Trace.trace $"Packing {project}"
@@ -63,22 +95,58 @@ Target.create "Pack" (fun _ ->
|> Async.RunSynchronously
)
+Target.create "VerifyPackage" (fun _ ->
+ let package =
+ match Environment.environVarOrNone "PACKAGE_PATH" with
+ | Some package -> Path.getFullName package
+ | None ->
+ let nugets = !! $"{nugetsDir}/*.nupkg" |> Seq.toList
+ match nugets with
+ | [ package ] -> package
+ | _ -> failwith $"Expected exactly one package, found {nugets.Length}"
+
+ if not (File.exists package) then failwith $"Package does not exist: {package}"
+
+ PackageVerification.verify
+ (fun workDir args -> dotnet workDir args |> Async.RunSynchronously)
+ package
+)
+
Target.create "PushNugets" (fun _ ->
let nugets = !! $"{nugetsDir}/*.nupkg" |> String.concat ", "
- Trace.trace $"Publishing {nugets}"
+ Trace.trace $"Publishing {nugets} and its associated symbol package"
let apiKey = Environment.environVarOrFail "NUGET_API_KEY"
dotnet rootDir ["nuget"; "push"; $"{nugetsDir}/*.nupkg"; "--source"; "https://api.nuget.org/v3/index.json"; "--api-key"; apiKey]
|> Async.RunSynchronously
)
Target.create "WatchDocs" (fun _ ->
- let watchApp = dotnet docsDir ["watch"; "run"; "--no-restore"]
- let watchCss = tailwindcss ["--input"; "input.css"; "--output"; "wwwroot/css/output.css"; "--watch"]
+ let docsUrl =
+ System.Environment.GetEnvironmentVariable("SERVER_URL")
+ |> Option.ofObj
+ |> Option.defaultWith (fun () -> $"http://127.0.0.1:{availableLocalPort ()}")
+
+ Trace.trace $"Starting the FSharp.ViewEngine Docs at {docsUrl}"
+
+ let watchApp =
+ execEnv "SERVER_URL" docsUrl docsDir "dotnet" ["watch"; "run"; "--no-restore"]
+
+ let watchCss =
+ tailwindcss ["--input"; "input.css"; "--output"; "wwwroot/css/output.css"; "--watch"]
+
Async.Parallel [| watchApp; watchCss |]
|> Async.RunSynchronously
|> ignore
)
+Target.create "Benchmark" <| fun parameters ->
+ dotnet benchmarksDir ([ "run"; "--configuration"; "Release"; "--" ] @ parameters.Context.Arguments)
+ |> Async.RunSynchronously
+
+Target.create "BenchmarkSmoke" <| fun parameters ->
+ dotnet benchmarksDir ([ "run"; "--configuration"; "Release"; "--"; "--smoke" ] @ parameters.Context.Arguments)
+ |> Async.RunSynchronously
+
Target.create "BuildDocsCss" <| fun _ ->
tailwindcss [ "--input"; "input.css"; "--output"; "wwwroot/css/output.css"; "--minify" ]
|> Async.RunSynchronously
@@ -95,7 +163,8 @@ Target.create "Default" (fun _ -> Target.listAvailable())
"Test" ==>! "Pack"
"CleanNugets" ==>! "Pack"
-"Pack" ==>! "PushNugets"
+"Pack" ==>! "VerifyPackage"
+"VerifyPackage" ==>! "PushNugets"
"BuildDocsCss" ==>! "PublishDocs"
Target.runOrDefaultWithArguments "Default"
diff --git a/sln/src/Build/Release.fs b/sln/src/Build/Release.fs
new file mode 100644
index 0000000..3bac41b
--- /dev/null
+++ b/sln/src/Build/Release.fs
@@ -0,0 +1,131 @@
+module Release
+
+open System
+open System.Globalization
+open System.IO
+open System.Text.Json
+open System.Text.RegularExpressions
+open Fake.Tools.Git
+
+[]
+type private CalendarVersion =
+ { year:int
+ month:int
+ minor:int }
+
+ override this.ToString() = $"{this.year}.{this.month}.{this.minor}"
+
+type Metadata =
+ { tag:string
+ version:string
+ commit:string }
+
+let private versionPattern = Regex("^v?(?[0-9]{4})\\.(?[0-9]{1,2})\\.(?[0-9]+)$")
+
+let private parseVersion (value:string) =
+ let matched = versionPattern.Match value
+ if not matched.Success then
+ invalidArg (nameof value) $"Invalid calendar version: {value}. Expected YYYY.M.MINOR."
+
+ let number (group:string) = Int32.Parse(matched.Groups[group].Value, CultureInfo.InvariantCulture)
+ let version =
+ { year = number "year"
+ month = number "month"
+ minor = number "minor" }
+
+ if version.month < 1 || version.month > 12 then
+ invalidArg (nameof value) $"Invalid calendar month in version: {value}"
+
+ version
+
+let private gitValue repository command =
+ CommandHelper.runSimpleGitCommand repository command
+
+let private knownTags (repository:string) =
+ CommandHelper.getGitResult repository "tag --list"
+ |> List.choose (fun tag ->
+ let matched = versionPattern.Match tag
+ if not matched.Success then None
+ else
+ let version = parseVersion tag
+ let commit = gitValue repository $"rev-list -n 1 {tag}"
+ Some(tag, version, commit))
+
+let private versionKey (version:CalendarVersion) = version.year, version.month, version.minor
+
+let private resolveMetadata (repository:string) (now:DateTimeOffset) (versionOverride:string option) =
+ let commit = Information.getCurrentSHA1 repository
+ let tags = knownTags repository
+
+ let selectedTag, selectedVersion =
+ match versionOverride |> Option.filter (String.IsNullOrWhiteSpace >> not) with
+ | Some requested ->
+ let version = parseVersion requested
+
+ match tags |> List.tryFind (fun (_, existingVersion, _) -> versionKey existingVersion = versionKey version) with
+ | Some(tag, _, existingCommit) when existingCommit <> commit ->
+ raise (InvalidOperationException $"Release tag {tag} already points to {existingCommit}, not {commit}")
+ | Some(tag, _, _) -> tag, tag.Substring 1
+ | None -> $"v{version}", version.ToString()
+ | None ->
+ match tags |> List.filter (fun (_, _, tagCommit) -> tagCommit = commit) |> List.sortBy (fun (_, version, _) -> versionKey version) |> List.tryLast with
+ | Some(tag, _, _) -> tag, tag.Substring 1
+ | None ->
+ let year = now.Year
+ let month = now.Month
+ let nextMinor =
+ tags
+ |> List.choose (fun (_, version, _) ->
+ if version.year = year && version.month = month then Some version.minor else None)
+ |> function
+ | [] -> 0
+ | minors -> List.max minors + 1
+
+ let version =
+ { year = year
+ month = month
+ minor = nextMinor }
+
+ $"v{version}", version.ToString()
+
+ { tag = selectedTag
+ version = selectedVersion
+ commit = commit }
+
+let private writeMetadata (path:string) (metadata:Metadata) =
+ let directory = Path.GetDirectoryName(Path.GetFullPath path)
+ Directory.CreateDirectory directory |> ignore
+
+ let options = JsonSerializerOptions(WriteIndented = true, PropertyNamingPolicy = JsonNamingPolicy.CamelCase)
+ File.WriteAllText(path, JsonSerializer.Serialize(metadata, options) + Environment.NewLine)
+
+let readMetadata (path:string) =
+ use document = JsonDocument.Parse(File.ReadAllText path)
+ let root = document.RootElement
+
+ { tag = root.GetProperty("tag").GetString()
+ version = root.GetProperty("version").GetString()
+ commit = root.GetProperty("commit").GetString() }
+
+let prepare (repository:string) (outputPath:string) (versionOverride:string option) =
+ let metadata = resolveMetadata repository DateTimeOffset.UtcNow versionOverride
+ writeMetadata outputPath metadata
+ metadata
+
+let tag (repository:string) (metadata:Metadata) =
+ let actualCommit = Information.getCurrentSHA1 repository
+ if actualCommit <> metadata.commit then
+ raise (InvalidOperationException $"Release metadata identifies {metadata.commit}, but HEAD is {actualCommit}")
+
+ let existingCommit =
+ knownTags repository
+ |> List.tryFind (fun (existingTag, _, _) -> existingTag = metadata.tag)
+ |> Option.map (fun (_, _, commit) -> commit)
+
+ match existingCommit with
+ | Some commit when commit <> metadata.commit ->
+ raise (InvalidOperationException $"Release tag {metadata.tag} already points to {commit}, not {metadata.commit}")
+ | Some _ -> ()
+ | None -> Branches.tag repository metadata.tag
+
+ Branches.pushTag repository "origin" metadata.tag
diff --git a/sln/src/Build/paket.references b/sln/src/Build/paket.references
index 9bc568a..92f8cf0 100644
--- a/sln/src/Build/paket.references
+++ b/sln/src/Build/paket.references
@@ -1 +1,2 @@
Fake.Core.Target
+Fake.Tools.Git
diff --git a/sln/src/Docs.Tests/Docs.Tests.fsproj b/sln/src/Docs.Tests/Docs.Tests.fsproj
new file mode 100644
index 0000000..a3c12f4
--- /dev/null
+++ b/sln/src/Docs.Tests/Docs.Tests.fsproj
@@ -0,0 +1,15 @@
+
+
+
+ Exe
+ net10.0
+
+
+
+
+
+
+
+
+
+
diff --git a/sln/src/Docs.Tests/Program.fs b/sln/src/Docs.Tests/Program.fs
new file mode 100644
index 0000000..d076c30
--- /dev/null
+++ b/sln/src/Docs.Tests/Program.fs
@@ -0,0 +1,278 @@
+module Docs.Tests.Program
+
+open System.Net
+open Expecto
+open FSharp.ViewEngine
+open Docs.Common
+open Docs.Pages
+
+let private expectedPaths =
+ set [
+ "/"
+ "/installation"
+ "/custom"
+ "/usage"
+ "/extensions/alpine"
+ "/extensions/datastar"
+ "/extensions/htmx"
+ "/extensions/svg"
+ "/extensions/tailwind-elements"
+ "/benchmarks"
+ "/changelog"
+ ]
+
+[]
+let tests =
+ testList "Direct F# documentation" [
+ test "Page registry covers every public documentation route" {
+ let actual = Registry.all |> List.map _.path |> Set.ofList
+ Expect.equal actual expectedPaths "documentation routes"
+ Expect.equal Registry.all.Length expectedPaths.Count "one page per route"
+ }
+
+ test "Navigation keeps extensions and project pages in the documented order" {
+ let section label =
+ Registry.navigation
+ |> List.find (fun candidate -> candidate.label = label)
+ |> _.pages
+ |> List.map _.navLabel
+
+ Expect.sequenceEqual
+ (section "Extensions")
+ [ "SVG"; "Datastar"; "HTMX"; "Alpine"; "Tailwind Plus Elements" ]
+ "extension order"
+ Expect.sequenceEqual (section "Project") [ "Benchmarks"; "Changelog" ] "project order"
+ }
+
+ test "Benchmark documentation records methodology, versions, and results" {
+ let benchmarkPage = Registry.all |> List.find (fun page -> page.path = "/benchmarks")
+ let html = benchmarkPage |> View.document Registry.navigation |> Render.toHtmlDocString
+
+ Expect.stringContains html "BenchmarkDotNet 0.15.8" "measurement framework"
+ Expect.stringContains html "Oxpecker.ViewEngine 2.0.1" "Oxpecker comparison version"
+ Expect.stringContains html "Giraffe.ViewEngine 1.4.0" "Giraffe comparison version"
+ Expect.stringContains html "Feliz.ViewEngine 1.0.3" "Feliz comparison version"
+ Expect.stringContains html "Typical Render Times" "typical render-time summary"
+ Expect.stringContains html "1.585 μs" "typical build-and-render time"
+ Expect.stringContains html "833.5 ns" "typical render-only time"
+ Expect.stringContains html "not HTTP requests per second" "throughput limitation"
+ Expect.stringContains html "How the Benchmarks Were Run" "methodology heading"
+ Expect.stringContains html "How to Run the Benchmarks" "reproduction heading"
+ Expect.isFalse (html.Contains "Which Scenario Matches My App?") "scenario guide removed"
+ Expect.isFalse (html.Contains "What the Results Suggest") "results interpretation section removed"
+ Expect.stringContains html "1.35× as long" "Oxpecker relative comparison"
+ Expect.stringContains html "2.35× as long" "Feliz relative comparison"
+ Expect.stringContains html "not CI regression thresholds" "results interpretation"
+ Expect.stringContains html " View.document Registry.navigation |> Render.toHtmlDocString
+ let encodedTitle = WebUtility.HtmlEncode page.title
+ Expect.stringContains html page.category $"{page.path} eyebrow"
+ Expect.stringContains html $">{encodedTitle}" $"{page.path} title"
+
+ for heading in DocPage.headings page do
+ Expect.stringContains html $"id=\"{heading.id}\"" $"{page.path} heading {heading.id}"
+ if heading.level <= 3 then
+ Expect.stringContains html $"href=\"#{heading.id}\"" $"{page.path} TOC {heading.id}"
+ }
+
+ test "Code examples are encoded and retain Prism language classes" {
+ let html = Custom.page |> View.document Registry.navigation |> Render.toHtmlDocString
+ Expect.stringContains html "language-fsharp" "F# language class"
+ Expect.stringContains html "language-html" "HTML language class"
+ Expect.stringContains html "<my-component class="container">" "HTML source is encoded"
+ Expect.isFalse (html.Contains("")) "example markup must not execute"
+ }
+
+ test "Migrated pages retain recent documentation updates" {
+ let customHtml = Custom.page |> View.document Registry.navigation |> Render.toHtmlDocString
+ let usageHtml = Usage.page |> View.document Registry.navigation |> Render.toHtmlDocString
+ let datastarHtml = Datastar.page |> View.document Registry.navigation |> Render.toHtmlDocString
+
+ Expect.stringContains customHtml "Trusted Content Boundaries" "trusted-content guidance"
+ Expect.stringContains usageHtml "titleBuilder" "title builder guidance"
+ Expect.stringContains datastarHtml "Datastar 1.0.2" "pinned Datastar reference"
+ }
+
+ test "Rendered pages contain no Markdown fences" {
+ for page in Registry.all do
+ let html = page |> View.document Registry.navigation |> Render.toHtmlDocString
+ Expect.isFalse (html.Contains("```")) $"{page.path} contains a Markdown fence"
+ }
+
+ test "HTMX docs cover every dedicated stable helper" {
+ let html = Htmx.page |> View.document Registry.navigation |> Render.toHtmlDocString
+ let helpers =
+ [ "_hxBoost"; "_hxConfirm"; "_hxDelete"; "_hxDisable"; "_hxDisabledElt"
+ "_hxDisinherit"; "_hxEncoding"; "_hxExt"; "_hxGet"; "_hxHeaders"
+ "_hxHistory"; "_hxHistoryElt"; "_hxInclude"; "_hxIndicator"; "_hxInherit"
+ "_hxOn"; "_hxParams"; "_hxPatch"; "_hxPost"; "_hxPreserve"; "_hxPrompt"
+ "_hxPushUrl"; "_hxPut"; "_hxReplaceUrl"; "_hxRequest"; "_hxSelect"
+ "_hxSelectOOB"; "_hxSwap"; "_hxSwapOOB"; "_hxSync"; "_hxTarget"
+ "_hxTrigger"; "_hxValidate"; "_hxVals" ]
+
+ for helper in helpers do
+ Expect.stringContains html helper helper
+
+ Expect.stringContains html "htmx:before-request" "kebab-case HTMX event"
+ Expect.isFalse (html.Contains("htmx:beforeRequest")) "camelCase event names fail after DOM normalization"
+ }
+
+ test "Alpine docs cover core and plugin directives" {
+ let html = Alpine.page |> View.document Registry.navigation |> Render.toHtmlDocString
+ let coreHelpers =
+ [ "_xBind"; "_xCloak"; "_xData"; "_xEffect"; "_xFor"; "_xHtml"
+ "_xId"; "_xIf"; "_xIgnore"; "_xInit"; "_xModel"; "_xModelable"
+ "_xOn"; "_xRef"; "_xShow"; "_xTeleport"; "_xText"; "_xTransition" ]
+ let pluginHelpers =
+ [ "_xMask"; "_xMaskDynamic"; "_xIntersect"; "_xResize"; "_xCollapse"
+ "_xTrap"; "_xAnchor"; "_xSort"; "_xSortItem"; "_xSortGroup"
+ "_xSortConfig"; "_xSortHandle"; "_xSortIgnore" ]
+
+ for helper in coreHelpers @ pluginHelpers do
+ Expect.stringContains html helper helper
+
+ Expect.stringContains html "Focus plugin" "x-trap dependency"
+ Expect.stringContains html "Anchor plugin" "x-anchor dependency"
+ Expect.stringContains html "$persist" "Persist has no directive helper"
+ Expect.stringContains html "Alpine.morph" "Morph has no directive helper"
+ }
+
+ test "Docs use only the pinned self-hosted Datastar runtime" {
+ let html = Home.page |> View.document Registry.navigation |> Render.toHtmlDocString
+ Expect.stringContains html "/scripts/datastar.1.0.2.js" "pinned Datastar script"
+ Expect.stringContains html "type=\"module\"" "Datastar module script"
+ Expect.isFalse (html.Contains("alpinejs")) "Alpine runtime removed"
+ Expect.isFalse (html.Contains(" x-data=")) "Alpine directives removed"
+ }
+
+ test "Homepage quick example is Datastar-first" {
+ let html = Home.page |> View.document Registry.navigation |> Render.toHtmlDocString
+ Expect.stringContains html "open type Datastar" "Datastar API"
+ Expect.stringContains html "_dataOn" "Datastar interaction"
+ Expect.isFalse (html.Contains("open type Htmx")) "HTMX is not used by the quick example"
+ Expect.isFalse (html.Contains("_hxGet")) "HTMX is not used as the attribute example"
+ }
+
+ test "Docs use the pinned Prism runtime and FSharp grammar" {
+ let html = Home.page |> View.document Registry.navigation |> Render.toHtmlDocString
+ let prismBase = "https://cdnjs.cloudflare.com/ajax/libs/prism/1.30.0"
+ Expect.stringContains html $"{prismBase}/prism.min.js" "pinned Prism script"
+ Expect.stringContains html $"{prismBase}/themes/prism-tomorrow.min.css" "pinned Prism theme"
+ Expect.stringContains html $"{prismBase}/components/prism-fsharp.min.js" "pinned FSharp grammar"
+ }
+
+ test "Datastar docs cover every stable helper and modifier shapes" {
+ let html = Datastar.page |> View.document Registry.navigation |> Render.toHtmlDocString
+ let helpers =
+ [ "_dataAnimate"; "_dataAttr"; "_dataBind"; "_dataClass"; "_dataComputed"
+ "_dataCustomValidity"; "_dataEffect"; "_dataIgnore"; "_dataIgnoreMorph"
+ "_dataIndicator"; "_dataInit"; "_dataJsonSignals"; "_dataMatchMedia"; "_dataOn"
+ "_dataOnIntersect"; "_dataOnInterval"; "_dataOnRaf"; "_dataOnResize"
+ "_dataOnSignalPatch"; "_dataOnSignalPatchFilter"; "_dataPersist"
+ "_dataPreserveAttr"; "_dataQueryString"; "_dataRef"; "_dataReplaceUrl"
+ "_dataScrollIntoView"; "_dataShow"; "_dataSignals"; "_dataStyle"; "_dataText"
+ "_dataViewTransition" ]
+
+ for helper in helpers do
+ Expect.stringContains html helper helper
+
+ Expect.stringContains html "debounce.200ms" "keyed modifier example"
+ Expect.stringContains html "smooth" "no-value modifier example"
+ Expect.isFalse (html.Contains("_dataRocket")) "removed data-rocket helper"
+ }
+
+ test "SVG docs cover the maintained production subset" {
+ let html = Svg.page |> View.document Registry.navigation |> Render.toHtmlDocString
+ let elements =
+ [ "circle"; "clipPath"; "defs"; "desc"; "ellipse"; "g"; "line"
+ "linearGradient"; "mask"; "path"; "polygon"; "polyline"
+ "radialGradient"; "rect"; "stop"; "svg"; "symbol"; "textElement"
+ "titleElement"; "tspan"; "useElement" ]
+ let attributes =
+ [ "_clipPath"; "_clipPathUnits"; "_cx"; "_cy"; "_dominantBaseline"
+ "_fillOpacity"; "_gradientTransform"; "_gradientUnits"; "_maskContentUnits"
+ "_maskUnits"; "_pathLength"; "_preserveAspectRatio"; "_spreadMethod"
+ "_stopColor"; "_stopOpacity"; "_strokeDasharray"; "_strokeDashoffset"
+ "_strokeMiterlimit"; "_strokeOpacity"; "_textAnchor"; "_textLength"
+ "_vectorEffect"; "_xmlns" ]
+
+ for helper in elements @ attributes do
+ Expect.stringContains html helper helper
+
+ Expect.stringContains html "production subset" "support policy"
+ Expect.stringContains html "Html.el" "unsupported element escape hatch"
+ Expect.stringContains html "_attr" "unsupported attribute escape hatch"
+ Expect.stringContains html "_ariaLabelledby" "informative accessibility pattern"
+ Expect.stringContains html "_ariaHidden" "decorative accessibility pattern"
+ Expect.stringContains html "xlink:href" "deprecated linking guidance"
+ }
+
+ test "Tailwind Plus Elements docs cover the complete 1.0.22 API" {
+ let html = TailwindElements.page |> View.document Registry.navigation |> Render.toHtmlDocString
+ let helpers =
+ [ "elAutocomplete"; "elCommandGroup"; "elCommandList"; "elCommandPalette"
+ "elCommandPreview"; "elCopyable"; "elDefaults"; "elDialog"
+ "elDialogBackdrop"; "elDialogPanel"; "elDisclosure"; "elDropdown"
+ "elMenu"; "elNoResults"; "elOption"; "elOptions"; "elPopover"
+ "elPopoverGroup"; "elSelect"; "elSelectedContent"; "elTabGroup"
+ "elTabList"; "elTabPanels"; "_anchorStrategy" ]
+
+ for helper in helpers do
+ Expect.stringContains html helper helper
+
+ Expect.stringContains html "@tailwindplus/elements@1.0.22" "pinned Elements installation"
+ Expect.stringContains html "open type TailwindElements" "current API name"
+ Expect.isFalse (html.Contains("open type Tailwind\n")) "removed Tailwind API"
+ }
+
+ test "Removed Tailwind documentation route is not registered" {
+ Expect.isFalse (Registry.all |> List.exists (fun page -> page.path = "/extensions/tailwind")) "old canonical route"
+ Expect.isFalse (Registry.aliases |> List.exists (fun (alias, _) -> alias = "/extensions/tailwind")) "old route alias"
+ }
+
+ test "Changelog identifies unreleased breaking changes and migrations" {
+ let html = Changelog.page |> View.document Registry.navigation |> Render.toHtmlDocString
+ Expect.stringContains html "Unreleased" "unreleased section"
+ Expect.stringContains html "Breaking:" "breaking change marker"
+ Expect.stringContains html "_dataBind" "removed data-bind overload"
+ Expect.stringContains html "_dataAnimate" "changed data-animate signature"
+ Expect.stringContains html "_dataRocket" "removed legacy helper"
+ Expect.stringContains html "_dataScrollIntoView ()" "presence-only migration"
+ Expect.stringContains html "Alpine Migration" "Alpine migration section"
+ Expect.stringContains html "_by" "removed Alpine helper"
+ Expect.stringContains html "_xModel" "Alpine modifier migration"
+ Expect.stringContains html "Tailwind Plus Elements Migration" "Tailwind migration section"
+ Expect.stringContains html "TailwindElements" "renamed Tailwind type"
+ Expect.stringContains html "public API" "package compatibility validation"
+ Expect.stringContains html "Source Link" "portable source-debugging symbols"
+ }
+
+ test "Installation docs distinguish package assets from runtime support" {
+ let html = Installation.page |> View.document Registry.navigation |> Render.toHtmlDocString
+ Expect.stringContains html "net8.0 compatibility asset" "package asset baseline"
+ Expect.stringContains html ".NET 8, .NET 9, and .NET 10" "tested runtime matrix"
+ Expect.stringContains html "November 10, 2026" "net8/net9 support horizon"
+ Expect.stringContains html "November 14, 2028" ".NET 10 support horizon"
+ Expect.stringContains html "Source Link" "source debugging support"
+ }
+
+ test "Compatibility route remains registered" {
+ Expect.contains Registry.aliases ("/giraffe", Usage.page.path) "legacy Giraffe route"
+ }
+ ]
+
+[]
+let main args = runTestsWithCLIArgs [] args tests
diff --git a/sln/src/Docs.Tests/paket.references b/sln/src/Docs.Tests/paket.references
new file mode 100644
index 0000000..011ea03
--- /dev/null
+++ b/sln/src/Docs.Tests/paket.references
@@ -0,0 +1 @@
+Expecto
diff --git a/sln/src/Docs/Docs.fsproj b/sln/src/Docs/Docs.fsproj
index 1a71835..8147456 100644
--- a/sln/src/Docs/Docs.fsproj
+++ b/sln/src/Docs/Docs.fsproj
@@ -3,19 +3,30 @@
net10.0
$(NoWarn);NU1510
+ false
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
- PreserveNewest
-
true
PreserveNewest
diff --git a/sln/src/Docs/Handlers.fs b/sln/src/Docs/Handlers.fs
deleted file mode 100644
index 5b9e989..0000000
--- a/sln/src/Docs/Handlers.fs
+++ /dev/null
@@ -1,69 +0,0 @@
-namespace Docs
-
-open System
-open System.IO
-open System.Text.RegularExpressions
-open Giraffe
-open Microsoft.AspNetCore.Http
-open Markdig
-open FSharp.ViewEngine
-
-module Handlers =
-
- let private markdownPipeline =
- MarkdownPipelineBuilder()
- .UseAdvancedExtensions()
- .Build()
-
- let private readMarkdownFile (fileName: string) =
- let filePath = Path.Combine(AppContext.BaseDirectory, "docs", fileName + ".md")
- if File.Exists(filePath) then
- let content = File.ReadAllText(filePath)
- Markdown.ToHtml(content, markdownPipeline)
- else
- "Page Not Found
The requested page could not be found.
"
-
- let private extractHeadings (html: string) =
- let pattern = """]*>([^<]+)"""
- Regex.Matches(html, pattern)
- |> Seq.cast
- |> Seq.map (fun m -> (m.Groups.[2].Value.Trim(), m.Groups.[1].Value))
- |> Seq.toList
-
- let private renderPage (title: string) (fileName: string) : HttpHandler =
- fun next ctx -> task {
- let currentPath = ctx.Request.Path.Value
- let markdownContent = readMarkdownFile fileName
- let headings = extractHeadings markdownContent
- let content = Views.layout title currentPath headings markdownContent
- let html = Render.toHtmlDocString content
- return! htmlString html next ctx
- }
-
- // Route handlers
- let homeHandler : HttpHandler =
- renderPage "FSharp.ViewEngine Documentation" "home"
-
- let installationHandler : HttpHandler =
- renderPage "Installation - FSharp.ViewEngine" "installation"
-
- let customHandler : HttpHandler =
- renderPage "Custom Elements & Attributes - FSharp.ViewEngine" "custom"
-
- let alpineHandler : HttpHandler =
- renderPage "Alpine.js - FSharp.ViewEngine" "alpine"
-
- let datastarHandler : HttpHandler =
- renderPage "Datastar - FSharp.ViewEngine" "datastar"
-
- let htmxHandler : HttpHandler =
- renderPage "HTMX - FSharp.ViewEngine" "htmx"
-
- let svgHandler : HttpHandler =
- renderPage "SVG - FSharp.ViewEngine" "svg"
-
- let usageHandler : HttpHandler =
- renderPage "Usage - FSharp.ViewEngine" "usage"
-
- let tailwindHandler : HttpHandler =
- renderPage "Tailwind - FSharp.ViewEngine" "tailwind"
diff --git a/sln/src/Docs/Program.fs b/sln/src/Docs/Program.fs
deleted file mode 100644
index a9469b4..0000000
--- a/sln/src/Docs/Program.fs
+++ /dev/null
@@ -1,83 +0,0 @@
-open Microsoft.AspNetCore.Builder
-open Microsoft.Extensions.Hosting
-open Microsoft.Extensions.DependencyInjection
-open Giraffe
-open Serilog
-open Serilog.Events
-open Serilog.Sinks.OpenTelemetry
-open Docs.Handlers
-
-let webApp =
- choose [
- GET >=> choose [
- route "/health" >=> text "ok"
- route "/" >=> homeHandler
- route "/installation" >=> installationHandler
- route "/custom" >=> customHandler
- route "/usage" >=> usageHandler
- route "/giraffe" >=> usageHandler
- route "/extensions/alpine" >=> alpineHandler
- route "/extensions/datastar" >=> datastarHandler
- route "/extensions/htmx" >=> htmxHandler
- route "/extensions/svg" >=> svgHandler
- route "/extensions/tailwind" >=> tailwindHandler
- ]
- ]
-
-let configureLogger (config: Docs.Config) =
- let initialLogLevel =
- if config.debug then LogEventLevel.Debug
- else LogEventLevel.Information
-
- let logger =
- LoggerConfiguration()
- .MinimumLevel.Is(initialLogLevel)
- .MinimumLevel.Override("Microsoft.AspNetCore", LogEventLevel.Warning)
- .WriteTo.Console()
- .WriteTo.OpenTelemetry(fun opts ->
- opts.Endpoint <- config.seq.endpoint + "/ingest/otlp/v1/logs"
- opts.Protocol <- OtlpProtocol.HttpProtobuf
- opts.ResourceAttributes <- dict [ "service.name", box config.appName ])
- .CreateLogger()
-
- Log.Logger <- logger
-
-let configureApp (app : IApplicationBuilder) =
- app
- .UseSerilogRequestLogging(fun opts ->
- opts.GetLevel <- fun ctx _ _ ->
- if ctx.Request.Path.Value = "/health" then LogEventLevel.Verbose
- else LogEventLevel.Information)
- .UseStaticFiles() |> ignore
- app.UseGiraffe(webApp)
-
-let configureServices (services : IServiceCollection) =
- services
- .AddSerilog()
- .AddGiraffe() |> ignore
-
-[]
-let main _args =
- let config = Docs.Config.load()
- configureLogger config
-
- try
- try
- let builder = WebApplication.CreateBuilder()
- configureServices builder.Services
-
- let app = builder.Build()
-
- if app.Environment.IsDevelopment() then
- app.UseDeveloperExceptionPage() |> ignore
-
- configureApp app
-
- Log.Information("Starting {AppName}", config.appName)
- app.Run(config.serverUrl)
- 0
- with ex ->
- Log.Fatal(ex, "Application start-up failed")
- 1
- finally
- Log.CloseAndFlush()
diff --git a/sln/src/Docs/Views.fs b/sln/src/Docs/Views.fs
deleted file mode 100644
index f350aa3..0000000
--- a/sln/src/Docs/Views.fs
+++ /dev/null
@@ -1,342 +0,0 @@
-module Docs.Views
-
-open System
-open FSharp.ViewEngine
-open type Html
-open type Alpine
-open type Tailwind
-
-type Page =
- { title:string }
-
-let magnifyingGlassIcon = raw """"""
-let menuIcon = raw """"""
-let xMarkIcon = raw """"""
-let githubIcon = raw """"""
-let sunIcon = raw """"""
-let moonIcon = raw """"""
-let sunIconSmall = raw """"""
-let moonIconSmall = raw """"""
-let monitorIcon = raw """"""
-
-let private pageHeader =
- header {
- _class [
- "sticky top-0 z-50 flex flex-none flex-wrap items-center justify-between"
- "bg-white/75 px-4 py-5 shadow-md shadow-slate-900/5 backdrop-blur transition duration-500"
- "sm:px-6 lg:px-8 dark:shadow-none dark:bg-slate-900/75 dark:backdrop-blur"
- ]
- // Left section: hamburger + logo
- div {
- _class "flex items-center gap-4"
- // Mobile menu button (hidden on desktop)
- div {
- _class "flex lg:hidden"
- button {
- _type "button"
- _xOn ("click", "mobileNavOpen = true")
- _class "relative cursor-pointer rounded-lg p-1 text-slate-500 hover:bg-slate-100 dark:text-slate-400 dark:hover:bg-slate-700"
- menuIcon
- }
- }
- // Logo
- a {
- _href "/"
- _class "flex items-center gap-2 text-sm font-semibold tracking-wider text-slate-700 dark:text-white"
- img { _src "/logo.svg"; _alt "FSharp.ViewEngine"; _class "h-6 w-6" }
- "FSharp.ViewEngine"
- }
- }
- // Right section with theme toggle and GitHub
- div {
- _class "relative flex basis-0 items-center justify-end gap-6 sm:gap-8 md:grow"
- // Theme toggle dropdown
- div {
- _class "relative z-10"
- _xData "{ open: false, theme: localStorage.getItem('theme') || 'system' }"
- _xInit """
- $watch('theme', (val) => {
- localStorage.setItem('theme', val);
- if (val === 'dark' || (val === 'system' && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
- document.documentElement.classList.add('dark');
- } else {
- document.documentElement.classList.remove('dark');
- }
- });
- if (theme === 'dark' || (theme === 'system' && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
- document.documentElement.classList.add('dark');
- }
- """
- button {
- _type "button"
- _class [
- "flex h-8 w-8 cursor-pointer items-center justify-center rounded-lg p-1"
- "hover:bg-slate-100 dark:hover:bg-slate-700"
- "transition-colors"
- ]
- _xOn ("click", "open = !open")
- // Light mode icon (sun)
- sunIcon
- moonIcon
- }
- // Dropdown menu
- div {
- _xShow "open"
- _xCloak
- _xOn ("click.away", "open = false")
- _xTransition ()
- _class [
- "absolute right-0 top-full mt-3 w-36 overflow-hidden rounded-lg"
- "bg-white py-1 text-sm font-semibold text-slate-700 shadow-lg ring-1"
- "ring-slate-900/10 dark:bg-slate-800 dark:text-slate-300 dark:ring-0"
- "dark:highlight-white/5"
- ]
- // Light option
- button {
- _type "button"
- _class "flex w-full items-center gap-2 px-3 py-2 hover:bg-slate-100 dark:hover:bg-slate-700/50"
- _xOn ("click", "theme = 'light'; open = false")
- _xBind ("class", "theme === 'light' ? 'text-sky-500' : ''")
- sunIconSmall
- text "Light"
- }
- // Dark option
- button {
- _type "button"
- _class "flex w-full items-center gap-2 px-3 py-2 hover:bg-slate-100 dark:hover:bg-slate-700/50"
- _xOn ("click", "theme = 'dark'; open = false")
- _xBind ("class", "theme === 'dark' ? 'text-sky-500' : ''")
- moonIconSmall
- text "Dark"
- }
- // System option
- button {
- _type "button"
- _class "flex w-full items-center gap-2 px-3 py-2 hover:bg-slate-100 dark:hover:bg-slate-700/50"
- _xOn ("click", "theme = 'system'; open = false")
- _xBind ("class", "theme === 'system' ? 'text-sky-500' : ''")
- monitorIcon
- text "System"
- }
- }
- }
- // GitHub link
- a {
- _href "https://github.com/meiermade/FSharp.ViewEngine"
- _class "group"
- githubIcon
- }
- }
- }
-
-let private navLink (currentPath: string) (href': string) (label': string) =
- let isActive = currentPath = href'
- li {
- _class "relative"
- a {
- _class [
- "block w-full pl-3.5 before:pointer-events-none before:absolute"
- "before:-left-1 before:top-1/2 before:h-1.5 before:w-1.5"
- "before:-translate-y-1/2 before:rounded-full"
- if isActive then
- "font-semibold text-sky-500 before:bg-sky-500"
- else
- "text-slate-500 before:hidden before:bg-slate-300 hover:text-slate-600"
- + " hover:before:block dark:text-slate-400 dark:before:bg-slate-700 dark:hover:text-slate-300"
- ]
- _href href'
- _xOn ("click", "mobileNavOpen = false")
- label'
- }
- }
-
-let private sidebarNavigation (currentPath: string) =
- nav {
- _class "text-base lg:text-sm"
- ul {
- _role "list"
- _class "space-y-9"
- li {
- h2 {
- _class "font-display font-medium text-slate-900 dark:text-white"
- "Getting started"
- }
- ul {
- _role "list"
- _class [
- "mt-2 space-y-2 border-l-2 border-slate-100"
- "lg:mt-4 lg:space-y-4 lg:border-slate-200 dark:border-slate-800"
- ]
- navLink currentPath "/" "Introduction"
- navLink currentPath "/installation" "Installation"
- navLink currentPath "/custom" "Custom Elements & Attributes"
- navLink currentPath "/usage" "Usage"
- }
- }
- li {
- h2 {
- _class "font-display font-medium text-slate-900 dark:text-white"
- "Extensions"
- }
- ul {
- _role "list"
- _class [
- "mt-2 space-y-2 border-l-2 border-slate-100"
- "lg:mt-4 lg:space-y-4 lg:border-slate-200 dark:border-slate-800"
- ]
- navLink currentPath "/extensions/alpine" "Alpine"
- navLink currentPath "/extensions/datastar" "Datastar"
- navLink currentPath "/extensions/htmx" "HTMX"
- navLink currentPath "/extensions/svg" "SVG"
- navLink currentPath "/extensions/tailwind" "Tailwind"
- }
- }
- }
- }
-
-let private sidebar (currentPath: string) =
- div {
- _class "hidden lg:relative lg:block lg:flex-none"
- div {
- _class [
- "sticky top-[4.75rem] -ml-0.5 h-[calc(100vh-4.75rem)] w-64"
- "overflow-y-auto py-16 pl-0.5 pr-8 xl:w-72 xl:pr-16"
- ]
- sidebarNavigation currentPath
- }
- }
-
-let private tableOfContents (headings: (string * string) list) =
- if List.isEmpty headings then
- empty
- else
- nav {
- _class "sticky top-[4.75rem] -mr-6 w-56 flex-none overflow-y-auto py-16 pr-6"
- h2 {
- _class "font-display text-sm font-medium text-zinc-900 dark:text-white"
- "On this page"
- }
- ul {
- _role "list"
- _class "mt-4 space-y-3 text-sm"
- for (title', anchor) in headings do
- li {
- a {
- _href $"#{anchor}"
- _class "text-zinc-500 hover:text-zinc-600 dark:text-zinc-400 dark:hover:text-zinc-300"
- title'
- }
- }
- }
- }
-
-let layout (pageTitle: string) (currentPath: string) (headings: (string * string) list) (content: string) =
- let siteUrl = "https://fsharpviewengine.meiermade.com"
- let pagePath = if String.IsNullOrWhiteSpace(currentPath) then "/" else currentPath
- let pageUrl = if pagePath = "/" then siteUrl else siteUrl + pagePath
- let socialDescription = "A minimal, fast view engine for F#. Documentation and examples for FSharp.ViewEngine."
- let socialImageUrl = siteUrl + "/android-chrome-512x512.png"
-
- html {
- _lang "en"
- _class "h-full antialiased"
- head {
- meta { _charset "utf-8" }
- meta { _name "viewport"; _content "width=device-width, initial-scale=1" }
- title pageTitle
- link { _rel "canonical"; _href pageUrl }
- meta { _name "description"; _content socialDescription }
- meta { _property "og:type"; _content "website" }
- meta { _property "og:site_name"; _content "FSharp.ViewEngine" }
- meta { _property "og:title"; _content pageTitle }
- meta { _property "og:description"; _content socialDescription }
- meta { _property "og:url"; _content pageUrl }
- meta { _property "og:image"; _content socialImageUrl }
- meta { _property "og:image:alt"; _content "FSharp.ViewEngine logo" }
- meta { _name "twitter:card"; _content "summary_large_image" }
- meta { _name "twitter:title"; _content pageTitle }
- meta { _name "twitter:description"; _content socialDescription }
- meta { _name "twitter:image"; _content socialImageUrl }
- meta { _name "twitter:image:alt"; _content "FSharp.ViewEngine logo" }
- script { js "let t=localStorage.getItem('theme');if(t==='dark'||(!t||t==='system')&&window.matchMedia('(prefers-color-scheme: dark)').matches){document.documentElement.classList.add('dark')}" }
- link { _rel "stylesheet"; _href "/css/output.css" }
- script { _src "https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js"; _defer true }
- script { _src "https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/prism.min.js" }
- link { _rel "stylesheet"; _href "https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism-tomorrow.min.css" }
- script { _src "https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-fsharp.min.js" }
- }
- body {
- _class "min-h-full bg-white dark:bg-slate-900"
- _xData "{ mobileNavOpen: false }"
- _xOn ("keydown.escape.window", "mobileNavOpen = false")
- pageHeader
- div {
- _xShow "mobileNavOpen"
- _xCloak
- _xTransition ()
- _class "fixed inset-0 z-[70] lg:hidden"
- div {
- _class "absolute inset-0 bg-slate-950/60 backdrop-blur-sm"
- _xOn ("click", "mobileNavOpen = false")
- }
- div {
- _class "absolute inset-y-0 left-0 w-full max-w-xs overflow-y-auto bg-white px-6 py-5 shadow-2xl ring-1 ring-slate-900/10 dark:bg-slate-900 dark:ring-white/10"
- div {
- _class "mb-6 flex items-center justify-between"
- a {
- _href "/"
- _class "flex items-center gap-2 text-sm font-semibold tracking-wider text-slate-700 dark:text-white"
- _xOn ("click", "mobileNavOpen = false")
- img { _src "/logo.svg"; _alt "FSharp.ViewEngine"; _class "h-6 w-6" }
- "FSharp.ViewEngine"
- }
- button {
- _type "button"
- _class "cursor-pointer rounded p-1 text-slate-500 hover:bg-slate-100 dark:text-slate-400 dark:hover:bg-slate-700"
- _xOn ("click", "mobileNavOpen = false")
- xMarkIcon
- }
- }
- sidebarNavigation currentPath
- }
- }
- div {
- _id "app"
- _class [
- "relative mx-auto flex max-w-8xl justify-center"
- "sm:px-2 lg:px-8 xl:px-12"
- ]
- sidebar currentPath
- div {
- _class [
- "min-w-0 max-w-3xl flex-auto px-4 pt-6 pb-12"
- "lg:max-w-none lg:pl-8 lg:pr-0 xl:px-16"
- ]
- article {
- div {
- _class "mb-8"
- p {
- _class "font-display text-sm font-medium text-sky-500"
- if currentPath.StartsWith("/extensions") then
- "Extensions"
- else
- "Getting started"
- }
- }
- div {
- _class "prose prose-slate dark:prose-invert max-w-none [&_h1]:scroll-mt-28 [&_h2]:scroll-mt-28 [&_h3]:scroll-mt-28"
- raw content
- }
- }
- }
- div {
- _class [
- "hidden xl:sticky xl:top-[4.75rem] xl:-mr-6 xl:block"
- "xl:h-[calc(100vh-4.75rem)] xl:flex-none xl:overflow-y-auto"
- "xl:py-12 xl:pr-6"
- ]
- tableOfContents headings
- }
- }
- }
- }
diff --git a/sln/src/Docs/docs/alpine.md b/sln/src/Docs/docs/alpine.md
deleted file mode 100644
index ca06fda..0000000
--- a/sln/src/Docs/docs/alpine.md
+++ /dev/null
@@ -1,185 +0,0 @@
-# Alpine.js
-
-FSharp.ViewEngine provides type-safe Alpine.js directives through the `Alpine` type.
-
-## Setup
-
-Open the `Alpine` type to access Alpine.js directives:
-
-```fsharp
-open FSharp.ViewEngine
-open type Html
-open type Alpine
-```
-
-## Directives
-
-### x-data
-
-Initialize a component's reactive data with `_xData`:
-
-```fsharp
-div {
- _xData "{ open: false, count: 0 }"
- button {
- _xOn ("click", "count++")
- "Increment"
- }
- span { _xText "count" }
-}
-```
-
-### x-init
-
-Run an expression when a component is initialized:
-
-```fsharp
-div {
- _xData "{ users: [] }"
- _xInit "users = await (await fetch('/api/users')).json()"
-}
-```
-
-### x-show
-
-Toggle the visibility of an element:
-
-```fsharp
-div {
- _xData "{ open: false }"
- button { _xOn ("click", "open = !open"); "Toggle" }
- div { _xShow "open"; "Content here" }
-}
-```
-
-### x-if
-
-Conditionally add or remove an element from the DOM:
-
-```fsharp
-template { _xIf "open"; div { "Shown when open is true" } }
-```
-
-### x-for
-
-Loop over a list and render elements:
-
-```fsharp
-template {
- _xFor "item in items"
- li { _xText "item" }
-}
-```
-
-### x-bind
-
-Dynamically set HTML attributes:
-
-```fsharp
-div {
- _xBind ("class", "open ? 'block' : 'hidden'")
-}
-```
-
-### x-on
-
-Listen for browser events:
-
-```fsharp
-button {
- _xOn ("click", "handleClick()")
- _xOn ("mouseenter", "hovering = true")
- "Click me"
-}
-```
-
-### x-text
-
-Set the text content of an element:
-
-```fsharp
-span { _xText "message" }
-```
-
-### x-model
-
-Two-way data binding for form elements:
-
-```fsharp
-div {
- _xData "{ search: '' }"
- input { _type "text"; _xModel "search" }
- p { _xText "search" }
-}
-```
-
-You can also use modifiers:
-
-```fsharp
-input { _xModel ("value", ".debounce.500ms") }
-```
-
-### x-ref
-
-Reference elements directly:
-
-```fsharp
-div {
- input { _xRef "nameInput" }
- button { _xOn ("click", "$refs.nameInput.focus()"); "Focus" }
-}
-```
-
-## Additional Directives
-
-### x-effect
-
-Execute a script each time one of its dependencies change:
-
-```fsharp
-div { _xEffect "console.log(count)" }
-```
-
-### x-transition
-
-Apply transition classes during enter/leave:
-
-```fsharp
-div {
- _xShow "open"
- _xTransition ()
- "Animated content"
-}
-```
-
-### x-cloak
-
-Hide an element until Alpine is initialized:
-
-```fsharp
-div { _xCloak; "Hidden until Alpine loads" }
-```
-
-### x-teleport
-
-Move an element to another location in the DOM:
-
-```fsharp
-div { _xTeleport "body"; "Teleported to body" }
-```
-
-### x-trap
-
-Trap focus within an element:
-
-```fsharp
-div { _xTrap "open"; "Focus is trapped here" }
-```
-
-### x-id
-
-Scope generated IDs to the component:
-
-```fsharp
-div { _xId "['dropdown']" }
-```
diff --git a/sln/src/Docs/docs/custom.md b/sln/src/Docs/docs/custom.md
deleted file mode 100644
index 3d67109..0000000
--- a/sln/src/Docs/docs/custom.md
+++ /dev/null
@@ -1,228 +0,0 @@
-# Custom Elements & Attributes
-
-FSharp.ViewEngine covers all standard HTML elements and attributes, but you may need custom ones for web components or non-standard attributes.
-
-## Custom Elements
-
-### el
-
-Use `Html.el` to create a custom element with children. This is useful for web components:
-
-```fsharp
-open FSharp.ViewEngine
-open type Html
-
-el "my-component" {
- _class "container"
- p { "Hello from a web component!" }
-}
-```
-
-Renders:
-
-```html
-
- Hello from a web component!
-
-```
-
-### elVoid
-
-Use `Html.elVoid` to create a custom self-closing (void) element:
-
-```fsharp
-elVoid "my-icon" {
- _attr("name", "star")
- _attr("size", "24")
-}
-```
-
-Renders:
-
-```html
-
-```
-
-### Nested Web Components
-
-Custom elements can be nested just like regular elements:
-
-```fsharp
-el "my-card" {
- _attr("variant", "outlined")
- el "my-card-header" {
- h2 { "Card Title" }
- }
- el "my-card-body" {
- p { "Card content goes here." }
- }
- el "my-card-footer" {
- button { _onclick "handleClick()"; "Action" }
- }
-}
-```
-
-## Custom Attributes
-
-### _attr
-
-Use `Html._attr` to add any attribute not covered by the built-in helpers.
-
-#### Key-value attribute
-
-```fsharp
-div {
- _attr("my-custom-attr", "value")
- "Content"
-}
-```
-
-Renders:
-
-```html
-Content
-```
-
-#### Boolean attribute
-
-Pass only the name to render a valueless (boolean) attribute:
-
-```fsharp
-div {
- _attr "my-flag"
- "Content"
-}
-```
-
-Renders:
-
-```html
-Content
-```
-
-### Combining with Built-in Attributes
-
-Custom attributes work alongside all built-in attributes:
-
-```fsharp
-el "sl-button" {
- _attr("variant", "primary")
- _attr("size", "large")
- _attr "pill"
- _onclick "handleClick()"
- _class "my-button"
- "Click Me"
-}
-```
-
-Renders:
-
-```html
-
- Click Me
-
-```
-
-## Extending the Html Type
-
-F# supports [type extensions](https://learn.microsoft.com/en-us/dotnet/fsharp/language-reference/type-extensions) which let you add your own elements and attributes to the `Html` type. This is useful for project-specific conventions or design system components.
-
-### Adding Custom Elements
-
-```fsharp
-open FSharp.ViewEngine
-
-type Html with
- static member val myCard = TagBuilder("my-card") with get
- static member val myIcon = VoidBuilder("my-icon") with get
-```
-
-Then use them just like built-in elements:
-
-```fsharp
-open type Html
-
-myCard {
- _class "shadow-lg"
- h2 { "Title" }
- p { "Card content" }
-}
-
-myIcon { _attr("name", "star") }
-```
-
-### Adding Custom Attributes
-
-```fsharp
-open FSharp.ViewEngine
-
-type Html with
- static member inline _theme (v: string) = { Name = "data-theme"; Value = ValueSome v }
- static member inline _variant (v: string) = { Name = "variant"; Value = ValueSome v }
- static member inline _loading = { Name = "data-loading"; Value = ValueNone }
-```
-
-Then use them alongside built-in attributes:
-
-```fsharp
-open type Html
-
-div {
- _theme "dark"
- _variant "outlined"
- _loading
- "Content"
-}
-```
-
-### Design System Example
-
-You can build a full design system module with reusable elements and attributes:
-
-```fsharp
-open FSharp.ViewEngine
-
-type Ds =
- static member val alert = TagBuilder("ds-alert") with get
- static member val badge = TagBuilder("ds-badge") with get
- static member val tooltip = TagBuilder("ds-tooltip") with get
- static member inline _severity (v: string) = { Name = "severity"; Value = ValueSome v }
- static member inline _placement (v: string) = { Name = "placement"; Value = ValueSome v }
- static member inline _dismissible = { Name = "dismissible"; Value = ValueNone }
-```
-
-```fsharp
-open type Html
-open type Ds
-
-alert {
- _severity "warning"
- _dismissible
- "This is a warning message."
-}
-
-tooltip {
- _placement "top"
- button { "Hover me" }
-}
-```
-
-## Shoelace Example
-
-Here's a more complete example using [Shoelace](https://shoelace.style/) web components:
-
-```fsharp
-el "sl-dialog" {
- _attr("label", "Confirm")
- _attr "open"
- p { "Are you sure?" }
- div {
- _slot "footer"
- el "sl-button" {
- _attr("variant", "primary")
- _onclick "this.closest('sl-dialog').hide()"
- "Confirm"
- }
- }
-}
-```
diff --git a/sln/src/Docs/docs/datastar.md b/sln/src/Docs/docs/datastar.md
deleted file mode 100644
index 62f9a88..0000000
--- a/sln/src/Docs/docs/datastar.md
+++ /dev/null
@@ -1,298 +0,0 @@
-# Datastar
-
-FSharp.ViewEngine provides type-safe Datastar attributes through the `Datastar` type.
-
-## Setup
-
-Open the `Datastar` type to access Datastar attributes:
-
-```fsharp
-open FSharp.ViewEngine
-open type Html
-open type Datastar
-```
-
-## Core Attributes
-
-### data-signals
-
-Define reactive signals on an element:
-
-```fsharp
-div {
- _dataSignals ("count", "0")
- _dataSignals ("name", "'World'")
-}
-```
-
-### data-on
-
-Listen for events and run expressions:
-
-```fsharp
-button {
- _dataOn ("click", "$count++")
- "Increment"
-}
-```
-
-### data-bind
-
-Two-way bind a signal to an input element:
-
-```fsharp
-input { _type "text"; _dataBind "name" }
-```
-
-### data-show
-
-Conditionally show or hide an element:
-
-```fsharp
-div { _dataShow "$count > 0"; "Count is positive" }
-```
-
-### data-text
-
-Set the text content of an element reactively:
-
-```fsharp
-span { _dataText "$count" }
-```
-
-### data-effect
-
-Run an expression whenever its dependencies change:
-
-```fsharp
-div { _dataEffect "console.log($count)" }
-```
-
-### data-init
-
-Run an expression when the element is initialized:
-
-```fsharp
-div { _dataInit "console.log('initialized')" }
-```
-
-### data-attr
-
-Dynamically set an HTML attribute:
-
-```fsharp
-div { _dataAttr ("disabled", "$count === 0") }
-```
-
-### data-class
-
-Toggle a CSS class based on an expression:
-
-```fsharp
-div { _dataClass ("active", "$isActive") }
-```
-
-### data-computed
-
-Define a computed signal derived from other signals:
-
-```fsharp
-div { _dataComputed ("double", "$count * 2") }
-```
-
-### data-style
-
-Dynamically set a CSS style property:
-
-```fsharp
-div { _dataStyle ("color", "$isError ? 'red' : 'green'") }
-```
-
-### data-ref
-
-Reference an element by name:
-
-```fsharp
-input { _dataRef "myInput" }
-```
-
-### data-indicator
-
-Bind a loading indicator signal:
-
-```fsharp
-button { _dataIndicator "loading" }
-```
-
-### data-json-signals
-
-Render signals as JSON for debugging:
-
-```fsharp
-pre { _dataJsonSignals () }
-pre { _dataJsonSignals "{include: /counter/, exclude: /temp$/}" }
-```
-
-### data-ignore
-
-Prevent Datastar from processing an element:
-
-```fsharp
-div { _dataIgnore }
-```
-
-### data-ignore-morph
-
-Prevent morphing of an element during updates:
-
-```fsharp
-div { _dataIgnoreMorph }
-```
-
-### data-on-intersect
-
-Run an expression when an element enters the viewport:
-
-```fsharp
-div { _dataOnIntersect "$count++" }
-```
-
-### data-on-interval
-
-Run an expression on a timed interval:
-
-```fsharp
-div { _dataOnInterval "$count++" }
-```
-
-### data-on-signal-patch
-
-Run an expression when signals are patched:
-
-```fsharp
-div { _dataOnSignalPatch "console.log('patched')" }
-```
-
-### data-on-signal-patch-filter
-
-Filter which signal patches trigger the expression:
-
-```fsharp
-div { _dataOnSignalPatchFilter "{include: /^count$/}" }
-```
-
-### data-preserve-attr
-
-Preserve specified attributes during morphing:
-
-```fsharp
-div { _dataPreserveAttr "class" }
-```
-
-## Pro Attributes
-
-### data-animate
-
-Apply animations to an element:
-
-```fsharp
-div { _dataAnimate "fadeIn 0.5s" }
-```
-
-### data-custom-validity
-
-Set custom validation messages:
-
-```fsharp
-input { _dataCustomValidity "$name === '' ? 'Name is required' : ''" }
-```
-
-### data-on-raf
-
-Run an expression on every animation frame:
-
-```fsharp
-canvas { _dataOnRaf "draw()" }
-```
-
-### data-on-resize
-
-Run an expression when the element is resized:
-
-```fsharp
-div { _dataOnResize "console.log('resized')" }
-```
-
-### data-persist
-
-Persist signals to local storage (or session storage with modifiers):
-
-```fsharp
-div { _dataPersist () } // default key: datastar
-div { _dataPersist "mykey" } // custom storage key
-div { _dataPersist ("mykey", "{include: /foo/}") } // key + filter object
-```
-
-### data-query-string
-
-Sync signals with URL query parameters:
-
-```fsharp
-div { _dataQueryString () }
-div { _dataQueryString "{include: /foo/, exclude: /temp$/}" }
-```
-
-### data-replace-url
-
-Replace the current URL:
-
-```fsharp
-div { _dataReplaceUrl "/new-path" }
-```
-
-### data-rocket
-
-Create a Rocket web component:
-
-```fsharp
-div { _dataRocket "{ endpoint: '/stream' }" }
-```
-
-### data-scroll-into-view
-
-Scroll the element into view:
-
-```fsharp
-div { _dataScrollIntoView }
-```
-
-### data-view-transition
-
-Apply view transitions:
-
-```fsharp
-div { _dataViewTransition "fade" }
-```
-
-## Complete Example
-
-Here's a complete example combining multiple Datastar attributes:
-
-```fsharp
-div {
- _dataSignals ("count", "0")
- _dataSignals ("name", "'World'")
- _dataComputed ("greeting", "'Hello, ' + $name + '!'")
-
- input { _type "text"; _dataBind "name" }
- span { _dataText "$greeting" }
-
- button {
- _dataOn ("click", "$count++")
- _dataClass ("active", "$count > 0")
- "Clicked "
- }
- span { _dataText "$count" }
- span { _dataShow "$count > 0"; " times" }
-}
-```
diff --git a/sln/src/Docs/docs/home.md b/sln/src/Docs/docs/home.md
deleted file mode 100644
index c69be79..0000000
--- a/sln/src/Docs/docs/home.md
+++ /dev/null
@@ -1,59 +0,0 @@
-# FSharp.ViewEngine
-
-A minimal, fast view engine for F# that combines the best ideas from several F# view engines. Inspired by [Giraffe.ViewEngine](https://github.com/giraffe-fsharp/Giraffe.ViewEngine), [Feliz.ViewEngine](https://github.com/dbrattli/Feliz.ViewEngine), [Oxpecker.ViewEngine](https://github.com/Lanayx/Oxpecker), and [Bolero](https://github.com/fsbolero/Bolero).
-
-## Design
-
-FSharp.ViewEngine uses **computation expressions** (like Oxpecker.ViewEngine and Bolero) to build elements. Each element takes a **Feliz-style single sequence** of attributes and children — there are no separate attribute and children lists. Attributes are **prefixed with underscore** by convention (like Giraffe.ViewEngine, e.g. `_class`, `_id`, `_hxGet`), which produces clean syntax with nice syntax highlighting. The computation expression allows **mixed yielding** of strings, elements, and attributes in any order, so there is no need for a special `_children` attribute.
-
-## Key Features
-
-- **Minimal and fast** — as lean as possible while remaining expressive and type-safe
-- **Type-safe HTML generation** with F#
-- **Built-in support for HTMX, Alpine.js, Datastar, Tailwind CSS, and SVG**
-- **Composable and functional approach**
-- **No runtime dependencies**
-
-## Quick Example
-
-```fsharp
-open FSharp.ViewEngine
-open type Html
-open type Htmx
-open type Tailwind
-
-let myPage =
- html {
- _lang "en"
- head {
- title "My App"
- meta { _charset "utf-8" }
- link { _href "/css/tailwind.css"; _rel "stylesheet" }
- }
- body {
- _class "bg-gray-100"
- div {
- _class [ "container"; "mx-auto"; "p-4" ]
- h1 {
- _class [ "text-3xl"; "font-bold"; "text-blue-600"; "mb-4" ]
- "Welcome!"
- }
- button {
- _class [ "bg-blue-500"; "text-white"; "px-4"; "py-2"; "rounded" ]
- _hxGet "/api/data"
- _hxTarget "#content"
- "Load Content"
- }
- div {
- _id "content"
- _class [ "mt-4" ]
- }
- }
- }
- }
- |> Render.toHtmlDocString
-```
-
-## Getting Started
-
-To get started with FSharp.ViewEngine, check out the [Installation](installation) guide and then see the [Usage](usage) example.
diff --git a/sln/src/Docs/docs/htmx.md b/sln/src/Docs/docs/htmx.md
deleted file mode 100644
index 8b284aa..0000000
--- a/sln/src/Docs/docs/htmx.md
+++ /dev/null
@@ -1,206 +0,0 @@
-# HTMX
-
-FSharp.ViewEngine provides type-safe HTMX attributes through the `Htmx` type.
-
-## Setup
-
-Open the `Htmx` type to access HTMX attributes:
-
-```fsharp
-open FSharp.ViewEngine
-open type Html
-open type Htmx
-```
-
-## Request Attributes
-
-### hx-get
-
-Issue a GET request to a URL:
-
-```fsharp
-button {
- _hxGet "/api/data"
- "Load Data"
-}
-```
-
-### hx-post
-
-Issue a POST request to a URL:
-
-```fsharp
-form {
- _hxPost "/api/submit"
- input { _type "text"; _name "email" }
- button { "Submit" }
-}
-```
-
-### hx-delete
-
-Issue a DELETE request to a URL:
-
-```fsharp
-button {
- _hxDelete "/api/items/1"
- "Delete"
-}
-```
-
-### Generic hx attribute
-
-Use `_hx` for any HTMX attribute not covered by a dedicated helper:
-
-```fsharp
-div {
- _hx ("put", "/api/items/1")
- _hx ("patch", "/api/items/1")
-}
-```
-
-## Targeting and Swapping
-
-### hx-target
-
-Specify the target element for the response:
-
-```fsharp
-button {
- _hxGet "/api/data"
- _hxTarget "#result"
- "Load into #result"
-}
-div { _id "result" }
-```
-
-### hx-swap
-
-Control how the response content is swapped in:
-
-```fsharp
-button {
- _hxGet "/api/items"
- _hxSwap "beforeend"
- "Append Items"
-}
-```
-
-Common swap values: `innerHTML`, `outerHTML`, `beforebegin`, `afterbegin`, `beforeend`, `afterend`, `delete`, `none`.
-
-### hx-swap-oob
-
-Swap content out-of-band (outside the target):
-
-```fsharp
-div {
- _hxSwapOOB "true"
- _id "notifications"
- "Updated notification content"
-}
-```
-
-## Triggering and Events
-
-### hx-trigger
-
-Specify the event that triggers the request:
-
-```fsharp
-input {
- _type "text"
- _hxGet "/api/search"
- _hxTrigger "keyup changed delay:500ms"
- _hxTarget "#results"
-}
-```
-
-### hx-on
-
-Listen for HTMX events:
-
-```fsharp
-form {
- _hxPost "/api/submit"
- _hxOn ("htmx:beforeRequest", "showSpinner()")
- _hxOn ("htmx:afterRequest", "hideSpinner()")
-}
-```
-
-## Other Attributes
-
-### hx-indicator
-
-Show a loading indicator during requests:
-
-```fsharp
-button {
- _hxGet "/api/slow"
- _hxIndicator "#spinner"
- "Load"
-}
-div { _id "spinner"; _class "htmx-indicator"; "Loading..." }
-```
-
-### hx-include
-
-Include additional element values in the request:
-
-```fsharp
-button {
- _hxPost "/api/submit"
- _hxInclude "[name='email']"
- "Submit"
-}
-```
-
-### hx-encoding
-
-Set the encoding type for requests:
-
-```fsharp
-form {
- _hxPost "/api/upload"
- _hxEncoding "multipart/form-data"
-}
-```
-
-### hx-vals
-
-Add additional values to the request:
-
-```fsharp
-button {
- _hxPost "/api/action"
- _hxVals """{"key": "value"}"""
- "Submit"
-}
-```
-
-### hx-history
-
-Control the history behavior:
-
-```fsharp
-div { _hxHistory "false" }
-```
-
-## Complete Example
-
-Here's a complete example combining multiple HTMX attributes:
-
-```fsharp
-div {
- _class "search-container"
- input {
- _type "text"
- _name "q"
- _hxGet "/api/search"
- _hxTrigger "keyup changed delay:300ms"
- _hxTarget "#search-results"
- _hxIndicator "#search-spinner"
- }
- span { _id "search-spinner"; _class "htmx-indicator"; "Searching..." }
- div { _id "search-results" }
-}
-```
diff --git a/sln/src/Docs/docs/installation.md b/sln/src/Docs/docs/installation.md
deleted file mode 100644
index adbf580..0000000
--- a/sln/src/Docs/docs/installation.md
+++ /dev/null
@@ -1,23 +0,0 @@
-# Installation
-
-FSharp.ViewEngine is distributed as a NuGet package. You can install it using your preferred package manager.
-
-## Using .NET CLI
-
-```bash
-dotnet package add FSharp.ViewEngine
-```
-
-## Using Paket CLI
-
-```bash
-dotnet paket add FSharp.ViewEngine
-```
-
-## Requirements
-
-- .NET 8.0, 9.0, or 10.0
-
-## Next Steps
-
-Once you have FSharp.ViewEngine installed, head over to the [Usage](usage) guide to start building your first HTML views.
\ No newline at end of file
diff --git a/sln/src/Docs/docs/svg.md b/sln/src/Docs/docs/svg.md
deleted file mode 100644
index 2713b26..0000000
--- a/sln/src/Docs/docs/svg.md
+++ /dev/null
@@ -1,155 +0,0 @@
-# SVG
-
-FSharp.ViewEngine provides type-safe SVG elements and attributes through the `Svg` type.
-
-## Setup
-
-Open the `Svg` type to access SVG elements and attributes:
-
-```fsharp
-open FSharp.ViewEngine
-open type Html
-open type Svg
-```
-
-## Elements
-
-### svg
-
-The root SVG container element:
-
-```fsharp
-svg {
- _viewBox "0 0 24 24"
- _width 24
- _height 24
- // child elements here
-}
-```
-
-### path
-
-Define a shape using a path data string:
-
-```fsharp
-svg {
- _viewBox "0 0 24 24"
- _fill "none"
- path {
- _d "M12 2L2 22h20L12 2z"
- _stroke "currentColor"
- _strokeWidth 2
- }
-}
-```
-
-### circle
-
-Draw a circle:
-
-```fsharp
-svg {
- _viewBox "0 0 100 100"
- circle {
- _cx 50
- _cy 50
- _r 40
- _fill "blue"
- _stroke "black"
- _strokeWidth 2
- }
-}
-```
-
-## Attributes
-
-### Dimensions
-
-Set the width and height of the SVG:
-
-```fsharp
-svg { _width 48; _height 48 }
-```
-
-### viewBox
-
-Define the coordinate system and aspect ratio:
-
-```fsharp
-svg { _viewBox "0 0 100 100" }
-```
-
-### Fill and Stroke
-
-Control the fill color and stroke styling:
-
-```fsharp
-path {
- _d "M0 0 L10 10"
- _fill "none"
- _stroke "red"
- _strokeWidth 2
- _strokeLinecap "round"
- _strokeLinejoin "round"
-}
-```
-
-### Fill Rule and Clip Rule
-
-Control how overlapping paths are filled or clipped:
-
-```fsharp
-path {
- _d "M0 0 L10 10 L20 0 Z"
- _fillRule "evenodd"
- _clipRule "evenodd"
-}
-```
-
-## Complete Example
-
-Here's a complete icon example:
-
-```fsharp
-let checkIcon =
- svg {
- _viewBox "0 0 24 24"
- _width 24
- _height 24
- _fill "none"
- path {
- _d "M5 13l4 4L19 7"
- _stroke "currentColor"
- _strokeWidth 2
- _strokeLinecap "round"
- _strokeLinejoin "round"
- }
- }
-```
-
-And a more complex example with multiple elements:
-
-```fsharp
-let statusIcon =
- svg {
- _viewBox "0 0 100 100"
- _width 100
- _height 100
- circle {
- _cx 50
- _cy 50
- _r 45
- _fill "green"
- _stroke "darkgreen"
- _strokeWidth 2
- }
- path {
- _d "M30 50 L45 65 L70 35"
- _fill "none"
- _stroke "white"
- _strokeWidth 6
- _strokeLinecap "round"
- _strokeLinejoin "round"
- }
- }
-```
diff --git a/sln/src/Docs/docs/tailwind.md b/sln/src/Docs/docs/tailwind.md
deleted file mode 100644
index 196647b..0000000
--- a/sln/src/Docs/docs/tailwind.md
+++ /dev/null
@@ -1,201 +0,0 @@
-# Tailwind
-
-FSharp.ViewEngine provides custom elements for Tailwind UI components through the `Tailwind` type.
-
-## Setup
-
-Open the `Tailwind` type to access Tailwind custom elements:
-
-```fsharp
-open FSharp.ViewEngine
-open type Html
-open type Tailwind
-```
-
-## Form Elements
-
-### Autocomplete
-
-Build an autocomplete input with filtered options:
-
-```fsharp
-elAutocomplete {
- _class "w-64"
- input { _type "text"; _placeholder "Search..." }
- elOptions {
- elOption { "Option 1" }
- elOption { "Option 2" }
- elOption { "Option 3" }
- }
-}
-```
-
-### Select
-
-Create a custom select dropdown:
-
-```fsharp
-elSelect {
- _class "w-48"
- elSelectedContent { "Choose an option" }
- elOptions {
- elOption { "Small" }
- elOption { "Medium" }
- elOption { "Large" }
- }
-}
-```
-
-## Overlay Elements
-
-### Dialog
-
-Build a modal dialog with a backdrop:
-
-```fsharp
-elDialog {
- elDialogBackdrop { _class "fixed inset-0 bg-black/30" }
- elDialogPanel {
- _class "mx-auto max-w-sm rounded bg-white p-6"
- h2 { "Dialog Title" }
- p { "Dialog content goes here." }
- button { "Close" }
- }
-}
-```
-
-### Dropdown
-
-Create a dropdown menu:
-
-```fsharp
-elDropdown {
- button { "Options" }
- elMenu {
- _class "absolute mt-2 w-48 rounded bg-white shadow-lg"
- a { _href "#"; "Edit" }
- a { _href "#"; "Delete" }
- }
-}
-```
-
-## Navigation Elements
-
-### Tab Group
-
-Build a tabbed interface:
-
-```fsharp
-elTabGroup {
- elTabList {
- _class "flex space-x-1 rounded-xl bg-blue-900/20 p-1"
- button { "Tab 1" }
- button { "Tab 2" }
- button { "Tab 3" }
- }
- elTabPanels {
- div { "Content for Tab 1" }
- div { "Content for Tab 2" }
- div { "Content for Tab 3" }
- }
-}
-```
-
-## Command Palette
-
-### Command Palette
-
-Build a command palette for search and navigation:
-
-```fsharp
-elCommandPalette {
- input { _type "text"; _placeholder "Search commands..." }
- elCommandList {
- elCommandGroup {
- _class "p-2"
- div { "Open File" }
- div { "Run Command" }
- }
- elCommandPreview {
- _class "p-4"
- "Preview content here"
- }
- }
- elNoResults { "No results found." }
-}
-```
-
-## Attributes
-
-### popover
-
-Mark an element as a popover:
-
-```fsharp
-div { _popover; "Popover content" }
-```
-
-### anchor
-
-Position an element relative to a reference using anchor positioning:
-
-```fsharp
-div {
- _anchor "bottom-start"
- "Anchored content"
-}
-```
-
-## Defaults
-
-Use `elDefaults` to set default values for child components:
-
-```fsharp
-elDefaults {
- _class "text-sm"
- elSelect {
- elOptions {
- elOption { "A" }
- elOption { "B" }
- }
- }
-}
-```
-
-## Complete Example
-
-Here's a complete example combining several Tailwind UI elements:
-
-```fsharp
-div {
- _class "p-8"
- elTabGroup {
- elTabList {
- _class "flex space-x-1 rounded-xl bg-blue-900/20 p-1"
- button { _class "rounded-lg px-3 py-2"; "Search" }
- button { _class "rounded-lg px-3 py-2"; "Browse" }
- }
- elTabPanels {
- div {
- elAutocomplete {
- _class "mt-4 w-full"
- input { _type "text"; _placeholder "Search items..." }
- elOptions {
- elOption { "Item 1" }
- elOption { "Item 2" }
- }
- }
- }
- div {
- elSelect {
- _class "mt-4 w-full"
- elOptions {
- elOption { "Category A" }
- elOption { "Category B" }
- }
- }
- }
- }
- }
-}
-```
diff --git a/sln/src/Docs/docs/usage.md b/sln/src/Docs/docs/usage.md
deleted file mode 100644
index 71dfd73..0000000
--- a/sln/src/Docs/docs/usage.md
+++ /dev/null
@@ -1,54 +0,0 @@
-# Usage
-
-FSharp.ViewEngine integrates with [Giraffe](https://giraffe.wiki/) by rendering elements to an HTML string and returning it via Giraffe's `htmlString` handler.
-
-## Minimal Example
-
-```fsharp
-open Microsoft.AspNetCore.Builder
-open Microsoft.Extensions.DependencyInjection
-open Giraffe
-open FSharp.ViewEngine
-open type Html
-
-let indexView =
- html {
- _lang "en"
- head {
- title "My App"
- meta { _charset "utf-8" }
- }
- body {
- h1 { "Hello from FSharp.ViewEngine!" }
- p { "Served by Giraffe." }
- }
- }
-
-let indexHandler : HttpHandler =
- fun next ctx ->
- let html = Render.toHtmlDocString indexView
- htmlString html next ctx
-
-let webApp =
- choose [
- GET >=> route "/" >=> indexHandler
- ]
-
-[]
-let main args =
- let builder = WebApplication.CreateBuilder(args)
- builder.Services.AddGiraffe() |> ignore
-
- let app = builder.Build()
- app.UseGiraffe(webApp)
- app.Run()
- 0
-```
-
-## How It Works
-
-1. Build your HTML using FSharp.ViewEngine elements
-2. Call `Render.toHtmlDocString` to get a `` string (or `Render.toString` for a fragment without the doctype)
-3. Return it with Giraffe's `htmlString` handler
-
-That's it — no special adapter or middleware needed.
diff --git a/sln/src/Docs/input.css b/sln/src/Docs/input.css
index 423a23e..da32580 100644
--- a/sln/src/Docs/input.css
+++ b/sln/src/Docs/input.css
@@ -1,5 +1,5 @@
@import "tailwindcss";
-@source "./{*.fs,**/*.fs}";
+@source "./src/**/*.fs";
@plugin "@tailwindcss/typography";
@custom-variant dark (&:where(.dark, .dark *));
diff --git a/sln/src/Docs/paket.references b/sln/src/Docs/paket.references
index 52d67b0..9dd9f5a 100644
--- a/sln/src/Docs/paket.references
+++ b/sln/src/Docs/paket.references
@@ -1,5 +1,4 @@
Giraffe
-Markdig
Serilog
Serilog.AspNetCore
Serilog.Sinks.Console
diff --git a/sln/src/Docs/Config.fs b/sln/src/Docs/src/Common/Config.fs
similarity index 54%
rename from sln/src/Docs/Config.fs
rename to sln/src/Docs/src/Common/Config.fs
index d64009c..c649637 100644
--- a/sln/src/Docs/Config.fs
+++ b/sln/src/Docs/src/Common/Config.fs
@@ -1,34 +1,40 @@
-namespace Docs
+namespace Docs.Common
open System
module Env =
- let variable (key: string) =
- match Environment.GetEnvironmentVariable(key) with
- | value when String.IsNullOrEmpty(value) -> failwith $"Environment variable '{key}' is required"
- | value -> value
-
- let variableOrDefault (key: string) (defaultValue: string) =
+ let variableOrDefault (key:string) (defaultValue:string) =
match Environment.GetEnvironmentVariable(key) with
| value when String.IsNullOrEmpty(value) -> defaultValue
| value -> value
type SeqConfig =
- { endpoint: string }
+ { endpoint:string }
module SeqConfig =
let load () =
{ endpoint = Env.variableOrDefault "SEQ_ENDPOINT" "http://localhost:5341" }
+type ReleaseConfig =
+ { version:string
+ commit:string }
+
+module ReleaseConfig =
+ let load () =
+ { version = Env.variableOrDefault "RELEASE_VERSION" "development"
+ commit = Env.variableOrDefault "RELEASE_COMMIT" "local" }
+
type Config =
- { debug: bool
- appName: string
- serverUrl: string
- seq: SeqConfig }
+ { debug:bool
+ appName:string
+ serverUrl:string
+ release:ReleaseConfig
+ seq:SeqConfig }
module Config =
let load () =
{ debug = Env.variableOrDefault "DEBUG" "false" |> Boolean.Parse
appName = "fsharp-viewengine-docs"
serverUrl = Env.variableOrDefault "SERVER_URL" "https://localhost:5000"
+ release = ReleaseConfig.load ()
seq = SeqConfig.load () }
diff --git a/sln/src/Docs/src/Common/Handler.fs b/sln/src/Docs/src/Common/Handler.fs
new file mode 100644
index 0000000..58dedc3
--- /dev/null
+++ b/sln/src/Docs/src/Common/Handler.fs
@@ -0,0 +1,18 @@
+namespace Docs.Common
+
+open Docs.Pages
+open FSharp.ViewEngine
+open Giraffe
+
+module Handler =
+ let render page : HttpHandler =
+ let html = page |> View.document Registry.navigation |> Render.toHtmlDocString
+ htmlString html
+
+ let private pageRoutes =
+ Registry.all
+ |> List.collect (fun page ->
+ (page.path :: page.aliases)
+ |> List.map (fun path -> route path >=> render page))
+
+ let routes : HttpHandler = choose pageRoutes
diff --git a/sln/src/Docs/src/Common/Model.fs b/sln/src/Docs/src/Common/Model.fs
new file mode 100644
index 0000000..409eb6d
--- /dev/null
+++ b/sln/src/Docs/src/Common/Model.fs
@@ -0,0 +1,59 @@
+namespace Docs.Common
+
+type InlineContent =
+ | Text of string
+ | Strong of InlineContent list
+ | Code of string
+ | Link of label:string * href:string
+
+type DocHeading =
+ { id:string
+ title:string
+ level:int }
+
+type ComparisonBar =
+ { label:string
+ duration:string
+ comparison:string
+ widthPercent:int
+ highlighted:bool }
+
+type ComparisonChart =
+ { label:string
+ title:string
+ description:string
+ bars:ComparisonBar list }
+
+type DocNode =
+ | Heading of DocHeading
+ | Paragraph of InlineContent list
+ | UnorderedList of InlineContent list list
+ | OrderedList of InlineContent list list
+ | BarChart of ComparisonChart
+ | DataTable of headers:string list * rows:string list list
+ | CodeBlock of language:string * source:string
+
+type DocPage =
+ { id:string
+ path:string
+ aliases:string list
+ navLabel:string
+ category:string
+ title:string
+ browserTitle:string
+ nodes:DocNode list }
+
+type NavSection =
+ { label:string
+ pages:DocPage list }
+
+module DocPage =
+ let headings page =
+ page.nodes
+ |> List.choose (function
+ | Heading heading -> Some heading
+ | _ -> None)
+
+ let tableOfContents page =
+ headings page
+ |> List.filter (fun heading -> heading.level <= 3)
diff --git a/sln/src/Docs/src/Common/View.fs b/sln/src/Docs/src/Common/View.fs
new file mode 100644
index 0000000..d4f1396
--- /dev/null
+++ b/sln/src/Docs/src/Common/View.fs
@@ -0,0 +1,432 @@
+namespace Docs.Common
+
+open System
+open FSharp.ViewEngine
+open type Datastar
+open type Html
+open type Svg
+
+module View =
+ let private outlineIcon (classes:string) (ariaHidden:bool) (d:string) =
+ svg {
+ _xmlns "http://www.w3.org/2000/svg"
+ _fill "none"
+ _viewBox "0 0 24 24"
+ _strokeWidth 1.5
+ _stroke "currentColor"
+ _class classes
+ if ariaHidden then _ariaHidden true
+ path {
+ _strokeLinecap "round"
+ _strokeLinejoin "round"
+ _d d
+ }
+ }
+
+ let private menuIcon =
+ outlineIcon "h-6 w-6" false "M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5"
+
+ let private xMarkIcon =
+ outlineIcon "h-6 w-6" false "M6 18 18 6M6 6l12 12"
+
+ let private githubIcon =
+ svg {
+ _ariaHidden true
+ _viewBox "0 0 16 16"
+ _class "h-6 w-6 fill-slate-400 group-hover:fill-slate-500 dark:group-hover:fill-slate-300"
+ path { _d "M8 0C3.58 0 0 3.58 0 8C0 11.54 2.29 14.53 5.47 15.59C5.87 15.66 6.02 15.42 6.02 15.21C6.02 15.02 6.01 14.39 6.01 13.72C4 14.09 3.48 13.23 3.32 12.78C3.23 12.55 2.84 11.84 2.5 11.65C2.22 11.5 1.82 11.13 2.49 11.12C3.12 11.11 3.57 11.7 3.72 11.94C4.44 13.15 5.59 12.81 6.05 12.6C6.12 12.08 6.33 11.73 6.56 11.53C4.78 11.33 2.92 10.64 2.92 7.58C2.92 6.71 3.23 5.99 3.74 5.43C3.66 5.23 3.38 4.41 3.82 3.31C3.82 3.31 4.49 3.1 6.02 4.13C6.66 3.95 7.34 3.86 8.02 3.86C8.7 3.86 9.38 3.95 10.02 4.13C11.55 3.09 12.22 3.31 12.22 3.31C12.66 4.41 12.38 5.23 12.3 5.43C12.81 5.99 13.12 6.7 13.12 7.58C13.12 10.65 11.25 11.33 9.47 11.53C9.76 11.78 10.01 12.26 10.01 13.01C10.01 14.08 10 14.94 10 15.21C10 15.42 10.15 15.67 10.55 15.59C13.71 14.53 16 11.53 16 8C16 3.58 12.42 0 8 0Z" }
+ }
+
+ let private sunIcon =
+ svg {
+ _class "h-5 w-5 text-sky-500 dark:hidden"
+ _xmlns "http://www.w3.org/2000/svg"
+ _viewBox "0 0 20 20"
+ _fill "currentColor"
+ _ariaHidden true
+ path { _d "M10 2a.75.75 0 0 1 .75.75v1.5a.75.75 0 0 1-1.5 0v-1.5A.75.75 0 0 1 10 2ZM10 15a.75.75 0 0 1 .75.75v1.5a.75.75 0 0 1-1.5 0v-1.5A.75.75 0 0 1 10 15ZM10 7a3 3 0 1 0 0 6 3 3 0 0 0 0-6ZM15.657 5.404a.75.75 0 1 0-1.06-1.06l-1.061 1.06a.75.75 0 0 0 1.06 1.06l1.06-1.06ZM6.464 14.596a.75.75 0 1 0-1.06-1.06l-1.06 1.06a.75.75 0 0 0 1.06 1.06l1.06-1.06ZM18 10a.75.75 0 0 1-.75.75h-1.5a.75.75 0 0 1 0-1.5h1.5A.75.75 0 0 1 18 10ZM5 10a.75.75 0 0 1-.75.75h-1.5a.75.75 0 0 1 0-1.5h1.5A.75.75 0 0 1 5 10ZM14.596 15.657a.75.75 0 0 0 1.06-1.06l-1.06-1.061a.75.75 0 1 0-1.06 1.06l1.06 1.06ZM5.404 6.464a.75.75 0 0 0 1.06-1.06l-1.06-1.06a.75.75 0 1 0-1.061 1.06l1.06 1.06Z" }
+ }
+
+ let private moonIcon =
+ outlineIcon
+ "hidden h-5 w-5 stroke-sky-500 dark:block"
+ true
+ "M21.752 15.002A9.72 9.72 0 0 1 18 15.75c-5.385 0-9.75-4.365-9.75-9.75 0-1.33.266-2.597.748-3.752A9.753 9.753 0 0 0 3 11.25C3 16.635 7.365 21 12.75 21a9.753 9.753 0 0 0 9.002-5.998Z"
+
+ let private sunIconSmall =
+ outlineIcon
+ "h-5 w-5"
+ false
+ "M12 3v2.25m6.364.386-1.591 1.591M21 12h-2.25m-.386 6.364-1.591-1.591M12 18.75V21m-4.773-4.227-1.591 1.591M5.25 12H3m4.227-4.773L5.636 5.636M15.75 12a3.75 3.75 0 1 1-7.5 0 3.75 3.75 0 0 1 7.5 0Z"
+
+ let private moonIconSmall =
+ outlineIcon
+ "h-5 w-5"
+ false
+ "M21.752 15.002A9.72 9.72 0 0 1 18 15.75c-5.385 0-9.75-4.365-9.75-9.75 0-1.33.266-2.597.748-3.752A9.753 9.753 0 0 0 3 11.25C3 16.635 7.365 21 12.75 21a9.753 9.753 0 0 0 9.002-5.998Z"
+
+ let private monitorIcon =
+ outlineIcon
+ "h-5 w-5"
+ false
+ "M9 17.25v1.007a3 3 0 0 1-.879 2.122L7.5 21h9l-.621-.621A3 3 0 0 1 15 18.257V17.25m6-12V15a2.25 2.25 0 0 1-2.25 2.25H5.25A2.25 2.25 0 0 1 3 15V5.25m18 0A2.25 2.25 0 0 0 18.75 3H5.25A2.25 2.25 0 0 0 3 5.25m18 0V12a2.25 2.25 0 0 1-2.25 2.25H5.25A2.25 2.25 0 0 1 3 12V5.25"
+
+ let private pageHeader =
+ header {
+ _class [
+ "sticky top-0 z-50 flex flex-none flex-wrap items-center justify-between"
+ "bg-white/75 px-4 py-5 shadow-md shadow-slate-900/5 backdrop-blur transition duration-500"
+ "sm:px-6 lg:px-8 dark:bg-slate-900/75 dark:shadow-none dark:backdrop-blur"
+ ]
+ div {
+ _class "flex items-center gap-4"
+ div {
+ _class "flex lg:hidden"
+ button {
+ _type "button"
+ _ariaLabel "Open navigation"
+ _dataOn ("click", "$mobileNavOpen = true")
+ _dataAttr ("aria-expanded", "$mobileNavOpen")
+ _class "relative cursor-pointer rounded-lg p-1 text-slate-500 hover:bg-slate-100 dark:text-slate-400 dark:hover:bg-slate-700"
+ menuIcon
+ }
+ }
+ a {
+ _href "/"
+ _class "flex items-center gap-2 text-sm font-semibold tracking-wider text-slate-700 dark:text-white"
+ img { _src "/logo.svg"; _alt "FSharp.ViewEngine"; _class "h-6 w-6" }
+ "FSharp.ViewEngine"
+ }
+ }
+ div {
+ _class "relative flex basis-0 items-center justify-end gap-6 sm:gap-8 md:grow"
+ div {
+ _class "relative z-10"
+ _dataOn ("click", [ "outside" ], "$themeMenuOpen = false")
+ button {
+ _type "button"
+ _ariaLabel "Choose color theme"
+ _class "flex h-8 w-8 cursor-pointer items-center justify-center rounded-lg p-1 transition-colors hover:bg-slate-100 dark:hover:bg-slate-700"
+ _dataOn ("click", "$themeMenuOpen = !$themeMenuOpen")
+ _dataAttr ("aria-expanded", "$themeMenuOpen")
+ sunIcon
+ moonIcon
+ }
+ div {
+ _dataShow "$themeMenuOpen"
+ _style "display: none"
+ _class [
+ "absolute right-0 top-full mt-3 w-36 overflow-hidden rounded-lg"
+ "bg-white py-1 text-sm font-semibold text-slate-700 shadow-lg ring-1"
+ "ring-slate-900/10 dark:bg-slate-800 dark:text-slate-300 dark:ring-0"
+ ]
+ for value, label, icon in [ "light", "Light", sunIconSmall; "dark", "Dark", moonIconSmall; "system", "System", monitorIcon ] do
+ button {
+ _type "button"
+ _class "flex w-full items-center gap-2 px-3 py-2 hover:bg-slate-100 dark:hover:bg-slate-700/50"
+ _dataOn ("click", $"$theme = '{value}'; $themeMenuOpen = false")
+ _dataClass ("text-sky-500", $"$theme === '{value}'")
+ _dataAttr ("aria-pressed", $"$theme === '{value}'")
+ icon
+ label
+ }
+ }
+ }
+ a {
+ _href "https://github.com/meiermade/FSharp.ViewEngine"
+ _ariaLabel "FSharp.ViewEngine on GitHub"
+ _class "group"
+ githubIcon
+ }
+ }
+ }
+
+ let private navLink currentPath (page:DocPage) =
+ let isActive = currentPath = page.path || List.contains currentPath page.aliases
+ li {
+ _class "relative"
+ a {
+ _class [
+ "block w-full pl-3.5 before:pointer-events-none before:absolute"
+ "before:-left-1 before:top-1/2 before:h-1.5 before:w-1.5"
+ "before:-translate-y-1/2 before:rounded-full"
+ if isActive then
+ "font-semibold text-sky-500 before:bg-sky-500"
+ else
+ "text-slate-500 before:hidden before:bg-slate-300 hover:text-slate-600"
+ + " hover:before:block dark:text-slate-400 dark:before:bg-slate-700 dark:hover:text-slate-300"
+ ]
+ _href page.path
+ _dataOn ("click", "$mobileNavOpen = false")
+ page.navLabel
+ }
+ }
+
+ let private sidebarNavigation navigation currentPath =
+ nav {
+ _ariaLabel "Documentation"
+ _class "text-base lg:text-sm"
+ ul {
+ _role "list"
+ _class "space-y-9"
+ for navSection in navigation do
+ li {
+ h2 {
+ _class "font-display font-medium text-slate-900 dark:text-white"
+ navSection.label
+ }
+ ul {
+ _role "list"
+ _class "mt-2 space-y-2 border-l-2 border-slate-100 lg:mt-4 lg:space-y-4 lg:border-slate-200 dark:border-slate-800"
+ for page in navSection.pages do
+ navLink currentPath page
+ }
+ }
+ }
+ }
+
+ let private sidebar navigation currentPath =
+ div {
+ _class "hidden lg:relative lg:block lg:flex-none"
+ div {
+ _class "sticky top-[4.75rem] -ml-0.5 h-[calc(100vh-4.75rem)] w-64 overflow-y-auto py-16 pl-0.5 pr-8 xl:w-72 xl:pr-16"
+ sidebarNavigation navigation currentPath
+ }
+ }
+
+ let private tableOfContents page =
+ let headings = DocPage.tableOfContents page
+ if List.isEmpty headings then
+ empty
+ else
+ nav {
+ _ariaLabel "On this page"
+ _class "sticky top-[4.75rem] -mr-6 w-56 flex-none overflow-y-auto py-16 pr-6"
+ h2 {
+ _class "font-display text-sm font-medium text-zinc-900 dark:text-white"
+ "On this page"
+ }
+ ul {
+ _role "list"
+ _class "mt-4 space-y-3 text-sm"
+ for heading in headings do
+ li {
+ _class (if heading.level = 3 then "pl-3" else "")
+ a {
+ _href $"#{heading.id}"
+ _class "text-zinc-500 hover:text-zinc-600 dark:text-zinc-400 dark:hover:text-zinc-300"
+ heading.title
+ }
+ }
+ }
+ }
+
+ let rec private renderInline content =
+ match content with
+ | Text value -> text value
+ | Strong children -> strong { for child in children do renderInline child }
+ | InlineContent.Code value -> code { value }
+ | Link(label, href) -> a { _href href; label }
+
+ let private renderNode node =
+ match node with
+ | Heading heading ->
+ match heading.level with
+ | 2 -> h2 { _id heading.id; heading.title }
+ | 3 -> h3 { _id heading.id; heading.title }
+ | _ -> h4 { _id heading.id; heading.title }
+ | Paragraph children -> p { for child in children do renderInline child }
+ | UnorderedList items -> ul { for item in items do li { for child in item do renderInline child } }
+ | OrderedList items -> ol { for item in items do li { for child in item do renderInline child } }
+ | BarChart chart ->
+ figure {
+ _ariaLabel chart.label
+ _class "not-prose my-8 rounded-xl border border-slate-200 bg-slate-50/60 p-5 sm:p-6 dark:border-slate-700 dark:bg-slate-800/40"
+ figcaption {
+ p {
+ _class "font-semibold text-slate-900 dark:text-white"
+ chart.title
+ }
+ p {
+ _class "mt-1 text-sm text-slate-600 dark:text-slate-300"
+ chart.description
+ }
+ }
+ div {
+ _class "mt-6 space-y-5"
+ for bar in chart.bars do
+ div {
+ div {
+ _class "mb-2 flex flex-wrap items-baseline justify-between gap-x-4 gap-y-1 text-sm"
+ span {
+ _class [
+ "font-medium"
+ if bar.highlighted then "text-sky-600 dark:text-sky-400" else "text-slate-700 dark:text-slate-200"
+ ]
+ bar.label
+ }
+ span {
+ _class "font-mono text-xs tabular-nums text-slate-600 dark:text-slate-300"
+ $"{bar.duration} · {bar.comparison}"
+ }
+ }
+ div {
+ _ariaHidden true
+ _class "h-3 overflow-hidden rounded-full bg-slate-200 dark:bg-slate-700"
+ div {
+ _class [
+ "h-full rounded-full"
+ if bar.highlighted then "bg-sky-500" else "bg-slate-400 dark:bg-slate-500"
+ ]
+ _style ("width: " + string (Math.Clamp(bar.widthPercent, 0, 100)) + "%")
+ }
+ }
+ }
+ }
+ }
+ | DataTable(headers, rows) ->
+ div {
+ _class "not-prose my-8 overflow-x-auto rounded-xl ring-1 ring-slate-200 dark:ring-slate-700"
+ table {
+ _class "min-w-full divide-y divide-slate-200 text-sm dark:divide-slate-700"
+ thead {
+ _class "bg-slate-50 dark:bg-slate-800/60"
+ tr {
+ for index, header in List.indexed headers do
+ th {
+ _scope "col"
+ _class [
+ "whitespace-nowrap px-4 py-3 font-semibold text-slate-900 dark:text-white"
+ if index = 0 then "text-left" else "text-right"
+ ]
+ header
+ }
+ }
+ }
+ tbody {
+ _class "divide-y divide-slate-100 bg-white dark:divide-slate-800 dark:bg-slate-900"
+ for row in rows do
+ tr {
+ _class "hover:bg-slate-50/70 dark:hover:bg-slate-800/40"
+ for index, value in List.indexed row do
+ td {
+ _class [
+ "whitespace-nowrap px-4 py-3"
+ if index = 0 then
+ "font-medium text-slate-700 dark:text-slate-200"
+ else
+ "text-right font-mono tabular-nums text-slate-600 dark:text-slate-300"
+ ]
+ value
+ }
+ }
+ }
+ }
+ }
+ | CodeBlock(language, source) ->
+ let prismLanguage = if language = "fs" then "fsharp" else language
+ pre {
+ _class $"language-{prismLanguage}"
+ code { _class $"language-{prismLanguage}"; source }
+ }
+
+ let document navigation page =
+ let siteUrl = "https://fsharpviewengine.meiermade.com"
+ let pageUrl = if page.path = "/" then siteUrl else siteUrl + page.path
+ let socialDescription = "A minimal, fast view engine for F#. Documentation and examples for FSharp.ViewEngine."
+ let socialImageUrl = siteUrl + "/android-chrome-512x512.png"
+
+ html {
+ _lang "en"
+ _class "h-full antialiased"
+ head {
+ meta { _charset "utf-8" }
+ meta { _name "viewport"; _content "width=device-width, initial-scale=1" }
+ title page.browserTitle
+ link { _rel "canonical"; _href pageUrl }
+ meta { _name "description"; _content socialDescription }
+ meta { _property "og:type"; _content "website" }
+ meta { _property "og:site_name"; _content "FSharp.ViewEngine" }
+ meta { _property "og:title"; _content page.browserTitle }
+ meta { _property "og:description"; _content socialDescription }
+ meta { _property "og:url"; _content pageUrl }
+ meta { _property "og:image"; _content socialImageUrl }
+ meta { _property "og:image:alt"; _content "FSharp.ViewEngine logo" }
+ meta { _name "twitter:card"; _content "summary_large_image" }
+ meta { _name "twitter:title"; _content page.browserTitle }
+ meta { _name "twitter:description"; _content socialDescription }
+ meta { _name "twitter:image"; _content socialImageUrl }
+ meta { _name "twitter:image:alt"; _content "FSharp.ViewEngine logo" }
+ script { js "let t=localStorage.getItem('theme');if(t==='dark'||(!t||t==='system')&&window.matchMedia('(prefers-color-scheme: dark)').matches){document.documentElement.classList.add('dark')}" }
+ link { _rel "stylesheet"; _href "/css/output.css" }
+ script { _type "module"; _src "/scripts/datastar.1.0.2.js" }
+ script { _src "https://cdnjs.cloudflare.com/ajax/libs/prism/1.30.0/prism.min.js" }
+ link { _rel "stylesheet"; _href "https://cdnjs.cloudflare.com/ajax/libs/prism/1.30.0/themes/prism-tomorrow.min.css" }
+ script { _src "https://cdnjs.cloudflare.com/ajax/libs/prism/1.30.0/components/prism-fsharp.min.js" }
+ }
+ body {
+ _class "min-h-full bg-white dark:bg-slate-900"
+ _dataSignals "{mobileNavOpen: false, themeMenuOpen: false, theme: localStorage.getItem('theme') || 'system'}"
+ _dataEffect "localStorage.setItem('theme', $theme); document.documentElement.classList.toggle('dark', $theme === 'dark' || ($theme === 'system' && window.matchMedia('(prefers-color-scheme: dark)').matches))"
+ _dataOn ("keydown", [ "window" ], "evt.key === 'Escape' && ($mobileNavOpen = false, $themeMenuOpen = false)")
+ pageHeader
+ div {
+ _dataShow "$mobileNavOpen"
+ _style "display: none"
+ _class "fixed inset-0 z-[70] lg:hidden"
+ div {
+ _id "mobile-navigation-backdrop"
+ _class "absolute inset-0 bg-slate-950/60 backdrop-blur-sm"
+ _dataOn ("click", "$mobileNavOpen = false")
+ }
+ div {
+ _class "absolute inset-y-0 left-0 w-full max-w-xs overflow-y-auto bg-white px-6 py-5 shadow-2xl ring-1 ring-slate-900/10 dark:bg-slate-900 dark:ring-white/10"
+ div {
+ _class "mb-6 flex items-center justify-between"
+ a {
+ _href "/"
+ _class "flex items-center gap-2 text-sm font-semibold tracking-wider text-slate-700 dark:text-white"
+ _dataOn ("click", "$mobileNavOpen = false")
+ img { _src "/logo.svg"; _alt "FSharp.ViewEngine"; _class "h-6 w-6" }
+ "FSharp.ViewEngine"
+ }
+ button {
+ _type "button"
+ _ariaLabel "Close navigation"
+ _class "cursor-pointer rounded p-1 text-slate-500 hover:bg-slate-100 dark:text-slate-400 dark:hover:bg-slate-700"
+ _dataOn ("click", "$mobileNavOpen = false")
+ xMarkIcon
+ }
+ }
+ sidebarNavigation navigation page.path
+ }
+ }
+ div {
+ _id "app"
+ _class "relative mx-auto flex max-w-8xl justify-center sm:px-2 lg:px-8 xl:px-12"
+ sidebar navigation page.path
+ div {
+ _class "min-w-0 max-w-3xl flex-auto px-4 pt-6 pb-12 lg:max-w-none lg:pl-8 lg:pr-0 xl:px-16"
+ article {
+ div {
+ _class "mb-8"
+ p {
+ _class "font-display text-sm font-medium text-sky-500"
+ page.category
+ }
+ }
+ div {
+ _class "prose prose-slate max-w-none dark:prose-invert [&_h1]:scroll-mt-28 [&_h2]:scroll-mt-28 [&_h3]:scroll-mt-28"
+ h1 { page.title }
+ for node in page.nodes do
+ renderNode node
+ }
+ }
+ }
+ div {
+ _class "hidden xl:sticky xl:top-[4.75rem] xl:-mr-6 xl:block xl:h-[calc(100vh-4.75rem)] xl:flex-none xl:overflow-y-auto xl:py-12 xl:pr-6"
+ tableOfContents page
+ }
+ }
+ }
+ }
diff --git a/sln/src/Docs/src/Pages/Alpine.fs b/sln/src/Docs/src/Pages/Alpine.fs
new file mode 100644
index 0000000..3ac47ba
--- /dev/null
+++ b/sln/src/Docs/src/Pages/Alpine.fs
@@ -0,0 +1,159 @@
+namespace Docs.Pages
+
+open Docs.Common
+
+module Alpine =
+ let private section id title =
+ [ Heading { id = id; title = title; level = 2 } ]
+
+ let private directive id description source =
+ [ Heading { id = id; title = id; level = 3 };
+ Paragraph [ Text description ];
+ CodeBlock("fsharp", source) ]
+
+ let private nodes =
+ [ [ Paragraph [ Text "FSharp.ViewEngine covers all 18 core directives in "; Link("Alpine.js 3.15.12", "https://github.com/alpinejs/alpine/releases/tag/v3.15.12"); Text " and provides dedicated helpers for official plugins that expose HTML directives." ] ];
+ section "setup" "Setup";
+ [ Paragraph [ Text "Open the "; InlineContent.Code "Alpine"; Text " type to access Alpine directives:" ];
+ CodeBlock("fsharp", """open FSharp.ViewEngine
+open type Html
+open type Alpine""");
+ Paragraph [ Text "Modifier overloads accept ordered strings without leading periods. Directive arguments, such as event names and transition phases, are separate parameters:" ];
+ CodeBlock("fsharp", """button {
+ _xOn ("keydown", [ "enter"; "prevent"; "once" ], "save()")
+}
+div { _xTransition ("enter-start", "opacity-0 scale-90") }""");
+ Paragraph [ Text "Directive arguments and modifier strings become part of the HTML attribute name. Treat them as trusted application tokens; FSharp.ViewEngine does not validate or encode attribute names." ] ];
+
+ section "core-directives" "Core Directives";
+ directive "x-data" "Initialize a component's reactive state with _xData." """div {
+ _xData "{ open: false, count: 0 }"
+ button { _xOn ("click", "count++"); "Increment" }
+ span { _xText "count" }
+}""";
+ directive "x-init" "Run an expression when an element is initialized with _xInit." """div {
+ _xData "{ users: [] }"
+ _xInit "users = await (await fetch('/api/users')).json()"
+}""";
+ directive "x-show" "Toggle an element's display with _xShow. The important modifier applies display: none !important." """div { _xShow ([ "important" ], "open"); "Content" }""";
+ directive "x-bind" "Bind an HTML attribute or property with _xBind. Use it for keyed x-for iterations instead of a plain by attribute." """template {
+ _xFor "item in items"
+ _xBind ("key", "item.id")
+ li { _xText "item.label" }
+}""";
+ directive "x-on" "Handle browser events with _xOn. Modifiers are emitted in their supplied order." """button {
+ _xOn ("click", [ "prevent"; "once" ], "save()")
+ "Save"
+}""";
+ directive "x-text" "Set textContent from an expression with _xText." """span { _xText "message" }""";
+ directive "x-html" "Set innerHTML from an expression with _xHtml." """div { _xHtml "trustedHtml" }""";
+ [ Paragraph [ Strong [ Text "Security:" ]; Text " Alpine inserts "; InlineContent.Code "x-html"; Text " content as HTML. Only use HTML created by trusted application code; never pass unsanitized user content." ] ];
+ directive "x-model" "Create two-way form bindings with _xModel. Modifiers use ordered strings." """input {
+ _type "search"
+ _xModel ([ "lazy"; "debounce.500ms" ], "query")
+}""";
+ directive "x-modelable" "Expose a component property to an outer x-model binding with _xModelable." """div {
+ _xData "{ value: 0 }"
+ _xModelable "value"
+}""";
+ directive "x-for" "Repeat a template with _xFor. Alpine requires x-for on a template with one root child." """template {
+ _xFor "item in items"
+ _xBind ("key", "item.id")
+ li { _xText "item.label" }
+}""";
+ directive "x-transition" "Apply Alpine's transition helper or explicit phase classes with _xTransition." """div {
+ _xShow "open"
+ _xTransition [ "duration.500ms"; "opacity" ]
+}
+div { _xTransition ("leave-end", "opacity-0 scale-90") }""";
+ directive "x-effect" "Re-run an expression whenever its reactive dependencies change with _xEffect." """div { _xEffect "console.log(count)" }""";
+ directive "x-ignore" "Prevent Alpine from initializing an element tree with _xIgnore. Use self to ignore only the element." """div { _xIgnore () }
+div { _xIgnore [ "self" ] }""";
+ directive "x-ref" "Name an element for access through $refs with _xRef." """input { _xRef "searchInput" }
+button { _xOn ("click", "$refs.searchInput.focus()"); "Focus" }""";
+ directive "x-cloak" "Hide an element until Alpine initializes with the presence-only _xCloak directive." """div { _xCloak; "Hidden until Alpine loads" }""";
+ directive "x-teleport" "Move a template to the first element matching a CSS selector with _xTeleport." """template {
+ _xTeleport "body"
+ div { "Modal" }
+}""";
+ directive "x-if" "Conditionally add or remove a template's child from the DOM with _xIf." """template {
+ _xIf "open"
+ div { "Visible while open" }
+}""";
+ directive "x-id" "Create a scoped set of generated IDs with _xId." """div { _xId "['dropdown']" }""";
+
+ section "plugin-directives" "Plugin Directives";
+ [ Paragraph [ Text "Plugin helpers only render attributes; applications must install and register the corresponding Alpine package before Alpine starts. See the "; Link("official plugin documentation", "https://alpinejs.dev/plugins/"); Text " for script and module setup." ] ];
+ directive "x-mask" "The Mask plugin formats input as the user types. Use _xMask for a fixed mask or _xMaskDynamic for an expression." """input { _xMask "99/99/9999" }
+input { _xMaskDynamic "$money($input)" }""";
+ directive "x-intersect" "The Intersect plugin runs an expression when an element enters or leaves the viewport." """div {
+ _xIntersect ([ "once"; "threshold.50" ], "visible = true")
+}
+div { _xIntersect ("leave", [ "full" ], "visible = false") }""";
+ directive "x-resize" "The Resize plugin runs an expression when an element or document changes size." """div { _xResize "width = $width" }
+div { _xResize ([ "document" ], "viewportWidth = $width") }""";
+ directive "x-collapse" "The Collapse plugin animates an x-show element's height." """div {
+ _xShow "open"
+ _xCollapse [ "duration.500ms"; "min.50px" ]
+}""";
+ directive "x-trap" "The _xTrap helper requires Alpine's Focus plugin. Focus modifiers include inert, noscroll, noreturn, and noautofocus." """div {
+ _xShow "open"
+ _xTrap ([ "inert"; "noscroll" ], "open")
+}""";
+ [ Paragraph [ Strong [ Text "Dependency:" ]; Text " "; InlineContent.Code "x-trap"; Text " is provided by the "; Link("Focus plugin", "https://alpinejs.dev/plugins/focus"); Text ", not Alpine core." ] ];
+ directive "x-anchor" "The _xAnchor helper positions an element relative to a reference and requires Alpine's Anchor plugin." """button { _xRef "trigger"; "Open" }
+div {
+ _xAnchor ([ "bottom-start"; "offset.10"; "fixed" ], "$refs.trigger")
+}""";
+ [ Paragraph [ Strong [ Text "Dependency:" ]; Text " "; InlineContent.Code "x-anchor"; Text " is provided by the "; Link("Anchor plugin", "https://alpinejs.dev/plugins/anchor"); Text ", not Alpine core." ] ];
+ directive "x-sort" "The Sort plugin provides sortable containers, items, groups, configuration, handles, and ignored controls." """ul {
+ _xSort ([ "ghost" ], "handleSort($item, $position)")
+ _xSortGroup "tasks"
+ _xSortConfig "{ animation: 150 }"
+
+ li {
+ _xSortItem "task.id"
+ button { _xSortHandle; "Drag" }
+ button { _xSortIgnore; "Edit" }
+ }
+}""";
+
+ section "plugins-without-directives" "Plugins Without Directive Helpers";
+ [ Paragraph [ Text "The Persist plugin exposes the "; InlineContent.Code "$persist"; Text " magic rather than an HTML directive, so no dedicated attribute helper is provided:" ];
+ CodeBlock("fsharp", """div { _xData "{ count: $persist(0) }" }""");
+ Paragraph [ Text "The Morph plugin exposes the imperative "; InlineContent.Code "Alpine.morph"; Text " API rather than an HTML directive, so it also has no dedicated attribute helper." ];
+ Paragraph [ Text "Use the generic "; InlineContent.Code "_x"; Text " helper for third-party or future directives that are not yet represented:" ];
+ CodeBlock("fsharp", """div { _x ("third-party", "expression") }""") ];
+
+ section "trusted-expressions" "Trusted Expressions";
+ [ Paragraph [ Text "Alpine directive values execute JavaScript expressions. FSharp.ViewEngine HTML-encodes attribute values, but encoding does not make untrusted expressions safe. Build expressions from trusted application code and do not interpolate user input into them." ] ];
+
+ section "complete-example" "Complete Example";
+ [ Paragraph [ Text "A core-only disclosure component with keyboard handling and transitions:" ];
+ CodeBlock("fsharp", """div {
+ _xData "{ open: false }"
+
+ button {
+ _xOn ("click", "open = !open")
+ _xOn ("keydown", [ "escape"; "prevent" ], "open = false")
+ _xBind ("aria-expanded", "open")
+ "Toggle details"
+ }
+
+ div {
+ _xShow "open"
+ _xTransition [ "duration.200ms"; "opacity" ]
+ "Details"
+ }
+}""") ] ]
+ |> List.concat
+
+ let page =
+ { id = "alpine"
+ path = "/extensions/alpine"
+ aliases = []
+ navLabel = "Alpine"
+ category = "Extensions"
+ title = "Alpine.js"
+ browserTitle = "Alpine.js - FSharp.ViewEngine"
+ nodes = nodes }
diff --git a/sln/src/Docs/src/Pages/Benchmarks.fs b/sln/src/Docs/src/Pages/Benchmarks.fs
new file mode 100644
index 0000000..a1432e7
--- /dev/null
+++ b/sln/src/Docs/src/Pages/Benchmarks.fs
@@ -0,0 +1,122 @@
+namespace Docs.Pages
+
+open Docs.Common
+
+module Benchmarks =
+ let page =
+ { id = "benchmarks"
+ path = "/benchmarks"
+ aliases = []
+ navLabel = "Benchmarks"
+ category = "Project"
+ title = "Benchmarks"
+ browserTitle = "Benchmarks - FSharp.ViewEngine"
+ nodes = [
+ Paragraph [ Text "These benchmarks show typical render times, compare FSharp.ViewEngine with other F# view engines, and provide the commands needed to reproduce the results." ]
+ Heading { id = "typical-render-times"; title = "Typical Render Times"; level = 2 }
+ Paragraph [ Text "The representative page is a complete HTML document with metadata, navigation, content, lists, a form, a table, and a footer." ]
+ UnorderedList [
+ [ Strong [ Text "Build and render:" ]; Text " 1.585 μs for the representative page." ]
+ [ Strong [ Text "Render only:" ]; Text " 833.5 ns when the representative page is already built." ]
+ [ Strong [ Text "Large response:" ]; Text " 0.23 ms to build and render 1,000 repeated articles." ]
+ ]
+ Paragraph [ Text "These are renderer operations, not HTTP requests per second. Routing, application work, I/O, concurrency, and response transport are not included." ]
+ Heading { id = "framework-comparison"; title = "How It Compares"; level = 2 }
+ Paragraph [ Text "For the representative build-and-render workload, the other engines take 1.35× to 2.35× as long as FSharp.ViewEngine. All four remain fast in absolute terms." ]
+ BarChart {
+ label = "Build and render comparison"
+ title = "Typical dynamic page: build and render"
+ description = "Mean duration per operation. Shorter bars are faster; values are normalized against the slowest result."
+ bars = [
+ { label = "FSharp.ViewEngine"; duration = "1.585 μs"; comparison = "baseline"; widthPercent = 43; highlighted = true }
+ { label = "Oxpecker.ViewEngine"; duration = "2.147 μs"; comparison = "1.35× as long"; widthPercent = 58; highlighted = false }
+ { label = "Giraffe.ViewEngine"; duration = "2.649 μs"; comparison = "1.67× as long"; widthPercent = 71; highlighted = false }
+ { label = "Feliz.ViewEngine"; duration = "3.723 μs"; comparison = "2.35× as long"; widthPercent = 100; highlighted = false }
+ ]
+ }
+ Heading { id = "methodology"; title = "How the Benchmarks Were Run"; level = 2 }
+ Paragraph [ Text "The current results were measured on August 6, 2026 with BenchmarkDotNet 0.15.8, .NET SDK 10.0.201, .NET runtime 10.0.5, macOS 26.4.1, and an Apple M5 Max Arm64 processor." ]
+ UnorderedList [
+ [ Strong [ Text "Process isolation" ]; Text " — every benchmark executes in a generated benchmark process rather than inside the runner." ]
+ [ Strong [ Text "Repeated measurement" ]; Text " — MediumRun uses two launches, ten warmups, and fifteen measured iterations." ]
+ [ Strong [ Text "Bounded iteration time" ]; Text " — a 100 ms target avoids multi-gigabyte allocation pressure in the fastest render-only cases." ]
+ [ Strong [ Text "Render-only setup" ]; Text " — documents are constructed once before warmup and measurement, keeping construction work out of the render loop." ]
+ [ Strong [ Text "Reported values" ]; Text " — tables show the arithmetic mean and managed allocation per operation; lower is better." ]
+ ]
+ Paragraph [ Text "The comparison suite uses the latest stable releases checked for this measurement: "; Link("Oxpecker.ViewEngine 2.0.1", "https://www.nuget.org/packages/Oxpecker.ViewEngine/2.0.1"); Text ", "; Link("Giraffe.ViewEngine 1.4.0", "https://www.nuget.org/packages/Giraffe.ViewEngine/1.4.0"); Text ", and "; Link("Feliz.ViewEngine 1.0.3", "https://www.nuget.org/packages/Feliz.ViewEngine/1.0.3"); Text ". The runner prints resolved package versions with every execution. Results are representative measurements, not CI regression thresholds." ]
+ Heading { id = "running-benchmarks"; title = "How to Run the Benchmarks"; level = 2 }
+ Paragraph [ Text "Run commands from the solution directory. The FAKE targets forward trailing BenchmarkDotNet options, including filters. "; InlineContent.Code "BenchmarkSmoke"; Text " executes selected cases once in isolated processes to validate the suite without producing stable measurements." ]
+ CodeBlock("shell", """cd sln
+
+# Run the complete measurement suite.
+./fake.sh Benchmark
+
+# List or target benchmark cases.
+./fake.sh Benchmark --list flat
+./fake.sh Benchmark --filter '*Benchmarks.RenderOnly.*'
+
+# Validate every case, or a filtered subset, once.
+./fake.sh BenchmarkSmoke
+./fake.sh BenchmarkSmoke --filter '*AttributeEncodingBenchmarks*'""")
+ Heading { id = "appendix"; title = "Appendix: Detailed Results"; level = 2 }
+ Paragraph [ Text "The following tables preserve the raw mean and managed-allocation results behind the analysis above." ]
+ Heading { id = "appendix-build-and-render"; title = "Comparison: Build and Render"; level = 3 }
+ DataTable(
+ [ "Method"; "Mean"; "Allocated" ],
+ [ [ "FSharp.ViewEngine"; "1.585 μs"; "11.39 KB" ]
+ [ "Oxpecker.ViewEngine"; "2.147 μs"; "12.88 KB" ]
+ [ "Giraffe.ViewEngine"; "2.649 μs"; "23.94 KB" ]
+ [ "Feliz.ViewEngine"; "3.723 μs"; "25.87 KB" ] ])
+ Heading { id = "appendix-render-only"; title = "Comparison: Render Only"; level = 3 }
+ DataTable(
+ [ "Method"; "Mean"; "Allocated" ],
+ [ [ "FSharp.ViewEngine"; "833.5 ns"; "2.93 KB" ]
+ [ "Oxpecker.ViewEngine"; "911.4 ns"; "2.93 KB" ]
+ [ "Giraffe.ViewEngine"; "989.6 ns"; "12.77 KB" ]
+ [ "Feliz.ViewEngine"; "1,872.9 ns"; "14.2 KB" ] ])
+ Heading { id = "appendix-build-only"; title = "Comparison: Build Only"; level = 3 }
+ DataTable(
+ [ "Method"; "Mean"; "Allocated" ],
+ [ [ "FSharp.ViewEngine"; "670.1 ns"; "8.46 KB" ]
+ [ "Oxpecker.ViewEngine"; "1,181.0 ns"; "9.95 KB" ]
+ [ "Giraffe.ViewEngine"; "1,654.9 ns"; "11.17 KB" ]
+ [ "Feliz.ViewEngine"; "1,782.9 ns"; "11.66 KB" ] ])
+ Heading { id = "appendix-attribute-encoding"; title = "Attribute Encoding"; level = 3 }
+ DataTable(
+ [ "Value"; "Mean"; "Allocated" ],
+ [ [ "Plain"; "36.17 ns"; "280 B" ]
+ [ "Encoded"; "81.92 ns"; "496 B" ] ])
+ Heading { id = "appendix-storage-boundaries"; title = "Inline and Overflow Storage"; level = 3 }
+ DataTable(
+ [ "Shape"; "Count"; "Mean"; "Allocated" ],
+ [ [ "Attributes"; "0"; "26.43 ns"; "200 B" ]
+ [ "Attributes"; "1"; "33.16 ns"; "216 B" ]
+ [ "Attributes"; "2"; "41.23 ns"; "240 B" ]
+ [ "Attributes"; "8"; "108.42 ns"; "744 B" ]
+ [ "Children"; "0"; "18.47 ns"; "160 B" ]
+ [ "Children"; "1"; "35.08 ns"; "320 B" ]
+ [ "Children"; "2"; "52.22 ns"; "488 B" ]
+ [ "Children"; "8"; "187.57 ns"; "1,648 B" ] ])
+ Heading { id = "appendix-collection-inputs"; title = "Equivalent Collection Inputs"; level = 3 }
+ DataTable(
+ [ "Collection"; "Mean"; "Allocated" ],
+ [ [ "Array"; "451.7 ns"; "3.45 KB" ]
+ [ "List"; "437.7 ns"; "3.45 KB" ]
+ [ "Sequence"; "482.8 ns"; "3.53 KB" ] ])
+ Heading { id = "appendix-document-workloads"; title = "Document Workloads"; level = 3 }
+ DataTable(
+ [ "Workload"; "Build + render"; "Allocation"; "Render only"; "Allocation" ],
+ [ [ "Small fragment"; "72.92 ns"; "680 B"; "51.05 ns"; "296 B" ]
+ [ "Representative page"; "1,538.00 ns"; "11,664 B"; "813.40 ns"; "3,000 B" ]
+ [ "Deeply nested"; "2,288.68 ns"; "12,096 B"; "1,069.54 ns"; "3,256 B" ]
+ [ "Large response"; "228,746.00 ns"; "1,252,539 B"; "77,196.10 ns"; "283,768 B" ] ])
+ Heading { id = "appendix-profiling"; title = "Profiling Findings"; level = 3 }
+ UnorderedList [
+ [ Text "Build-only CPU samples are dominated by "; InlineContent.Code "TagBuilder.Run"; Text " and generated computation-expression methods; sampled allocations are DOM nodes and overflow collections rather than closure objects." ]
+ [ Text "Render-only allocations are almost entirely the required returned "; InlineContent.Code "System.String"; Text "." ]
+ [ Text "Optimized ARM64 JIT output retains indirect child-render calls, but profiling does not identify dispatch as a dominant cost." ]
+ [ Text "General sequence input adds about 80 bytes and modest runtime overhead, which does not justify collection-specific overloads." ]
+ [ Text "Measurements continue to support inline storage for zero, one, and two attributes or children." ]
+ [ Text "The renderer retains at most one thread-static string builder no larger than 256K characters, bounding retained memory without regressing the representative large response." ]
+ ]
+ ] }
diff --git a/sln/src/Docs/src/Pages/Changelog.fs b/sln/src/Docs/src/Pages/Changelog.fs
new file mode 100644
index 0000000..6b7026b
--- /dev/null
+++ b/sln/src/Docs/src/Pages/Changelog.fs
@@ -0,0 +1,133 @@
+namespace Docs.Pages
+
+open Docs.Common
+
+module Changelog =
+ let page =
+ { id = "changelog"
+ path = "/changelog"
+ aliases = []
+ navLabel = "Changelog"
+ category = "Project"
+ title = "Changelog"
+ browserTitle = "Changelog - FSharp.ViewEngine"
+ nodes = [
+ Paragraph [ Text "Notable changes to FSharp.ViewEngine are recorded here. The structure follows "; Link("Keep a Changelog", "https://keepachangelog.com/en/2.0.0/"); Text ". Releases use the project's existing calendar-oriented version scheme, and incompatible API changes are marked explicitly." ];
+ Heading { id = "unreleased"; title = "Unreleased"; level = 2 };
+ Heading { id = "added"; title = "Added"; level = 3 };
+ UnorderedList [
+ [ Text "Complete WAI-ARIA 1.2 attribute coverage and a generic "; InlineContent.Code "_aria"; Text " escape hatch." ];
+ [ Text "Pinned WHATWG inventory checks, missing HTML elements and attributes, "; InlineContent.Code "titleBuilder"; Text ", and attribute-capable void element builders." ];
+ [ Text "Complete non-deprecated HTMX 2.0.9 attribute coverage." ];
+ [ Text "Complete Datastar 1.0.2 attribute coverage, including "; InlineContent.Code "_dataMatchMedia"; Text ", modifier overloads, and "; InlineContent.Code "_dataPersistFilter"; Text "." ];
+ [ Text "Complete Alpine.js 3.15.12 core directive coverage and dedicated helpers for official directive-based plugins." ];
+ [ Text "Complete Tailwind Plus Elements 1.0.22 coverage for all 23 custom elements and supported writable attributes." ];
+ [ Text "A documented SVG 2 production subset with 21 common elements, geometry and presentation attributes, invariant numeric values, modern linking, and accessibility guidance." ];
+ [ Text "Playwright end-to-end coverage for the production Docker Compose Docs image and deployed public site." ];
+ [ Text "NuGet package validation that verifies the single compatibility asset, checks the public API against the 2026.2.5 baseline, and executes consumers on each supported runtime." ];
+ [ Text "Portable PDB symbol packages with Source Link metadata for GitHub-hosted source debugging." ];
+ [ Text "Typed F# documentation pages, registry-driven routes and navigation, direct-render tests, an analysis-first benchmark page with an accessible comparison visual and detailed appendix, and this changelog." ];
+ [ Text "Opt-in "; InlineContent.Code "Benchmark"; Text " and "; InlineContent.Code "BenchmarkSmoke"; Text " FAKE targets with forwarded BenchmarkDotNet arguments." ]
+ ];
+ Heading { id = "changed"; title = "Changed"; level = 3 };
+ UnorderedList [
+ [ Strong [ Text "Breaking:" ]; Text " Attribute values are always HTML-encoded. Output that previously relied on pre-encoded values must now use the original unencoded value or an explicitly trusted raw boundary." ];
+ [ Text "Numeric HTML attribute values now use invariant-culture formatting." ];
+ [ Text "Documentation content is rendered directly from typed FSharp.ViewEngine nodes instead of runtime Markdown conversion." ];
+ [ Text "The Docs application and primary examples now use pinned, self-hosted Datastar 1.0.2 instead of Alpine.js, while retaining Alpine and HTMX as documented library integrations." ];
+ [ Strong [ Text "Breaking:" ]; Text " Datastar modifier-capable presence helpers "; InlineContent.Code "_dataIgnore"; Text " and "; InlineContent.Code "_dataScrollIntoView"; Text " now use unit for their unmodified forms." ];
+ [ Strong [ Text "Breaking:" ]; Text " Alpine modifier overloads now accept ordered string lists without leading periods; transition phase arguments now precede their values." ];
+ [ Strong [ Text "Breaking:" ]; Text " Renamed the Tailwind Plus Elements API from "; InlineContent.Code "Tailwind"; Text " to "; InlineContent.Code "TailwindElements"; Text " without a compatibility alias, and moved its documentation to "; InlineContent.Code "/extensions/tailwind-elements"; Text "." ];
+ [ Text "The NuGet package now ships one "; InlineContent.Code "net8.0"; Text " compatibility asset while testing .NET 8, .NET 9, and .NET 10 runtimes independently." ];
+ [ Text "Release execution now generates the next monthly calendar version and tag, verifies one package artifact, deploys and tests the matching Docs version and commit, publishes that exact artifact, waits for NuGet availability, and creates the GitHub Release." ];
+ [ Text "Preview CI now requires full solution builds, unit and Docs tests, package consumer validation, and Docker-based browser tests." ];
+ [ Text "Updated BenchmarkDotNet to 0.15.8, removed redundant .NET 10 benchmark dependency references, corrected render-only profiling setup, restored process-isolated execution, expanded representative workload coverage, standardized a practical 100 ms measurement target, refreshed published results, and bounded per-thread renderer buffer retention." ];
+ [ Text "Updated Expecto, Giraffe, JetBrains.Annotations, Oxpecker.ViewEngine, Serilog, Prism, cloudflared, and the Pulumi SDK/providers; refreshed compatible infrastructure locks to zero npm audit vulnerabilities." ];
+ [ Text "Updated the Docker Tailwind CSS CLI baseline to 4.3.3." ]
+ ];
+ Heading { id = "removed"; title = "Removed"; level = 3 };
+ UnorderedList [
+ [ Strong [ Text "Breaking:" ]; Text " Removed the unsupported "; InlineContent.Code "_dataAnimate expression"; Text " overload. Datastar 1.0.2 requires a keyed attribute name." ];
+ [ Strong [ Text "Breaking:" ]; Text " Removed key-plus-value overloads for "; InlineContent.Code "_dataBind"; Text ", "; InlineContent.Code "_dataIndicator"; Text ", and "; InlineContent.Code "_dataRef"; Text ". Datastar requires these attributes to use either a key or a value, never both." ];
+ [ Strong [ Text "Breaking:" ]; Text " Removed "; InlineContent.Code "_dataRocket"; Text "; "; InlineContent.Code "data-rocket"; Text " is not part of Datastar 1.x." ];
+ [ Strong [ Text "Breaking:" ]; Text " Removed Alpine's unrelated "; InlineContent.Code "_by"; Text " helper and the no-expression "; InlineContent.Code "_xOn event"; Text " overload." ];
+ [ Text "Removed Markdig and copied Markdown content from the Docs application." ]
+ ];
+ Heading { id = "deprecated"; title = "Deprecated"; level = 3 };
+ UnorderedList [
+ [ InlineContent.Code "Html.portal"; Text ", because "; InlineContent.Code "portal"; Text " is not a standard HTML element." ];
+ [ InlineContent.Code "_ariaDropeffect"; Text " and "; InlineContent.Code "_ariaGrabbed"; Text ", which are deprecated in WAI-ARIA 1.2." ]
+ ];
+ Heading { id = "datastar-migration"; title = "Datastar Migration"; level = 2 };
+ Paragraph [ Text "Update Datastar call sites as follows." ];
+ Heading { id = "key-data-animate"; title = "Key data-animate"; level = 3 };
+ CodeBlock("fsharp", """// Before
+_dataAnimate "$visible ? 1 : 0"
+
+// After
+_dataAnimate ("opacity", "$visible ? 1 : 0")""");
+ Heading { id = "initialize-before-binding"; title = "Initialize Before Binding"; level = 3 };
+ CodeBlock("fsharp", """// Before: the keyed value was not valid Datastar syntax
+input { _dataBind ("name", "'default'") }
+
+// After
+_dataSignals ("name", "'default'")
+input { _dataBind "name" }""");
+ Heading { id = "remove-keyed-values"; title = "Remove Keyed Values"; level = 3 };
+ CodeBlock("fsharp", """// Before
+_dataIndicator ("loading", "'true'")
+_dataRef ("input", "'fallback'")
+
+// After
+_dataIndicator "loading"
+_dataRef "input"
+""");
+ Heading { id = "call-modifier-capable-presence-helpers"; title = "Call Modifier-Capable Presence Helpers"; level = 3 };
+ CodeBlock("fsharp", """// Before
+_dataIgnore
+_dataScrollIntoView
+
+// After
+_dataIgnore ()
+_dataScrollIntoView ()
+
+// With modifiers
+_dataIgnore [ "self" ]
+_dataScrollIntoView [ "smooth"; "vcenter"; "focus" ]""");
+ Heading { id = "replace-data-rocket"; title = "Replace data-rocket"; level = 3 };
+ Paragraph [ Text "Remove "; InlineContent.Code "_dataRocket"; Text " when targeting Datastar 1.x. If intentionally rendering markup for an older Datastar release, use the generic trusted attribute escape hatch:" ];
+ CodeBlock("fsharp", """_attr ("data-rocket", legacyExpression)""");
+ Heading { id = "alpine-migration"; title = "Alpine Migration"; level = 2 };
+ Paragraph [ Text "Update Alpine modifier and presence call sites as follows." ];
+ Heading { id = "use-alpine-modifier-lists"; title = "Use Modifier Lists"; level = 3 };
+ CodeBlock("fsharp", """// Before
+_xModel ("name", ".lazy")
+_xTrap ("open", ".noscroll")
+_xAnchor ("$refs.trigger", ".bottom")
+
+// After
+_xModel ([ "lazy" ], "name")
+_xTrap ([ "noscroll" ], "open")
+_xAnchor ([ "bottom" ], "$refs.trigger")""");
+ Heading { id = "name-transition-phases-first"; title = "Name Transition Phases First"; level = 3 };
+ CodeBlock("fsharp", """// Before
+_xTransition ("opacity-0", ":enter-start")
+
+// After
+_xTransition ("enter-start", "opacity-0")""");
+ Heading { id = "call-x-ignore"; title = "Call x-ignore"; level = 3 };
+ CodeBlock("fsharp", """// New core helper
+_xIgnore ()
+_xIgnore [ "self" ]""");
+ Heading { id = "replace-by"; title = "Replace by"; level = 3 };
+ Paragraph [ Text "The removed "; InlineContent.Code "_by"; Text " helper rendered a plain attribute that Alpine does not define. For keyed "; InlineContent.Code "x-for"; Text " templates, bind the key explicitly:" ];
+ CodeBlock("fsharp", """_xBind ("key", "item.id")""");
+ Heading { id = "tailwind-plus-elements-migration"; title = "Tailwind Plus Elements Migration"; level = 2 };
+ Paragraph [ Text "Open the renamed type at Tailwind Plus Elements call sites:" ];
+ CodeBlock("fsharp", """// Before
+open type Tailwind
+
+// After
+open type TailwindElements""");
+ Paragraph [ Text "No compatibility type or documentation redirect is provided. The canonical documentation route is now "; InlineContent.Code "/extensions/tailwind-elements"; Text "." ];
+ ] }
diff --git a/sln/src/Docs/src/Pages/Custom.fs b/sln/src/Docs/src/Pages/Custom.fs
new file mode 100644
index 0000000..72c77a9
--- /dev/null
+++ b/sln/src/Docs/src/Pages/Custom.fs
@@ -0,0 +1,165 @@
+namespace Docs.Pages
+
+open Docs.Common
+
+module Custom =
+ let page =
+ { id = "custom"
+ path = "/custom"
+ aliases = [ ]
+ navLabel = "Custom Elements & Attributes"
+ category = "Getting started"
+ title = "Custom Elements & Attributes"
+ browserTitle = "Custom Elements & Attributes - FSharp.ViewEngine"
+ nodes = [
+ Paragraph [ Text "FSharp.ViewEngine covers all standard HTML elements and attributes, but you may need custom ones for web components or non-standard attributes." ];
+ Heading { id = "trusted-content-boundaries"; title = "Trusted Content Boundaries"; level = 2 };
+ Paragraph [ Text "Text and attribute values are HTML-encoded by default. The following APIs intentionally cross that safety boundary and must only receive trusted, developer-controlled content:" ];
+ UnorderedList [
+ [ InlineContent.Code "Html.raw"; Text " and "; InlineContent.Code "Html.js"; Text " emit their values without encoding." ];
+ [ Text "Inline event-handler helpers such as "; InlineContent.Code "_onclick"; Text " contain executable JavaScript. HTML encoding preserves valid markup but does not make untrusted JavaScript safe." ];
+ [ InlineContent.Code "Html.el"; Text ", "; InlineContent.Code "Html.elVoid"; Text ", and the name passed to "; InlineContent.Code "_attr"; Text " are emitted as markup names without validation." ]
+ ];
+ Paragraph [ Text "Keep user-controlled data in normal text nodes and attribute "; Strong [ Text "values" ]; Text ", where it will be encoded. Do not use user input as raw markup, JavaScript, element names, or attribute names." ];
+ Heading { id = "custom-elements"; title = "Custom Elements"; level = 2 };
+ Heading { id = "el"; title = "el"; level = 3 };
+ Paragraph [ Text "Use "; InlineContent.Code "Html.el"; Text " to create a custom element with children. This is useful for web components:" ];
+ CodeBlock("fsharp", """open FSharp.ViewEngine
+open type Html
+
+el "my-component" {
+ _class "container"
+ p { "Hello from a web component!" }
+}""");
+ Paragraph [ Text "Renders:" ];
+ CodeBlock("html", """
+ Hello from a web component!
+""");
+ Heading { id = "elvoid"; title = "elVoid"; level = 3 };
+ Paragraph [ Text "Use "; InlineContent.Code "Html.elVoid"; Text " to create a custom self-closing (void) element:" ];
+ CodeBlock("fsharp", """elVoid "my-icon" {
+ _attr("name", "star")
+ _attr("size", "24")
+}""");
+ Paragraph [ Text "Renders:" ];
+ CodeBlock("html", """""");
+ Heading { id = "nested-web-components"; title = "Nested Web Components"; level = 3 };
+ Paragraph [ Text "Custom elements can be nested just like regular elements:" ];
+ CodeBlock("fsharp", """el "my-card" {
+ _attr("variant", "outlined")
+ el "my-card-header" {
+ h2 { "Card Title" }
+ }
+ el "my-card-body" {
+ p { "Card content goes here." }
+ }
+ el "my-card-footer" {
+ button { _onclick "handleClick()"; "Action" }
+ }
+}""");
+ Heading { id = "custom-attributes"; title = "Custom Attributes"; level = 2 };
+ Heading { id = "attr"; title = "_attr"; level = 3 };
+ Paragraph [ Text "Use "; InlineContent.Code "Html._attr"; Text " to add any attribute not covered by the built-in helpers." ];
+ Heading { id = "key-value-attribute"; title = "Key-value attribute"; level = 4 };
+ CodeBlock("fsharp", """div {
+ _attr("my-custom-attr", "value")
+ "Content"
+}""");
+ Paragraph [ Text "Renders:" ];
+ CodeBlock("html", """Content
""");
+ Heading { id = "boolean-attribute"; title = "Boolean attribute"; level = 4 };
+ Paragraph [ Text "Pass only the name to render a valueless (boolean) attribute:" ];
+ CodeBlock("fsharp", """div {
+ _attr "my-flag"
+ "Content"
+}""");
+ Paragraph [ Text "Renders:" ];
+ CodeBlock("html", """Content
""");
+ Heading { id = "combining-with-built-in-attributes"; title = "Combining with Built-in Attributes"; level = 3 };
+ Paragraph [ Text "Custom attributes work alongside all built-in attributes:" ];
+ CodeBlock("fsharp", """el "sl-button" {
+ _attr("variant", "primary")
+ _attr("size", "large")
+ _attr "pill"
+ _onclick "handleClick()"
+ _class "my-button"
+ "Click Me"
+}""");
+ Paragraph [ Text "Renders:" ];
+ CodeBlock("html", """
+ Click Me
+""");
+ Heading { id = "extending-the-html-type"; title = "Extending the Html Type"; level = 2 };
+ Paragraph [ Text "F# supports "; Link("type extensions", "https://learn.microsoft.com/en-us/dotnet/fsharp/language-reference/type-extensions"); Text " which let you add your own elements and attributes to the "; InlineContent.Code "Html"; Text " type. This is useful for project-specific conventions or design system components." ];
+ Heading { id = "adding-custom-elements"; title = "Adding Custom Elements"; level = 3 };
+ CodeBlock("fsharp", """open FSharp.ViewEngine
+
+type Html with
+ static member val myCard = TagBuilder("my-card") with get
+ static member val myIcon = VoidBuilder("my-icon") with get""");
+ Paragraph [ Text "Then use them just like built-in elements:" ];
+ CodeBlock("fsharp", """open type Html
+
+myCard {
+ _class "shadow-lg"
+ h2 { "Title" }
+ p { "Card content" }
+}
+
+myIcon { _attr("name", "star") }""");
+ Heading { id = "adding-custom-attributes"; title = "Adding Custom Attributes"; level = 3 };
+ CodeBlock("fsharp", """open FSharp.ViewEngine
+
+type Html with
+ static member inline _theme (v: string) = { Name = "data-theme"; Value = ValueSome v }
+ static member inline _variant (v: string) = { Name = "variant"; Value = ValueSome v }
+ static member inline _loading = { Name = "data-loading"; Value = ValueNone }""");
+ Paragraph [ Text "Then use them alongside built-in attributes:" ];
+ CodeBlock("fsharp", """open type Html
+
+div {
+ _theme "dark"
+ _variant "outlined"
+ _loading
+ "Content"
+}""");
+ Heading { id = "design-system-example"; title = "Design System Example"; level = 3 };
+ Paragraph [ Text "You can build a full design system module with reusable elements and attributes:" ];
+ CodeBlock("fsharp", """open FSharp.ViewEngine
+
+type Ds =
+ static member val alert = TagBuilder("ds-alert") with get
+ static member val badge = TagBuilder("ds-badge") with get
+ static member val tooltip = TagBuilder("ds-tooltip") with get
+ static member inline _severity (v: string) = { Name = "severity"; Value = ValueSome v }
+ static member inline _placement (v: string) = { Name = "placement"; Value = ValueSome v }
+ static member inline _dismissible = { Name = "dismissible"; Value = ValueNone }""");
+ CodeBlock("fsharp", """open type Html
+open type Ds
+
+alert {
+ _severity "warning"
+ _dismissible
+ "This is a warning message."
+}
+
+tooltip {
+ _placement "top"
+ button { "Hover me" }
+}""");
+ Heading { id = "shoelace-example"; title = "Shoelace Example"; level = 2 };
+ Paragraph [ Text "Here's a more complete example using "; Link("Shoelace", "https://shoelace.style/"); Text " web components:" ];
+ CodeBlock("fsharp", """el "sl-dialog" {
+ _attr("label", "Confirm")
+ _attr "open"
+ p { "Are you sure?" }
+ div {
+ _slot "footer"
+ el "sl-button" {
+ _attr("variant", "primary")
+ _onclick "this.closest('sl-dialog').hide()"
+ "Confirm"
+ }
+ }
+}""");
+ ] }
diff --git a/sln/src/Docs/src/Pages/Datastar.fs b/sln/src/Docs/src/Pages/Datastar.fs
new file mode 100644
index 0000000..826668b
--- /dev/null
+++ b/sln/src/Docs/src/Pages/Datastar.fs
@@ -0,0 +1,153 @@
+namespace Docs.Pages
+
+open Docs.Common
+
+module Datastar =
+ let private section id title =
+ [ Heading { id = id; title = title; level = 2 } ]
+
+ let private attribute id description source =
+ [ Heading { id = id; title = id; level = 3 };
+ Paragraph [ Text description ];
+ CodeBlock("fsharp", source) ]
+
+ let private nodes =
+ [ [ Paragraph [ Text "FSharp.ViewEngine covers all 31 attributes in the stable "; Link("Datastar 1.0.2 reference", "https://data-star.dev/reference/attributes"); Text " through the "; InlineContent.Code "Datastar"; Text " type." ] ];
+ section "setup" "Setup";
+ [ Paragraph [ Text "Open the "; InlineContent.Code "Datastar"; Text " type to access Datastar attributes:" ];
+ CodeBlock("fsharp", """open FSharp.ViewEngine
+open type Html
+open type Datastar""");
+ Paragraph [ Text "Modifier overloads accept an ordered list of modifier strings without the leading "; InlineContent.Code "__"; Text ". Modifier arguments follow the modifier name after a period:" ];
+ CodeBlock("fsharp", """input {
+ _dataBind ("query", [ "event.input" ])
+ _dataOn ("input", [ "debounce.200ms" ], "@get('/search')")
+}""");
+ Paragraph [ Text "Keys, event names, and modifier strings become part of the HTML attribute name. Treat them as trusted application tokens; FSharp.ViewEngine does not validate or encode attribute names." ] ];
+
+ section "core-attributes" "Core Attributes";
+ attribute "data-signals" "Create or patch signals with _dataSignals. Use object syntax for multiple signals, keyed syntax for one signal, and modifiers for casing or if-missing behavior." """div {
+ _dataSignals "{count: 0, name: 'World'}"
+ _dataSignals ("page", [ "ifmissing" ], "1")
+}""";
+ attribute "data-bind" "Create a two-way binding with _dataBind. Keyed bindings are presence-only; modifiers can select a property and synchronization events." """input {
+ _type "search"
+ _dataBind ("query", [ "prop.value"; "event.input.change" ])
+}""";
+ attribute "data-computed" "Create a read-only computed signal with _dataComputed." """div {
+ _dataComputed ("double", "$count * 2")
+ _dataComputed "{greeting: () => 'Hello, ' + $name}"
+}""";
+ attribute "data-effect" "Run a side-effect expression on initialization and whenever its signal dependencies change with _dataEffect." """div { _dataEffect "console.log($count)" }""";
+ attribute "data-show" "Show or hide an element based on an expression with _dataShow." """div {
+ _dataShow "$count > 0"
+ "Count is positive"
+}""";
+ attribute "data-text" "Set an element's text content reactively with _dataText." """span { _dataText "$count" }""";
+ attribute "data-attr" "Set one or more HTML attributes reactively with _dataAttr." """button {
+ _dataAttr ("disabled", "$loading")
+ _dataAttr "{'aria-busy': $loading}"
+}""";
+ attribute "data-class" "Toggle one or more classes with _dataClass. Keyed modifiers can override casing behavior." """div {
+ _dataClass ("font-bold", "$strong")
+ _dataClass ("my-class", [ "case.camel" ], "$active")
+}""";
+ attribute "data-style" "Set one or more inline style properties reactively with _dataStyle." """div {
+ _dataStyle ("background-color", "$error ? 'red' : 'green'")
+ _dataStyle "{display: $visible ? 'flex' : 'none'}"
+}""";
+ attribute "data-init" "Run an expression when an attribute is initialized with _dataInit. Delay and view-transition modifiers are supported." """div {
+ _dataInit ([ "delay.500ms"; "viewtransition" ], "$ready = true")
+}""";
+ attribute "data-on" "Handle DOM events with _dataOn. Modifier order is preserved in the generated attribute name." """button {
+ _dataOn ("click", [ "once"; "prevent" ], "$count++")
+ "Increment"
+}""";
+ attribute "data-on-intersect" "Run an expression when an element enters or exits the viewport with _dataOnIntersect." """div {
+ _dataOnIntersect ([ "once"; "threshold.25" ], "$visible = true")
+}""";
+ attribute "data-on-interval" "Run an expression on an interval with _dataOnInterval." """div {
+ _dataOnInterval ([ "duration.500ms.leading" ], "$count++")
+}""";
+ attribute "data-on-signal-patch" "Run an expression when signals are patched with _dataOnSignalPatch. Timing modifiers can delay, debounce, or throttle the listener." """div {
+ _dataOnSignalPatch ([ "debounce.500ms" ], "console.log(patch)")
+}""";
+ attribute "data-on-signal-patch-filter" "Filter signal patches using include and exclude regular expressions with _dataOnSignalPatchFilter." """div {
+ _dataOnSignalPatchFilter "{include: /^count$/, exclude: /temp$/}"
+}""";
+ attribute "data-indicator" "Create a signal that tracks in-flight fetch requests with the presence-only _dataIndicator helper." """button {
+ _dataIndicator "fetching"
+ _dataOn ("click", "@get('/endpoint')")
+ _dataAttr ("disabled", "$fetching")
+}""";
+ attribute "data-ref" "Create a signal containing a reference to an element with the presence-only _dataRef helper." """input { _dataRef "searchInput" }""";
+ attribute "data-json-signals" "Render signals as JSON for debugging with _dataJsonSignals. Filters and terse output are optional." """pre { _dataJsonSignals () }
+pre { _dataJsonSignals ([ "terse" ], "{include: /counter/}") }""";
+ attribute "data-ignore" "Prevent Datastar from processing an element tree with _dataIgnore. Use the self modifier to ignore only the element." """div { _dataIgnore () }
+div { _dataIgnore [ "self" ] }""";
+ attribute "data-ignore-morph" "Prevent an element and its children from being processed during morphing with _dataIgnoreMorph." """div { _dataIgnoreMorph }""";
+ attribute "data-preserve-attr" "Preserve one or more existing attribute values during morphing with _dataPreserveAttr." """details {
+ _open true
+ _dataPreserveAttr "open class"
+}""";
+
+ section "pro-attributes" "Pro Attributes";
+ [ Paragraph [ Text "Datastar Pro attributes require a "; Link("Datastar Pro license", "https://data-star.dev/pro"); Text ". They are included as convenience helpers but are not part of the free core bundle." ] ];
+ attribute "data-animate" "Animate a named element attribute reactively with the keyed _dataAnimate helper." """div {
+ _dataAnimate ("opacity", "$visible ? 1 : 0")
+}""";
+ attribute "data-custom-validity" "Set a form control's custom validation message with _dataCustomValidity." """input {
+ _dataBind "email"
+ _dataCustomValidity "$email.includes('@') ? '' : 'Enter a valid email'"
+}""";
+ attribute "data-match-media" "Keep a signal synchronized with a media query using _dataMatchMedia." """div {
+ _dataMatchMedia ("is-dark", "'prefers-color-scheme: dark'")
+ _dataComputed ("theme", "$isDark ? 'dark' : 'light'")
+}""";
+ attribute "data-on-raf" "Run an expression on every animation frame with _dataOnRaf. Throttle modifiers can limit updates." """canvas {
+ _dataOnRaf ([ "throttle.10ms" ], "draw()")
+}""";
+ attribute "data-on-resize" "Run an expression when an element's dimensions change with _dataOnResize." """div {
+ _dataOnResize ([ "debounce.10ms" ], "$width = el.offsetWidth")
+}""";
+ attribute "data-persist" "Persist signals to local or session storage with _dataPersist. Use _dataPersistFilter for a default-key filter object." """div { _dataPersist () }
+div { _dataPersist "settings" }
+div { _dataPersist ("settings", [ "session" ]) }
+div { _dataPersistFilter "{include: /theme/}" }""";
+ attribute "data-query-string" "Synchronize signals with query-string parameters using _dataQueryString." """div {
+ _dataQueryString ([ "filter"; "history" ], "{include: /search|page/}")
+}""";
+ attribute "data-replace-url" "Replace the browser URL without reloading using an evaluated expression passed to _dataReplaceUrl." """div { _dataReplaceUrl "`/page${$page}`" }""";
+ attribute "data-scroll-into-view" "Scroll an element into view with _dataScrollIntoView. Behavior, alignment, and focus are controlled by modifiers." """div { _dataScrollIntoView () }
+div { _dataScrollIntoView [ "smooth"; "vcenter"; "focus" ] }""";
+ attribute "data-view-transition" "Set an element's view-transition-name reactively with _dataViewTransition." """div { _dataViewTransition "$transitionName" }""";
+
+ section "trusted-expressions" "Trusted Expressions";
+ [ Paragraph [ Text "Datastar expressions can execute JavaScript and backend actions. Attribute values are HTML-encoded by FSharp.ViewEngine, but encoding does not make untrusted expressions safe. Build expressions from trusted application code and never interpolate untrusted input into them." ] ];
+
+ section "complete-example" "Complete Example";
+ [ Paragraph [ Text "An active search form using signals, binding, modifiers, an indicator, and a backend action:" ];
+ CodeBlock("fsharp", """div {
+ _dataSignals ("query", [ "ifmissing" ], "''")
+ _dataIndicator "searching"
+
+ input {
+ _type "search"
+ _dataBind "query"
+ _dataOn ("input", [ "debounce.200ms" ], "@get('/api/search')")
+ }
+
+ span { _dataShow "$searching"; "Searching..." }
+ div { _id "search-results" }
+}""") ] ]
+ |> List.concat
+
+ let page =
+ { id = "datastar"
+ path = "/extensions/datastar"
+ aliases = []
+ navLabel = "Datastar"
+ category = "Extensions"
+ title = "Datastar"
+ browserTitle = "Datastar - FSharp.ViewEngine"
+ nodes = nodes }
diff --git a/sln/src/Docs/src/Pages/Home.fs b/sln/src/Docs/src/Pages/Home.fs
new file mode 100644
index 0000000..cde24b4
--- /dev/null
+++ b/sln/src/Docs/src/Pages/Home.fs
@@ -0,0 +1,66 @@
+namespace Docs.Pages
+
+open Docs.Common
+
+module Home =
+ let page =
+ { id = "home"
+ path = "/"
+ aliases = [ ]
+ navLabel = "Introduction"
+ category = "Getting started"
+ title = "FSharp.ViewEngine"
+ browserTitle = "FSharp.ViewEngine Documentation"
+ nodes = [
+ Paragraph [ Text "A minimal, fast view engine for F# that combines the best ideas from several F# view engines. Inspired by "; Link("Giraffe.ViewEngine", "https://github.com/giraffe-fsharp/Giraffe.ViewEngine"); Text ", "; Link("Feliz.ViewEngine", "https://github.com/dbrattli/Feliz.ViewEngine"); Text ", "; Link("Oxpecker.ViewEngine", "https://github.com/Lanayx/Oxpecker"); Text ", and "; Link("Bolero", "https://github.com/fsbolero/Bolero"); Text "." ];
+ Heading { id = "design"; title = "Design"; level = 2 };
+ Paragraph [ Text "FSharp.ViewEngine uses "; Strong [ Text "computation expressions" ]; Text " (like Oxpecker.ViewEngine and Bolero) to build elements. Each element takes a "; Strong [ Text "Feliz-style single sequence" ]; Text " of attributes and children — there are no separate attribute and children lists. Attributes are "; Strong [ Text "prefixed with underscore" ]; Text " by convention (like Giraffe.ViewEngine, e.g. "; InlineContent.Code "_class"; Text ", "; InlineContent.Code "_id"; Text ", "; InlineContent.Code "_dataOn"; Text "), which produces clean syntax with nice syntax highlighting. The computation expression allows "; Strong [ Text "mixed yielding" ]; Text " of strings, elements, and attributes in any order, so there is no need for a special "; InlineContent.Code "_children"; Text " attribute." ];
+ Heading { id = "key-features"; title = "Key Features"; level = 2 };
+ UnorderedList [
+ [ Strong [ Text "Minimal and fast" ]; Text " — as lean as possible while remaining expressive and type-safe" ];
+ [ Strong [ Text "Type-safe HTML generation" ]; Text " with F#" ];
+ [ Strong [ Text "Built-in support for Datastar, HTMX, Alpine.js, Tailwind CSS, and SVG" ] ];
+ [ Strong [ Text "Composable and functional approach" ] ];
+ [ Strong [ Text "No runtime dependencies" ] ]
+ ];
+ Heading { id = "quick-example"; title = "Quick Example"; level = 2 };
+ CodeBlock("fsharp", """open FSharp.ViewEngine
+open type Html
+open type Datastar
+open type TailwindElements
+
+let myPage =
+ html {
+ _lang "en"
+ head {
+ title "My App"
+ meta { _charset "utf-8" }
+ link { _href "/css/tailwind.css"; _rel "stylesheet" }
+ }
+ body {
+ _dataSignals "{showContent: false}"
+ _class "bg-gray-100"
+ div {
+ _class [ "container"; "mx-auto"; "p-4" ]
+ h1 {
+ _class [ "text-3xl"; "font-bold"; "text-blue-600"; "mb-4" ]
+ "Welcome!"
+ }
+ button {
+ _class [ "bg-blue-500"; "text-white"; "px-4"; "py-2"; "rounded" ]
+ _dataOn ("click", "$showContent = !$showContent")
+ "Toggle Content"
+ }
+ div {
+ _dataShow "$showContent"
+ _style "display: none"
+ _class [ "mt-4" ]
+ "Content loaded with Datastar."
+ }
+ }
+ }
+ }
+ |> Render.toHtmlDocString""");
+ Heading { id = "getting-started"; title = "Getting Started"; level = 2 };
+ Paragraph [ Text "To get started with FSharp.ViewEngine, check out the "; Link("Installation", "/installation"); Text " guide and then see the "; Link("Usage", "/usage"); Text " example." ];
+ ] }
diff --git a/sln/src/Docs/src/Pages/Htmx.fs b/sln/src/Docs/src/Pages/Htmx.fs
new file mode 100644
index 0000000..bd51312
--- /dev/null
+++ b/sln/src/Docs/src/Pages/Htmx.fs
@@ -0,0 +1,215 @@
+namespace Docs.Pages
+
+open Docs.Common
+
+module Htmx =
+ let private section id title =
+ [ Heading { id = id; title = title; level = 2 } ]
+
+ let private attribute id description source =
+ [ Heading { id = id; title = id; level = 3 }
+ Paragraph [ Text description ]
+ CodeBlock("fsharp", source) ]
+
+ let private nodes =
+ [ [ Paragraph [ Text "FSharp.ViewEngine provides all non-deprecated HTMX 2.0.9 attributes through the "; InlineContent.Code "Htmx"; Text " type. See the "; Link("official HTMX 2 reference", "https://v2-0v2-0.htmx.org/reference/"); Text " for the complete value grammar and inheritance rules." ] ]
+ section "setup" "Setup"
+ [ Paragraph [ Text "Open the "; InlineContent.Code "Htmx"; Text " type to access HTMX attributes:" ]
+ CodeBlock("fsharp", """open FSharp.ViewEngine
+open type Html
+open type Htmx""") ]
+
+ section "request-methods" "Request Methods"
+ attribute "hx-get" "Issue a GET request to a URL with _hxGet." """button {
+ _hxGet "/api/items"
+ "Load items"
+}"""
+ attribute "hx-post" "Issue a POST request to a URL with _hxPost." """form {
+ _hxPost "/api/items"
+ input { _name "title" }
+ button { "Create item" }
+}"""
+ attribute "hx-put" "Issue a PUT request to a URL with _hxPut." """button {
+ _hxPut "/api/items/1"
+ "Replace item"
+}"""
+ attribute "hx-patch" "Issue a PATCH request to a URL with _hxPatch." """button {
+ _hxPatch "/api/items/1"
+ "Update item"
+}"""
+ attribute "hx-delete" "Issue a DELETE request to a URL with _hxDelete." """button {
+ _hxDelete "/api/items/1"
+ "Delete item"
+}"""
+
+ section "request-data-and-configuration" "Request Data and Configuration"
+ attribute "hx-encoding" "Change the request encoding with _hxEncoding, typically for file uploads." """form {
+ _hxPost "/api/upload"
+ _hxEncoding "multipart/form-data"
+}"""
+ attribute "hx-headers" "Add request headers as JSON with _hxHeaders." "button {\n _hxPost \"/api/items\"\n _hxHeaders \"\"\"{\"X-CSRF-Token\": \"token\"}\"\"\"\n \"Create item\"\n}"
+ attribute "hx-include" "Include values from additional elements with _hxInclude." """button {
+ _hxPost "/api/search"
+ _hxInclude "[name='query']"
+ "Search"
+}"""
+ attribute "hx-params" "Filter submitted parameters with _hxParams. Values include *, none, a comma-separated list, or not followed by a list." """form {
+ _hxPost "/api/profile"
+ _hxParams "name,email"
+}"""
+ attribute "hx-request" "Configure request timeout, credentials, or headers with _hxRequest." "button {\n _hxGet \"/api/report\"\n _hxRequest \"\"\"{\"timeout\": 5000}\"\"\"\n \"Load report\"\n}"
+ attribute "hx-vals" "Add values to the request as JSON with _hxVals." "button {\n _hxPost \"/api/action\"\n _hxVals \"\"\"{\"source\": \"toolbar\"}\"\"\"\n \"Run action\"\n}"
+
+ section "targeting-and-swapping" "Targeting and Swapping"
+ attribute "hx-target" "Choose the element that receives the response with _hxTarget." """button {
+ _hxGet "/api/items"
+ _hxTarget "#results"
+ "Load items"
+}
+div { _id "results" }"""
+ attribute "hx-select" "Select a fragment from the response with _hxSelect." """button {
+ _hxGet "/items/1"
+ _hxSelect "#item-details"
+ "Load details"
+}"""
+ attribute "hx-select-oob" "Select one or more response fragments for out-of-band swaps with _hxSelectOOB." """button {
+ _hxGet "/dashboard"
+ _hxSelectOOB "#alerts,#navigation:outerHTML"
+ "Refresh dashboard"
+}"""
+ attribute "hx-swap" "Control how the response is swapped with _hxSwap. Swap modifiers can be included in the same value." """button {
+ _hxGet "/api/items"
+ _hxSwap "beforeend settle:200ms"
+ "Append items"
+}"""
+ attribute "hx-swap-oob" "Mark response content for an out-of-band swap with _hxSwapOOB." """div {
+ _id "notifications"
+ _hxSwapOOB "true"
+ "Updated notifications"
+}"""
+ attribute "hx-preserve" "Preserve an element by id across ancestor updates with the presence-only _hxPreserve attribute." """video {
+ _id "tutorial"
+ _hxPreserve
+}"""
+
+ section "requests-in-flight" "Requests in Flight"
+ attribute "hx-indicator" "Choose the element that receives the htmx-request class with _hxIndicator." """button {
+ _hxGet "/api/report"
+ _hxIndicator "#spinner"
+ "Load report"
+}
+span { _id "spinner"; _class "htmx-indicator"; "Loading..." }"""
+ attribute "hx-disabled-elt" "Disable selected elements while a request is in flight with _hxDisabledElt." """button {
+ _hxPost "/api/items"
+ _hxDisabledElt "this"
+ "Create item"
+}"""
+ attribute "hx-sync" "Coordinate requests with _hxSync using a selector and synchronization strategy." """input {
+ _name "query"
+ _hxGet "/api/search"
+ _hxTrigger "input changed delay:300ms"
+ _hxSync "this:replace"
+}"""
+ attribute "hx-validate" "Force an element to run HTML validation before a request with _hxValidate." """input {
+ _type "email"
+ _hxPost "/api/validate-email"
+ _hxValidate "true"
+}"""
+
+ section "triggering-and-interaction" "Triggering and Interaction"
+ attribute "hx-trigger" "Specify the events and modifiers that trigger a request with _hxTrigger." """input {
+ _name "query"
+ _hxGet "/api/search"
+ _hxTrigger "keyup changed delay:500ms"
+}"""
+ attribute "hx-confirm" "Ask for confirmation before issuing a request with _hxConfirm." """button {
+ _hxDelete "/account"
+ _hxConfirm "Delete your account?"
+ "Delete account"
+}"""
+ attribute "hx-prompt" "Prompt for a value before issuing a request with _hxPrompt. HTMX sends the result in the HX-Prompt header." """button {
+ _hxDelete "/account"
+ _hxPrompt "Enter your account name to confirm"
+ "Delete account"
+}"""
+ attribute "hx-on" "Handle DOM or HTMX events with _hxOn. Attribute names are case-insensitive, so use kebab-case HTMX event names rather than camelCase." """form {
+ _hxPost "/api/items"
+ _hxOn ("htmx:before-request", "showSpinner()")
+ _hxOn ("htmx:after-request", "hideSpinner()")
+}"""
+ [ Paragraph [ Strong [ Text "Security:" ]; Text " "; InlineContent.Code "_hxOn"; Text " executes inline JavaScript. Only use trusted script content and follow your application's Content Security Policy." ] ]
+
+ section "navigation-and-history" "Navigation and History"
+ attribute "hx-boost" "Progressively enhance links and forms with _hxBoost." """main {
+ _hxBoost "true"
+ a { _href "/account"; "Account" }
+}"""
+ attribute "hx-push-url" "Push the fetched URL, a custom URL, or no URL into browser history with _hxPushUrl." """button {
+ _hxGet "/account"
+ _hxPushUrl "true"
+ "Open account"
+}"""
+ attribute "hx-replace-url" "Replace the current browser-history URL with _hxReplaceUrl." """button {
+ _hxGet "/account"
+ _hxReplaceUrl "/account/home"
+ "Open account"
+}"""
+ attribute "hx-history" "Prevent sensitive page state from entering the HTMX history cache with _hxHistory \"false\"." """section {
+ _hxHistory "false"
+ "Sensitive account details"
+}"""
+ attribute "hx-history-elt" "Choose a narrower history snapshot element with the presence-only _hxHistoryElt attribute." """main {
+ _id "content"
+ _hxHistoryElt
+}"""
+
+ section "inheritance-and-processing" "Inheritance and Processing"
+ attribute "hx-disable" "Disable HTMX processing for an element and its descendants with the presence-only _hxDisable attribute." """section {
+ _hxDisable
+ "HTMX ignores this subtree"
+}"""
+ attribute "hx-disinherit" "Disable inheritance for selected attributes, or all attributes with *, using _hxDisinherit." """section {
+ _hxDisinherit "hx-target hx-swap"
+}"""
+ attribute "hx-inherit" "Explicitly enable inheritance when HTMX's disableInheritance configuration is active with _hxInherit." """section {
+ _hxTarget "#content"
+ _hxInherit "hx-target"
+}"""
+ attribute "hx-ext" "Enable one or more HTMX extensions for an element and its descendants with _hxExt." """body {
+ _hxExt "preload,morph"
+}"""
+
+ section "generic-and-deprecated-attributes" "Generic and Deprecated Attributes"
+ [ Paragraph [ Text "Use "; InlineContent.Code "_hx"; Text " for extension attributes or newer HTMX attributes that do not yet have a dedicated helper:" ];
+ CodeBlock("fsharp", """div {
+ _hx ("custom-extension-option", "value")
+}""");
+ Paragraph [ InlineContent.Code "hx-vars"; Text " is deprecated in HTMX 2; use "; InlineContent.Code "_hxVals"; Text ". The former "; InlineContent.Code "hx-sse"; Text " and "; InlineContent.Code "hx-ws"; Text " core attributes moved to extensions and therefore do not have dedicated core helpers." ] ]
+
+ section "complete-example" "Complete Example"
+ [ Paragraph [ Text "A search form combining request, synchronization, targeting, indicator, and history attributes:" ]
+ CodeBlock("fsharp", """form {
+ _hxGet "/api/search"
+ _hxTrigger "input changed delay:300ms, search"
+ _hxTarget "#search-results"
+ _hxIndicator "#search-spinner"
+ _hxDisabledElt "find button"
+ _hxSync "this:replace"
+ _hxPushUrl "true"
+
+ input { _type "search"; _name "query"; _hxValidate "true" }
+ button { _type "submit"; "Search" }
+ span { _id "search-spinner"; _class "htmx-indicator"; "Searching..." }
+ div { _id "search-results" }
+}""") ] ]
+ |> List.concat
+
+ let page =
+ { id = "htmx"
+ path = "/extensions/htmx"
+ aliases = []
+ navLabel = "HTMX"
+ category = "Extensions"
+ title = "HTMX"
+ browserTitle = "HTMX - FSharp.ViewEngine"
+ nodes = nodes }
diff --git a/sln/src/Docs/src/Pages/Installation.fs b/sln/src/Docs/src/Pages/Installation.fs
new file mode 100644
index 0000000..18d3c00
--- /dev/null
+++ b/sln/src/Docs/src/Pages/Installation.fs
@@ -0,0 +1,32 @@
+namespace Docs.Pages
+
+open Docs.Common
+
+module Installation =
+ let page =
+ { id = "installation"
+ path = "/installation"
+ aliases = [ ]
+ navLabel = "Installation"
+ category = "Getting started"
+ title = "Installation"
+ browserTitle = "Installation - FSharp.ViewEngine"
+ nodes = [
+ Paragraph [ Text "FSharp.ViewEngine is distributed as a NuGet package. You can install it using your preferred package manager." ];
+ Heading { id = "using-net-cli"; title = "Using .NET CLI"; level = 2 };
+ CodeBlock("bash", """dotnet package add FSharp.ViewEngine""");
+ Heading { id = "using-paket-cli"; title = "Using Paket CLI"; level = 2 };
+ CodeBlock("bash", """dotnet paket add FSharp.ViewEngine""");
+ Heading { id = "runtime-support"; title = "Runtime Support"; level = 2 };
+ Paragraph [ Text "The NuGet package ships a single "; InlineContent.Code "net8.0 compatibility asset"; Text ". NuGet selects that asset for compatible newer runtimes; a target asset is not a separate runtime dependency." ];
+ Paragraph [ Text "FSharp.ViewEngine is actively tested on .NET 8, .NET 9, and .NET 10 while those runtimes remain supported by Microsoft." ];
+ UnorderedList [
+ [ Text ".NET 8 and .NET 9 support ends November 10, 2026." ];
+ [ Text ".NET 10 LTS support ends November 14, 2028." ]
+ ];
+ Paragraph [ Text "After .NET 8 and .NET 9 reach end of support, their runtime tests will be removed. The "; InlineContent.Code "net8.0"; Text " package asset may remain as the compatibility baseline until the implementation needs APIs from a newer target framework." ];
+ Heading { id = "source-debugging"; title = "Source Debugging"; level = 2 };
+ Paragraph [ Text "Each release publishes portable symbols separately from the main package. Source Link maps those symbols to the matching GitHub commit so supported debuggers can retrieve the exact source on demand." ];
+ Heading { id = "next-steps"; title = "Next Steps"; level = 2 };
+ Paragraph [ Text "Once you have FSharp.ViewEngine installed, head over to the "; Link("Usage", "/usage"); Text " guide to start building your first HTML views." ];
+ ] }
diff --git a/sln/src/Docs/src/Pages/Registry.fs b/sln/src/Docs/src/Pages/Registry.fs
new file mode 100644
index 0000000..94e45da
--- /dev/null
+++ b/sln/src/Docs/src/Pages/Registry.fs
@@ -0,0 +1,18 @@
+namespace Docs.Pages
+
+open Docs.Common
+
+module Registry =
+ let navigation =
+ [ { label = "Getting started"
+ pages = [ Home.page; Installation.page; Custom.page; Usage.page ] }
+ { label = "Extensions"
+ pages = [ Svg.page; Datastar.page; Htmx.page; Alpine.page; TailwindElements.page ] }
+ { label = "Project"
+ pages = [ Benchmarks.page; Changelog.page ] } ]
+
+ let all = navigation |> List.collect _.pages
+
+ let aliases =
+ all
+ |> List.collect (fun page -> page.aliases |> List.map (fun alias -> alias, page.path))
diff --git a/sln/src/Docs/src/Pages/Svg.fs b/sln/src/Docs/src/Pages/Svg.fs
new file mode 100644
index 0000000..886a17d
--- /dev/null
+++ b/sln/src/Docs/src/Pages/Svg.fs
@@ -0,0 +1,174 @@
+namespace Docs.Pages
+
+open Docs.Common
+
+module Svg =
+ let private heading id title =
+ Heading { id = id; title = title; level = 2 }
+
+ let private nodes =
+ [ Paragraph [ Text "FSharp.ViewEngine provides a maintained SVG 2 production subset for icons, charts, gradients, clipping, reusable symbols, text, and accessible inline graphics." ];
+ heading "support-policy" "Support Policy";
+ Paragraph [ Text "The "; InlineContent.Code "Svg"; Text " type intentionally covers 21 common elements rather than every SVG 2 feature. Unsupported filters, animation, metadata, and specialized elements remain available through the generic trusted-name escape hatches." ];
+ Paragraph [ Text "SVG-specific helpers complement the global attributes on "; InlineContent.Code "Html"; Text ". Continue using "; InlineContent.Code "_id"; Text ", "; InlineContent.Code "_class"; Text ", "; InlineContent.Code "_style"; Text ", "; InlineContent.Code "_href"; Text ", "; InlineContent.Code "_role"; Text ", and the WAI-ARIA helpers from "; InlineContent.Code "Html"; Text "." ];
+
+ heading "setup" "Setup";
+ Paragraph [ Text "Open both static types when building inline SVG:" ];
+ CodeBlock("fsharp", """open FSharp.ViewEngine
+open type Html
+open type Svg""");
+ Paragraph [ Text "The SVG text and title builders are named "; InlineContent.Code "textElement"; Text " and "; InlineContent.Code "titleElement"; Text " so opening "; InlineContent.Code "Svg"; Text " does not shadow "; InlineContent.Code "Html.text"; Text " or "; InlineContent.Code "Html.title"; Text ". "; InlineContent.Code "useElement"; Text " avoids the F# "; InlineContent.Code "use"; Text " keyword." ];
+
+ heading "element-reference" "Element Reference";
+ Paragraph [ Strong [ Text "Structure and descriptions: " ]; InlineContent.Code "svg"; Text ", "; InlineContent.Code "g"; Text ", "; InlineContent.Code "defs"; Text ", "; InlineContent.Code "symbol"; Text ", "; InlineContent.Code "useElement"; Text ", "; InlineContent.Code "titleElement"; Text ", and "; InlineContent.Code "desc"; Text "." ];
+ Paragraph [ Strong [ Text "Shapes: " ]; InlineContent.Code "path"; Text ", "; InlineContent.Code "circle"; Text ", "; InlineContent.Code "rect"; Text ", "; InlineContent.Code "line"; Text ", "; InlineContent.Code "polyline"; Text ", "; InlineContent.Code "polygon"; Text ", and "; InlineContent.Code "ellipse"; Text "." ];
+ Paragraph [ Strong [ Text "Resources and paint: " ]; InlineContent.Code "clipPath"; Text ", "; InlineContent.Code "mask"; Text ", "; InlineContent.Code "linearGradient"; Text ", "; InlineContent.Code "radialGradient"; Text ", and "; InlineContent.Code "stop"; Text "." ];
+ Paragraph [ Strong [ Text "Text: " ]; InlineContent.Code "textElement"; Text " and "; InlineContent.Code "tspan"; Text "." ];
+
+ heading "numeric-and-length-values" "Numeric and Length Values";
+ Paragraph [ Text "Geometry and numeric presentation helpers accept integers, invariant-culture floating-point values, and—where SVG permits lengths, percentages, or lists—strings. Root width and height retain numeric "; InlineContent.Code "Svg._width"; Text " and "; InlineContent.Code "Svg._height"; Text " helpers; use the existing HTML string helpers for CSS lengths." ];
+ CodeBlock("fsharp", """svg {
+ Svg._width 24
+ Svg._height 24.5
+ Html._width "100%"
+ circle { _cx "50%"; _cy "50%"; _r 7.5 }
+}""");
+
+ heading "icon-example" "Icon Example";
+ CodeBlock("fsharp", """let checkIcon =
+ svg {
+ _viewBox "0 0 24 24"
+ _preserveAspectRatio "xMidYMid meet"
+ Svg._width 24
+ Svg._height 24
+ _fill "none"
+ _stroke "currentColor"
+ _strokeWidth 1.5
+ _strokeLinecap "round"
+ _strokeLinejoin "round"
+ _vectorEffect "non-scaling-stroke"
+ path {
+ _d "M5 13l4 4L19 7"
+ _pathLength 1.0
+ }
+ }""");
+
+ heading "chart-example" "Chart and Text Example";
+ CodeBlock("fsharp", """svg {
+ _viewBox "0 0 400 200"
+ g {
+ _transform "translate(40 10)"
+ _opacity 0.9
+ rect { _x 0; _y 0; Svg._width 320; Svg._height 160; _rx 4 }
+ line { _x1 0; _y1 160; _x2 320; _y2 160 }
+ polyline { _points "0,140 80,100 160,120 240,40 320,20" }
+ polygon { _points "0,160 80,120 160,140 160,160" }
+ ellipse { _cx 240; _cy 40; _rx 6; _ry 4 }
+ textElement {
+ _x 160
+ _y 190
+ _textAnchor "middle"
+ _dominantBaseline "middle"
+ _fontFamily "sans-serif"
+ _fontSize "12px"
+ tspan { "Revenue" }
+ }
+ }
+}""");
+
+ heading "resources-example" "Gradients, Clipping, Masking, and Reuse";
+ CodeBlock("fsharp", """svg {
+ _xmlns "http://www.w3.org/2000/svg"
+ defs {
+ linearGradient {
+ _id "brand-gradient"
+ _x1 "0%"
+ _y1 "0%"
+ _x2 "100%"
+ _y2 "0%"
+ _gradientUnits "objectBoundingBox"
+ _gradientTransform "rotate(10)"
+ _spreadMethod "pad"
+ stop { _offset "0%"; _stopColor "#0ea5e9"; _stopOpacity 1.0 }
+ stop { _offset "100%"; _stopColor "#6366f1"; _stopOpacity 0.75 }
+ }
+ radialGradient {
+ _id "spotlight"
+ _cx "50%"; _cy "50%"; _r "50%"
+ _fx "45%"; _fy "45%"; _fr "5%"
+ }
+ clipPath {
+ _id "plot-clip"
+ _clipPathUnits "userSpaceOnUse"
+ rect { Svg._width 100; Svg._height 50 }
+ }
+ mask {
+ _id "fade-mask"
+ _maskUnits "userSpaceOnUse"
+ _maskContentUnits "userSpaceOnUse"
+ }
+ symbol {
+ _id "dot"
+ _viewBox "0 0 10 10"
+ circle { _cx 5; _cy 5; _r 5 }
+ }
+ }
+ g {
+ _fill "url(#brand-gradient)"
+ _fillOpacity 0.8
+ _clipPath "url(#plot-clip)"
+ _mask "url(#fade-mask)"
+ useElement { _href "#dot"; _x 10; _y 20 }
+ }
+}""");
+
+ heading "accessibility" "Accessibility";
+ Paragraph [ Text "For an informative graphic, give the root an image role and connect direct child "; InlineContent.Code "titleElement"; Text " and "; InlineContent.Code "desc"; Text " elements with "; InlineContent.Code "_ariaLabelledby"; Text ". SVG Accessibility API Mappings expose these children as the accessible name and description." ];
+ CodeBlock("fsharp", """svg {
+ _role "img"
+ _ariaLabelledby "sales-title sales-description"
+ titleElement { _id "sales-title"; "Quarterly sales" }
+ desc {
+ _id "sales-description"
+ "Sales increased in each quarter."
+ }
+ // chart geometry
+}""");
+ Paragraph [ Text "Hide a purely decorative SVG from assistive technology:" ];
+ CodeBlock("fsharp", """svg {
+ _ariaHidden true
+ path { _d "M5 13l4 4L19 7" }
+}""");
+
+ heading "linking" "Linking and Namespaces";
+ Paragraph [ Text "SVG 2 uses the unnamespaced "; InlineContent.Code "href"; Text " attribute. Use "; InlineContent.Code "Html._href"; Text " with "; InlineContent.Code "useElement"; Text " and do not emit deprecated "; InlineContent.Code "xlink:href"; Text ". Add "; InlineContent.Code "_xmlns"; Text " when producing standalone SVG markup; it is optional for inline SVG parsed as HTML." ];
+ CodeBlock("fsharp", """symbol { _id "check"; path { _d "M5 13l4 4L19 7" } }
+useElement { _href "#check" }""");
+
+ heading "attribute-reference" "Attribute Reference";
+ Paragraph [ Strong [ Text "Viewport and global presentation: " ]; InlineContent.Code "_viewBox"; Text ", "; InlineContent.Code "_preserveAspectRatio"; Text ", "; InlineContent.Code "_xmlns"; Text ", "; InlineContent.Code "_transform"; Text ", "; InlineContent.Code "_opacity"; Text ", and "; InlineContent.Code "_vectorEffect"; Text "." ];
+ Paragraph [ Strong [ Text "Fill and stroke: " ]; InlineContent.Code "_fill"; Text ", "; InlineContent.Code "_fillOpacity"; Text ", "; InlineContent.Code "_fillRule"; Text ", "; InlineContent.Code "_stroke"; Text ", "; InlineContent.Code "_strokeWidth"; Text ", "; InlineContent.Code "_strokeOpacity"; Text ", "; InlineContent.Code "_strokeLinecap"; Text ", "; InlineContent.Code "_strokeLinejoin"; Text ", "; InlineContent.Code "_strokeMiterlimit"; Text ", "; InlineContent.Code "_strokeDasharray"; Text ", and "; InlineContent.Code "_strokeDashoffset"; Text "." ];
+ Paragraph [ Strong [ Text "Geometry: " ]; InlineContent.Code "_x"; Text ", "; InlineContent.Code "_y"; Text ", "; InlineContent.Code "_x1"; Text ", "; InlineContent.Code "_y1"; Text ", "; InlineContent.Code "_x2"; Text ", "; InlineContent.Code "_y2"; Text ", "; InlineContent.Code "_cx"; Text ", "; InlineContent.Code "_cy"; Text ", "; InlineContent.Code "_r"; Text ", "; InlineContent.Code "_rx"; Text ", "; InlineContent.Code "_ry"; Text ", "; InlineContent.Code "_width"; Text ", "; InlineContent.Code "_height"; Text ", "; InlineContent.Code "_d"; Text ", "; InlineContent.Code "_points"; Text ", and "; InlineContent.Code "_pathLength"; Text "." ];
+ Paragraph [ Strong [ Text "Resources: " ]; InlineContent.Code "_clipRule"; Text ", "; InlineContent.Code "_clipPath"; Text ", "; InlineContent.Code "_clipPathUnits"; Text ", "; InlineContent.Code "_mask"; Text ", "; InlineContent.Code "_maskUnits"; Text ", "; InlineContent.Code "_maskContentUnits"; Text ", "; InlineContent.Code "_gradientUnits"; Text ", "; InlineContent.Code "_gradientTransform"; Text ", "; InlineContent.Code "_spreadMethod"; Text ", "; InlineContent.Code "_fx"; Text ", "; InlineContent.Code "_fy"; Text ", "; InlineContent.Code "_fr"; Text ", "; InlineContent.Code "_offset"; Text ", "; InlineContent.Code "_stopColor"; Text ", and "; InlineContent.Code "_stopOpacity"; Text "." ];
+ Paragraph [ Strong [ Text "Text: " ]; InlineContent.Code "_dx"; Text ", "; InlineContent.Code "_dy"; Text ", "; InlineContent.Code "_textAnchor"; Text ", "; InlineContent.Code "_dominantBaseline"; Text ", "; InlineContent.Code "_fontFamily"; Text ", "; InlineContent.Code "_fontSize"; Text ", "; InlineContent.Code "_fontWeight"; Text ", "; InlineContent.Code "_textLength"; Text ", and "; InlineContent.Code "_lengthAdjust"; Text "." ];
+
+ heading "unsupported-svg" "Unsupported SVG";
+ Paragraph [ Text "Use "; InlineContent.Code "Html.el"; Text " for an SVG element outside the maintained production subset and "; InlineContent.Code "Html._attr"; Text " for an unsupported attribute. Names passed to both helpers are trusted markup tokens and are not validated." ];
+ CodeBlock("fsharp", """svg {
+ Html.el "filter" {
+ _id "blur"
+ Html.el "feGaussianBlur" {
+ _attr ("stdDeviation", "2")
+ }
+ }
+}""") ]
+
+ let page =
+ { id = "svg"
+ path = "/extensions/svg"
+ aliases = []
+ navLabel = "SVG"
+ category = "Extensions"
+ title = "SVG"
+ browserTitle = "SVG - FSharp.ViewEngine"
+ nodes = nodes }
diff --git a/sln/src/Docs/src/Pages/TailwindElements.fs b/sln/src/Docs/src/Pages/TailwindElements.fs
new file mode 100644
index 0000000..3a251ff
--- /dev/null
+++ b/sln/src/Docs/src/Pages/TailwindElements.fs
@@ -0,0 +1,179 @@
+namespace Docs.Pages
+
+open Docs.Common
+
+module TailwindElements =
+ let private heading id title =
+ Heading { id = id; title = title; level = 2 }
+
+ let private componentNodes id title description source =
+ [ heading id title;
+ Paragraph [ Text description ];
+ CodeBlock("fsharp", source) ]
+
+ let private nodes =
+ [ [ Paragraph [ Text "FSharp.ViewEngine covers all 23 custom elements published by "; Link("Tailwind Plus Elements 1.0.22", "https://www.npmjs.com/package/@tailwindplus/elements/v/1.0.22"); Text ", a framework-independent library for the interactive behavior in Tailwind Plus HTML components." ];
+ heading "setup" "Setup";
+ Paragraph [ Text "Install Elements in the application and open the "; InlineContent.Code "TailwindElements"; Text " type when building views:" ];
+ CodeBlock("shell", "npm install @tailwindplus/elements@1.0.22");
+ CodeBlock("javascript", "import '@tailwindplus/elements'");
+ CodeBlock("fsharp", """open FSharp.ViewEngine
+open type Html
+open type TailwindElements""");
+ Paragraph [ Text "For applications without a JavaScript build pipeline, load the pinned module from a CDN:" ];
+ CodeBlock("fsharp", """script {
+ _src "https://cdn.jsdelivr.net/npm/@tailwindplus/elements@1.0.22"
+ _type "module"
+}""");
+ Paragraph [ Text "Elements targets modern browsers supported by Tailwind CSS v4: Chrome 111+, Safari 16.4+, and Firefox 128+." ];
+ heading "shared-attributes" "Shared Attributes";
+ Paragraph [ Text "Use the Elements-specific helpers for popover positioning. Anchor values follow the official grammar, such as "; InlineContent.Code "bottom start"; Text "." ];
+ CodeBlock("fsharp", """elPopover {
+ _popover
+ _anchor "bottom start"
+ _anchorStrategy "fixed"
+}""");
+ Paragraph [ Text "Invoker commands and other platform attributes already belong to "; InlineContent.Code "Html"; Text ". Use helpers such as "; InlineContent.Code "_command"; Text ", "; InlineContent.Code "_commandfor"; Text ", "; InlineContent.Code "_popovertarget"; Text ", "; InlineContent.Code "_open"; Text ", and "; InlineContent.Code "_hidden"; Text " alongside the custom element builders." ];
+ Paragraph [ Text "Transition state attributes such as "; InlineContent.Code "data-closed"; Text ", "; InlineContent.Code "data-enter"; Text ", and "; InlineContent.Code "data-leave"; Text " are managed by Elements and should be treated as read-only styling hooks." ] ];
+
+ componentNodes "autocomplete" "Autocomplete" "Combine elAutocomplete, elOptions, elOption, and elSelectedContent with native form controls." """elAutocomplete {
+ input { _name "user" }
+ button {
+ _type "button"
+ elSelectedContent { "Choose a user" }
+ }
+ elOptions {
+ _popover
+ _anchor "bottom start"
+ elOption { _value "wade"; "Wade Cooper" }
+ elOption { _value "jane"; "Jane Doe" }
+ }
+}""";
+
+ componentNodes "command-palette" "Command Palette" "Use elCommandPalette with its list, defaults, grouping, empty-state, and preview helpers." """elCommandPalette {
+ _name "command"
+ input { _autofocus true; _placeholder "Search…" }
+ elCommandList {
+ elDefaults { button { _type "button"; "Recent command" } }
+ elCommandGroup {
+ button { _id "open-file"; _type "button"; "Open file" }
+ }
+ }
+ elNoResults { _hidden true; "No results found." }
+ elCommandPreview { _for "open-file"; "Open a file" }
+}""";
+
+ componentNodes "copy-button" "Copy Button" "Wrap copyable text with elCopyable and target it with the standard invoker-command helpers." """elCopyable {
+ _id "install-command"
+ "dotnet package add FSharp.ViewEngine"
+}
+button {
+ _type "button"
+ _command "--copy"
+ _commandfor "install-command"
+ "Copy"
+}""";
+
+ componentNodes "dialog" "Dialog" "Nest a native dialog inside elDialog, then use elDialogBackdrop and elDialogPanel for transitionable presentation." """button {
+ _type "button"
+ _command "show-modal"
+ _commandfor "delete-profile"
+ "Delete profile"
+}
+elDialog {
+ dialog {
+ _id "delete-profile"
+ elDialogBackdrop { _class "fixed inset-0 bg-black/50" }
+ elDialogPanel {
+ form {
+ _method "dialog"
+ p { "Delete this profile?" }
+ button {
+ _type "button"
+ _command "close"
+ _commandfor "delete-profile"
+ "Cancel"
+ }
+ button { _type "submit"; "Delete" }
+ }
+ }
+ }
+}""";
+
+ componentNodes "disclosure" "Disclosure" "Pair elDisclosure with show, hide, or toggle invoker commands." """button {
+ _type "button"
+ _command "--toggle"
+ _commandfor "answer"
+ "Show answer"
+}
+elDisclosure {
+ _id "answer"
+ _hidden true
+ "The answer is 42."
+}""";
+
+ componentNodes "dropdown-menu" "Dropdown Menu" "Use elDropdown to connect a native trigger button with an anchored elMenu." """elDropdown {
+ button { _type "button"; "Options" }
+ elMenu {
+ _popover
+ _anchor "bottom start"
+ button { _type "button"; "Edit" }
+ button { _type "button"; "Delete" }
+ }
+}""";
+
+ componentNodes "popover" "Popover" "Use elPopover for arbitrary floating content and elPopoverGroup to keep related popovers open while focus moves within the group." """elPopoverGroup {
+ button {
+ _type "button"
+ _popovertarget "account-menu"
+ "Account"
+ }
+ elPopover {
+ _id "account-menu"
+ _popover
+ _anchor "bottom end"
+ _anchorStrategy "fixed"
+ "Account options"
+ }
+}""";
+
+ componentNodes "select" "Select" "Build an accessible custom select with elSelect, elSelectedContent, elOptions, and elOption." """elSelect {
+ _name "status"
+ _value "active"
+ button {
+ _type "button"
+ elSelectedContent { "Active" }
+ }
+ elOptions {
+ _popover
+ _anchor "bottom start"
+ elOption { _value "active"; "Active" }
+ elOption { _value "inactive"; "Inactive" }
+ elOption { _value "archived"; "Archived" }
+ }
+}""";
+
+ componentNodes "tabs" "Tabs" "Place native tab buttons in elTabList and corresponding direct-child panels in elTabPanels." """elTabGroup {
+ elTabList {
+ button { _type "button"; "Account" }
+ button { _type "button"; "Security" }
+ }
+ elTabPanels {
+ div { "Account settings" }
+ div { _hidden true; "Security settings" }
+ }
+}""";
+
+ [ heading "runtime-readiness" "Runtime Readiness";
+ Paragraph [ Text "If application JavaScript needs to call component methods, first check "; InlineContent.Code "customElements.get"; Text " or wait for the "; InlineContent.Code "elements:ready"; Text " window event. Rendering helpers only produce markup; they do not install or initialize the Elements runtime." ] ] ]
+ |> List.concat
+
+ let page =
+ { id = "tailwind-elements"
+ path = "/extensions/tailwind-elements"
+ aliases = []
+ navLabel = "Tailwind Plus Elements"
+ category = "Extensions"
+ title = "Tailwind Plus Elements"
+ browserTitle = "Tailwind Plus Elements - FSharp.ViewEngine"
+ nodes = nodes }
diff --git a/sln/src/Docs/src/Pages/Usage.fs b/sln/src/Docs/src/Pages/Usage.fs
new file mode 100644
index 0000000..f2eb151
--- /dev/null
+++ b/sln/src/Docs/src/Pages/Usage.fs
@@ -0,0 +1,70 @@
+namespace Docs.Pages
+
+open Docs.Common
+
+module Usage =
+ let page =
+ { id = "usage"
+ path = "/usage"
+ aliases = [ "/giraffe" ]
+ navLabel = "Usage"
+ category = "Getting started"
+ title = "Usage"
+ browserTitle = "Usage - FSharp.ViewEngine"
+ nodes = [
+ Paragraph [ Text "FSharp.ViewEngine integrates with "; Link("Giraffe", "https://giraffe.wiki/"); Text " by rendering elements to an HTML string and returning it via Giraffe's "; InlineContent.Code "htmlString"; Text " handler." ];
+ Heading { id = "minimal-example"; title = "Minimal Example"; level = 2 };
+ CodeBlock("fsharp", """open Microsoft.AspNetCore.Builder
+open Microsoft.Extensions.DependencyInjection
+open Giraffe
+open FSharp.ViewEngine
+open type Html
+
+let indexView =
+ html {
+ _lang "en"
+ head {
+ title "My App"
+ meta { _charset "utf-8" }
+ }
+ body {
+ h1 { "Hello from FSharp.ViewEngine!" }
+ p { "Served by Giraffe." }
+ }
+ }
+
+let indexHandler : HttpHandler =
+ fun next ctx ->
+ let html = Render.toHtmlDocString indexView
+ htmlString html next ctx
+
+let webApp =
+ choose [
+ GET >=> route "/" >=> indexHandler
+ ]
+
+[]
+let main args =
+ let builder = WebApplication.CreateBuilder(args)
+ builder.Services.AddGiraffe() |> ignore
+
+ let app = builder.Build()
+ app.UseGiraffe(webApp)
+ app.Run()
+ 0""");
+ Heading { id = "how-it-works"; title = "How It Works"; level = 2 };
+ OrderedList [
+ [ Text "Build your HTML using FSharp.ViewEngine elements" ];
+ [ Text "Call "; InlineContent.Code "Render.toHtmlDocString"; Text " to get a "; InlineContent.Code ""; Text " string (or "; InlineContent.Code "Render.toString"; Text " for a fragment without the doctype)" ];
+ [ Text "Return it with Giraffe's "; InlineContent.Code "htmlString"; Text " handler" ]
+ ];
+ Paragraph [ Text "That's it — no special adapter or middleware needed." ];
+ Heading { id = "title-elements"; title = "Title Elements"; level = 2 };
+ Paragraph [ Text "Use "; InlineContent.Code "title \"My App\""; Text " for the common text-only form. Use "; InlineContent.Code "titleBuilder"; Text " when the title needs attributes or computation-expression content:" ];
+ CodeBlock("fsharp", """head {
+ titleBuilder {
+ _lang "en"
+ "My App"
+ }
+}""");
+ ] }
diff --git a/sln/src/Docs/src/Program.fs b/sln/src/Docs/src/Program.fs
new file mode 100644
index 0000000..0402aac
--- /dev/null
+++ b/sln/src/Docs/src/Program.fs
@@ -0,0 +1,78 @@
+open Docs.Common
+open Giraffe
+open Microsoft.AspNetCore.Builder
+open Microsoft.Extensions.DependencyInjection
+open Microsoft.Extensions.Hosting
+open Serilog
+open Serilog.Events
+open Serilog.Sinks.OpenTelemetry
+
+let webApp (config:Config) =
+ choose [
+ GET >=> choose [
+ route "/health" >=> json {| status = "ok"; version = config.release.version; commit = config.release.commit |}
+ Handler.routes
+ ]
+ setStatusCode 404 >=> text "Not found"
+ ]
+
+let configureLogger (config:Config) =
+ let initialLogLevel =
+ if config.debug then LogEventLevel.Debug
+ else LogEventLevel.Information
+
+ Log.Logger <-
+ LoggerConfiguration()
+ .MinimumLevel.Is(initialLogLevel)
+ .MinimumLevel.Override("Microsoft.AspNetCore", LogEventLevel.Warning)
+ .WriteTo.Console()
+ .WriteTo.OpenTelemetry(fun options ->
+ options.Endpoint <- config.seq.endpoint + "/ingest/otlp/v1/logs"
+ options.Protocol <- OtlpProtocol.HttpProtobuf
+ options.ResourceAttributes <- dict [ "service.name", box config.appName ])
+ .CreateLogger()
+
+let configureApp (config:Config) (app:IApplicationBuilder) =
+ app
+ .UseSerilogRequestLogging(fun options ->
+ options.GetLevel <- fun context _ _ ->
+ if context.Request.Path.Value = "/health" then LogEventLevel.Verbose
+ else LogEventLevel.Information)
+ .UseStaticFiles()
+ |> ignore
+ app.UseGiraffe(webApp config)
+
+let configureServices (services:IServiceCollection) =
+ services
+ .AddSerilog()
+ .AddGiraffe()
+ |> ignore
+
+[]
+let main args =
+ let config = Config.load ()
+ configureLogger config
+
+ try
+ try
+ let builder = WebApplication.CreateBuilder(args)
+ configureServices builder.Services
+ let app = builder.Build()
+
+ if app.Environment.IsDevelopment() then
+ app.UseDeveloperExceptionPage() |> ignore
+
+ configureApp config app
+ Log.Information(
+ "Starting {AppName} version {ReleaseVersion} at commit {ReleaseCommit}",
+ config.appName,
+ config.release.version,
+ config.release.commit)
+
+ app.Run(config.serverUrl)
+ 0
+ with ex ->
+ Log.Fatal(ex, "Application start-up failed")
+ 1
+ finally
+ Log.CloseAndFlush()
diff --git a/sln/src/Docs/wwwroot/css/output.css b/sln/src/Docs/wwwroot/css/output.css
index e2af5df..4edf367 100644
--- a/sln/src/Docs/wwwroot/css/output.css
+++ b/sln/src/Docs/wwwroot/css/output.css
@@ -1,2 +1,2 @@
-/*! tailwindcss v4.1.11 | MIT License | https://tailwindcss.com */
-@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-space-y-reverse:0;--tw-space-x-reverse:0;--tw-border-style:solid;--tw-font-weight:initial;--tw-tracking:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-backdrop-blur:initial;--tw-backdrop-brightness:initial;--tw-backdrop-contrast:initial;--tw-backdrop-grayscale:initial;--tw-backdrop-hue-rotate:initial;--tw-backdrop-invert:initial;--tw-backdrop-opacity:initial;--tw-backdrop-saturate:initial;--tw-backdrop-sepia:initial;--tw-duration:initial;--tw-content:"";--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0}}}@layer theme{:root,:host{--font-sans:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--font-mono:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;--color-green-500:oklch(72.3% .219 149.579);--color-green-700:oklch(52.7% .154 150.069);--color-sky-500:oklch(68.5% .169 237.323);--color-blue-500:oklch(62.3% .214 259.815);--color-blue-600:oklch(54.6% .245 262.881);--color-blue-900:oklch(37.9% .146 265.522);--color-slate-100:oklch(96.8% .007 247.896);--color-slate-200:oklch(92.9% .013 255.508);--color-slate-300:oklch(86.9% .022 252.894);--color-slate-400:oklch(70.4% .04 256.788);--color-slate-500:oklch(55.4% .046 257.417);--color-slate-600:oklch(44.6% .043 257.281);--color-slate-700:oklch(37.2% .044 257.287);--color-slate-800:oklch(27.9% .041 260.031);--color-slate-900:oklch(20.8% .042 265.755);--color-slate-950:oklch(12.9% .042 264.695);--color-gray-100:oklch(96.7% .003 264.542);--color-gray-600:oklch(44.6% .03 256.802);--color-gray-900:oklch(21% .034 264.665);--color-zinc-300:oklch(87.1% .006 286.286);--color-zinc-400:oklch(70.5% .015 286.067);--color-zinc-500:oklch(55.2% .016 285.938);--color-zinc-600:oklch(44.2% .017 285.786);--color-zinc-900:oklch(21% .006 285.885);--color-black:#000;--color-white:#fff;--spacing:.25rem;--container-xs:20rem;--container-sm:24rem;--container-3xl:48rem;--text-sm:.875rem;--text-sm--line-height:calc(1.25/.875);--text-base:1rem;--text-base--line-height:calc(1.5/1);--text-lg:1.125rem;--text-lg--line-height:calc(1.75/1.125);--text-3xl:1.875rem;--text-3xl--line-height:calc(2.25/1.875);--font-weight-medium:500;--font-weight-semibold:600;--font-weight-bold:700;--tracking-wider:.05em;--radius-lg:.5rem;--radius-xl:.75rem;--blur-sm:8px;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4,0,.2,1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono)}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab, red, red)){::placeholder{color:color-mix(in oklab,currentcolor 50%,transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}}@layer components;@layer utilities{.collapse{visibility:collapse}.visible{visibility:visible}.absolute{position:absolute}.fixed{position:fixed}.relative{position:relative}.sticky{position:sticky}.inset-0{inset:calc(var(--spacing)*0)}.inset-y-0{inset-block:calc(var(--spacing)*0)}.top-0{top:calc(var(--spacing)*0)}.top-\[4\.75rem\]{top:4.75rem}.top-full{top:100%}.right-0{right:calc(var(--spacing)*0)}.left-0{left:calc(var(--spacing)*0)}.z-10{z-index:10}.z-50{z-index:50}.z-\[70\]{z-index:70}.container{width:100%}@media (min-width:40rem){.container{max-width:40rem}}@media (min-width:48rem){.container{max-width:48rem}}@media (min-width:64rem){.container{max-width:64rem}}@media (min-width:80rem){.container{max-width:80rem}}@media (min-width:96rem){.container{max-width:96rem}}.mx-auto{margin-inline:auto}.prose{color:var(--tw-prose-body);max-width:65ch}.prose :where(p):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:1.25em;margin-bottom:1.25em}.prose :where([class~=lead]):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-lead);margin-top:1.2em;margin-bottom:1.2em;font-size:1.25em;line-height:1.6}.prose :where(a):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-links);font-weight:500;text-decoration:underline}.prose :where(strong):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-bold);font-weight:600}.prose :where(a strong):not(:where([class~=not-prose],[class~=not-prose] *)),.prose :where(blockquote strong):not(:where([class~=not-prose],[class~=not-prose] *)),.prose :where(thead th strong):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit}.prose :where(ol):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:1.25em;margin-bottom:1.25em;padding-inline-start:1.625em;list-style-type:decimal}.prose :where(ol[type=A]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:upper-alpha}.prose :where(ol[type=a]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:lower-alpha}.prose :where(ol[type=A s]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:upper-alpha}.prose :where(ol[type=a s]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:lower-alpha}.prose :where(ol[type=I]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:upper-roman}.prose :where(ol[type=i]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:lower-roman}.prose :where(ol[type=I s]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:upper-roman}.prose :where(ol[type=i s]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:lower-roman}.prose :where(ol[type="1"]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:decimal}.prose :where(ul):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:1.25em;margin-bottom:1.25em;padding-inline-start:1.625em;list-style-type:disc}.prose :where(ol>li):not(:where([class~=not-prose],[class~=not-prose] *))::marker{color:var(--tw-prose-counters);font-weight:400}.prose :where(ul>li):not(:where([class~=not-prose],[class~=not-prose] *))::marker{color:var(--tw-prose-bullets)}.prose :where(dt):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-headings);margin-top:1.25em;font-weight:600}.prose :where(hr):not(:where([class~=not-prose],[class~=not-prose] *)){border-color:var(--tw-prose-hr);border-top-width:1px;margin-top:3em;margin-bottom:3em}.prose :where(blockquote):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-quotes);border-inline-start-width:.25rem;border-inline-start-color:var(--tw-prose-quote-borders);quotes:"“""”""‘""’";margin-top:1.6em;margin-bottom:1.6em;padding-inline-start:1em;font-style:italic;font-weight:500}.prose :where(blockquote p:first-of-type):not(:where([class~=not-prose],[class~=not-prose] *)):before{content:open-quote}.prose :where(blockquote p:last-of-type):not(:where([class~=not-prose],[class~=not-prose] *)):after{content:close-quote}.prose :where(h1):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-headings);margin-top:0;margin-bottom:.888889em;font-size:2.25em;font-weight:800;line-height:1.11111}.prose :where(h1 strong):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit;font-weight:900}.prose :where(h2):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-headings);margin-top:2em;margin-bottom:1em;font-size:1.5em;font-weight:700;line-height:1.33333}.prose :where(h2 strong):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit;font-weight:800}.prose :where(h3):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-headings);margin-top:1.6em;margin-bottom:.6em;font-size:1.25em;font-weight:600;line-height:1.6}.prose :where(h3 strong):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit;font-weight:700}.prose :where(h4):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-headings);margin-top:1.5em;margin-bottom:.5em;font-weight:600;line-height:1.5}.prose :where(h4 strong):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit;font-weight:700}.prose :where(img):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:2em;margin-bottom:2em}.prose :where(picture):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:2em;margin-bottom:2em;display:block}.prose :where(video):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:2em;margin-bottom:2em}.prose :where(kbd):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-kbd);box-shadow:0 0 0 1px rgb(var(--tw-prose-kbd-shadows)/10%),0 3px 0 rgb(var(--tw-prose-kbd-shadows)/10%);padding-top:.1875em;padding-inline-end:.375em;padding-bottom:.1875em;border-radius:.3125rem;padding-inline-start:.375em;font-family:inherit;font-size:.875em;font-weight:500}.prose :where(code):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-code);font-size:.875em;font-weight:600}.prose :where(code):not(:where([class~=not-prose],[class~=not-prose] *)):before,.prose :where(code):not(:where([class~=not-prose],[class~=not-prose] *)):after{content:"`"}.prose :where(a code):not(:where([class~=not-prose],[class~=not-prose] *)),.prose :where(h1 code):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit}.prose :where(h2 code):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit;font-size:.875em}.prose :where(h3 code):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit;font-size:.9em}.prose :where(h4 code):not(:where([class~=not-prose],[class~=not-prose] *)),.prose :where(blockquote code):not(:where([class~=not-prose],[class~=not-prose] *)),.prose :where(thead th code):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit}.prose :where(pre):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-pre-code);background-color:var(--tw-prose-pre-bg);padding-top:.857143em;padding-inline-end:1.14286em;padding-bottom:.857143em;border-radius:.375rem;margin-top:1.71429em;margin-bottom:1.71429em;padding-inline-start:1.14286em;font-size:.875em;font-weight:400;line-height:1.71429;overflow-x:auto}.prose :where(pre code):not(:where([class~=not-prose],[class~=not-prose] *)){font-weight:inherit;color:inherit;font-size:inherit;font-family:inherit;line-height:inherit;background-color:#0000;border-width:0;border-radius:0;padding:0}.prose :where(pre code):not(:where([class~=not-prose],[class~=not-prose] *)):before,.prose :where(pre code):not(:where([class~=not-prose],[class~=not-prose] *)):after{content:none}.prose :where(table):not(:where([class~=not-prose],[class~=not-prose] *)){table-layout:auto;width:100%;margin-top:2em;margin-bottom:2em;font-size:.875em;line-height:1.71429}.prose :where(thead):not(:where([class~=not-prose],[class~=not-prose] *)){border-bottom-width:1px;border-bottom-color:var(--tw-prose-th-borders)}.prose :where(thead th):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-headings);vertical-align:bottom;padding-inline-end:.571429em;padding-bottom:.571429em;padding-inline-start:.571429em;font-weight:600}.prose :where(tbody tr):not(:where([class~=not-prose],[class~=not-prose] *)){border-bottom-width:1px;border-bottom-color:var(--tw-prose-td-borders)}.prose :where(tbody tr:last-child):not(:where([class~=not-prose],[class~=not-prose] *)){border-bottom-width:0}.prose :where(tbody td):not(:where([class~=not-prose],[class~=not-prose] *)){vertical-align:baseline}.prose :where(tfoot):not(:where([class~=not-prose],[class~=not-prose] *)){border-top-width:1px;border-top-color:var(--tw-prose-th-borders)}.prose :where(tfoot td):not(:where([class~=not-prose],[class~=not-prose] *)){vertical-align:top}.prose :where(th,td):not(:where([class~=not-prose],[class~=not-prose] *)){text-align:start}.prose :where(figure>*):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:0;margin-bottom:0}.prose :where(figcaption):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-captions);margin-top:.857143em;font-size:.875em;line-height:1.42857}.prose{--tw-prose-body:oklch(37.3% .034 259.733);--tw-prose-headings:oklch(21% .034 264.665);--tw-prose-lead:oklch(44.6% .03 256.802);--tw-prose-links:oklch(21% .034 264.665);--tw-prose-bold:oklch(21% .034 264.665);--tw-prose-counters:oklch(55.1% .027 264.364);--tw-prose-bullets:oklch(87.2% .01 258.338);--tw-prose-hr:oklch(92.8% .006 264.531);--tw-prose-quotes:oklch(21% .034 264.665);--tw-prose-quote-borders:oklch(92.8% .006 264.531);--tw-prose-captions:oklch(55.1% .027 264.364);--tw-prose-kbd:oklch(21% .034 264.665);--tw-prose-kbd-shadows:NaN NaN NaN;--tw-prose-code:oklch(21% .034 264.665);--tw-prose-pre-code:oklch(92.8% .006 264.531);--tw-prose-pre-bg:oklch(27.8% .033 256.848);--tw-prose-th-borders:oklch(87.2% .01 258.338);--tw-prose-td-borders:oklch(92.8% .006 264.531);--tw-prose-invert-body:oklch(87.2% .01 258.338);--tw-prose-invert-headings:#fff;--tw-prose-invert-lead:oklch(70.7% .022 261.325);--tw-prose-invert-links:#fff;--tw-prose-invert-bold:#fff;--tw-prose-invert-counters:oklch(70.7% .022 261.325);--tw-prose-invert-bullets:oklch(44.6% .03 256.802);--tw-prose-invert-hr:oklch(37.3% .034 259.733);--tw-prose-invert-quotes:oklch(96.7% .003 264.542);--tw-prose-invert-quote-borders:oklch(37.3% .034 259.733);--tw-prose-invert-captions:oklch(70.7% .022 261.325);--tw-prose-invert-kbd:#fff;--tw-prose-invert-kbd-shadows:255 255 255;--tw-prose-invert-code:#fff;--tw-prose-invert-pre-code:oklch(87.2% .01 258.338);--tw-prose-invert-pre-bg:#00000080;--tw-prose-invert-th-borders:oklch(44.6% .03 256.802);--tw-prose-invert-td-borders:oklch(37.3% .034 259.733);font-size:1rem;line-height:1.75}.prose :where(picture>img):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:0;margin-bottom:0}.prose :where(li):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:.5em;margin-bottom:.5em}.prose :where(ol>li):not(:where([class~=not-prose],[class~=not-prose] *)),.prose :where(ul>li):not(:where([class~=not-prose],[class~=not-prose] *)){padding-inline-start:.375em}.prose :where(.prose>ul>li p):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:.75em;margin-bottom:.75em}.prose :where(.prose>ul>li>p:first-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:1.25em}.prose :where(.prose>ul>li>p:last-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-bottom:1.25em}.prose :where(.prose>ol>li>p:first-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:1.25em}.prose :where(.prose>ol>li>p:last-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-bottom:1.25em}.prose :where(ul ul,ul ol,ol ul,ol ol):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:.75em;margin-bottom:.75em}.prose :where(dl):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:1.25em;margin-bottom:1.25em}.prose :where(dd):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:.5em;padding-inline-start:1.625em}.prose :where(hr+*):not(:where([class~=not-prose],[class~=not-prose] *)),.prose :where(h2+*):not(:where([class~=not-prose],[class~=not-prose] *)),.prose :where(h3+*):not(:where([class~=not-prose],[class~=not-prose] *)),.prose :where(h4+*):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:0}.prose :where(thead th:first-child):not(:where([class~=not-prose],[class~=not-prose] *)){padding-inline-start:0}.prose :where(thead th:last-child):not(:where([class~=not-prose],[class~=not-prose] *)){padding-inline-end:0}.prose :where(tbody td,tfoot td):not(:where([class~=not-prose],[class~=not-prose] *)){padding-top:.571429em;padding-inline-end:.571429em;padding-bottom:.571429em;padding-inline-start:.571429em}.prose :where(tbody td:first-child,tfoot td:first-child):not(:where([class~=not-prose],[class~=not-prose] *)){padding-inline-start:0}.prose :where(tbody td:last-child,tfoot td:last-child):not(:where([class~=not-prose],[class~=not-prose] *)){padding-inline-end:0}.prose :where(figure):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:2em;margin-bottom:2em}.prose :where(.prose>:first-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:0}.prose :where(.prose>:last-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-bottom:0}.mt-2{margin-top:calc(var(--spacing)*2)}.mt-3{margin-top:calc(var(--spacing)*3)}.mt-4{margin-top:calc(var(--spacing)*4)}.-mr-6{margin-right:calc(var(--spacing)*-6)}.mb-4{margin-bottom:calc(var(--spacing)*4)}.mb-6{margin-bottom:calc(var(--spacing)*6)}.mb-8{margin-bottom:calc(var(--spacing)*8)}.-ml-0\.5{margin-left:calc(var(--spacing)*-.5)}.block{display:block}.flex{display:flex}.hidden{display:none}.inline{display:inline}.size-4{width:calc(var(--spacing)*4);height:calc(var(--spacing)*4)}.h-5{height:calc(var(--spacing)*5)}.h-6{height:calc(var(--spacing)*6)}.h-8{height:calc(var(--spacing)*8)}.h-\[calc\(100vh-4\.75rem\)\]{height:calc(100vh - 4.75rem)}.h-full{height:100%}.min-h-full{min-height:100%}.w-5{width:calc(var(--spacing)*5)}.w-6{width:calc(var(--spacing)*6)}.w-8{width:calc(var(--spacing)*8)}.w-36{width:calc(var(--spacing)*36)}.w-48{width:calc(var(--spacing)*48)}.w-56{width:calc(var(--spacing)*56)}.w-64{width:calc(var(--spacing)*64)}.w-full{width:100%}.max-w-3xl{max-width:var(--container-3xl)}.max-w-none{max-width:none}.max-w-sm{max-width:var(--container-sm)}.max-w-xs{max-width:var(--container-xs)}.min-w-0{min-width:calc(var(--spacing)*0)}.flex-auto{flex:auto}.flex-none{flex:none}.basis-0{flex-basis:calc(var(--spacing)*0)}.transform{transform:var(--tw-rotate-x,)var(--tw-rotate-y,)var(--tw-rotate-z,)var(--tw-skew-x,)var(--tw-skew-y,)}.cursor-pointer{cursor:pointer}.flex-wrap{flex-wrap:wrap}.items-center{align-items:center}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.justify-end{justify-content:flex-end}.gap-2{gap:calc(var(--spacing)*2)}.gap-4{gap:calc(var(--spacing)*4)}.gap-6{gap:calc(var(--spacing)*6)}:where(.space-y-2>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*2)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*2)*calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-3>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*3)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*3)*calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-9>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*9)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*9)*calc(1 - var(--tw-space-y-reverse)))}:where(.space-x-1>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing)*1)*var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing)*1)*calc(1 - var(--tw-space-x-reverse)))}.overflow-hidden{overflow:hidden}.overflow-y-auto{overflow-y:auto}.rounded{border-radius:.25rem}.rounded-lg{border-radius:var(--radius-lg)}.rounded-xl{border-radius:var(--radius-xl)}.border-l-2{border-left-style:var(--tw-border-style);border-left-width:2px}.border-slate-100{border-color:var(--color-slate-100)}.bg-black\/30{background-color:#0000004d}@supports (color:color-mix(in lab, red, red)){.bg-black\/30{background-color:color-mix(in oklab,var(--color-black)30%,transparent)}}.bg-blue-500{background-color:var(--color-blue-500)}.bg-blue-900\/20{background-color:#1c398e33}@supports (color:color-mix(in lab, red, red)){.bg-blue-900\/20{background-color:color-mix(in oklab,var(--color-blue-900)20%,transparent)}}.bg-gray-100{background-color:var(--color-gray-100)}.bg-green-500{background-color:var(--color-green-500)}.bg-slate-950\/60{background-color:#02061899}@supports (color:color-mix(in lab, red, red)){.bg-slate-950\/60{background-color:color-mix(in oklab,var(--color-slate-950)60%,transparent)}}.bg-white{background-color:var(--color-white)}.bg-white\/75{background-color:#ffffffbf}@supports (color:color-mix(in lab, red, red)){.bg-white\/75{background-color:color-mix(in oklab,var(--color-white)75%,transparent)}}.fill-slate-400{fill:var(--color-slate-400)}.stroke-sky-500{stroke:var(--color-sky-500)}.p-1{padding:calc(var(--spacing)*1)}.p-2{padding:calc(var(--spacing)*2)}.p-4{padding:calc(var(--spacing)*4)}.p-6{padding:calc(var(--spacing)*6)}.p-8{padding:calc(var(--spacing)*8)}.px-3{padding-inline:calc(var(--spacing)*3)}.px-4{padding-inline:calc(var(--spacing)*4)}.px-6{padding-inline:calc(var(--spacing)*6)}.py-1{padding-block:calc(var(--spacing)*1)}.py-2{padding-block:calc(var(--spacing)*2)}.py-5{padding-block:calc(var(--spacing)*5)}.py-8{padding-block:calc(var(--spacing)*8)}.py-16{padding-block:calc(var(--spacing)*16)}.pt-6{padding-top:calc(var(--spacing)*6)}.pr-6{padding-right:calc(var(--spacing)*6)}.pr-8{padding-right:calc(var(--spacing)*8)}.pb-12{padding-bottom:calc(var(--spacing)*12)}.pl-0\.5{padding-left:calc(var(--spacing)*.5)}.pl-3\.5{padding-left:calc(var(--spacing)*3.5)}.font-sans{font-family:var(--font-sans)}.text-3xl{font-size:var(--text-3xl);line-height:var(--tw-leading,var(--text-3xl--line-height))}.text-base{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}.text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.font-bold{--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold)}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.tracking-wider{--tw-tracking:var(--tracking-wider);letter-spacing:var(--tracking-wider)}.text-blue-600{color:var(--color-blue-600)}.text-gray-600{color:var(--color-gray-600)}.text-gray-900{color:var(--color-gray-900)}.text-sky-500{color:var(--color-sky-500)}.text-slate-500{color:var(--color-slate-500)}.text-slate-700{color:var(--color-slate-700)}.text-slate-900{color:var(--color-slate-900)}.text-white{color:var(--color-white)}.text-zinc-500{color:var(--color-zinc-500)}.text-zinc-900{color:var(--color-zinc-900)}.antialiased{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.shadow-2xl{--tw-shadow:0 25px 50px -12px var(--tw-shadow-color,#00000040);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-lg{--tw-shadow:0 10px 15px -3px var(--tw-shadow-color,#0000001a),0 4px 6px -4px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-md{--tw-shadow:0 4px 6px -1px var(--tw-shadow-color,#0000001a),0 2px 4px -2px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.ring-1{--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(1px + var(--tw-ring-offset-width))var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-slate-900\/5{--tw-shadow-color:#0f172b0d}@supports (color:color-mix(in lab, red, red)){.shadow-slate-900\/5{--tw-shadow-color:color-mix(in oklab,color-mix(in oklab,var(--color-slate-900)5%,transparent)var(--tw-shadow-alpha),transparent)}}.ring-slate-900\/10{--tw-ring-color:#0f172b1a}@supports (color:color-mix(in lab, red, red)){.ring-slate-900\/10{--tw-ring-color:color-mix(in oklab,var(--color-slate-900)10%,transparent)}}.backdrop-blur{--tw-backdrop-blur:blur(8px);-webkit-backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,)}.backdrop-blur-sm{--tw-backdrop-blur:blur(var(--blur-sm));-webkit-backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,)}.transition{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter,display,visibility,content-visibility,overlay,pointer-events;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-colors{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.duration-500{--tw-duration:.5s;transition-duration:.5s}.prose-slate{--tw-prose-body:oklch(37.2% .044 257.287);--tw-prose-headings:oklch(20.8% .042 265.755);--tw-prose-lead:oklch(44.6% .043 257.281);--tw-prose-links:oklch(20.8% .042 265.755);--tw-prose-bold:oklch(20.8% .042 265.755);--tw-prose-counters:oklch(55.4% .046 257.417);--tw-prose-bullets:oklch(86.9% .022 252.894);--tw-prose-hr:oklch(92.9% .013 255.508);--tw-prose-quotes:oklch(20.8% .042 265.755);--tw-prose-quote-borders:oklch(92.9% .013 255.508);--tw-prose-captions:oklch(55.4% .046 257.417);--tw-prose-kbd:oklch(20.8% .042 265.755);--tw-prose-kbd-shadows:NaN NaN NaN;--tw-prose-code:oklch(20.8% .042 265.755);--tw-prose-pre-code:oklch(92.9% .013 255.508);--tw-prose-pre-bg:oklch(27.9% .041 260.031);--tw-prose-th-borders:oklch(86.9% .022 252.894);--tw-prose-td-borders:oklch(92.9% .013 255.508);--tw-prose-invert-body:oklch(86.9% .022 252.894);--tw-prose-invert-headings:#fff;--tw-prose-invert-lead:oklch(70.4% .04 256.788);--tw-prose-invert-links:#fff;--tw-prose-invert-bold:#fff;--tw-prose-invert-counters:oklch(70.4% .04 256.788);--tw-prose-invert-bullets:oklch(44.6% .043 257.281);--tw-prose-invert-hr:oklch(37.2% .044 257.287);--tw-prose-invert-quotes:oklch(96.8% .007 247.896);--tw-prose-invert-quote-borders:oklch(37.2% .044 257.287);--tw-prose-invert-captions:oklch(70.4% .04 256.788);--tw-prose-invert-kbd:#fff;--tw-prose-invert-kbd-shadows:255 255 255;--tw-prose-invert-code:#fff;--tw-prose-invert-pre-code:oklch(86.9% .022 252.894);--tw-prose-invert-pre-bg:#00000080;--tw-prose-invert-th-borders:oklch(44.6% .043 257.281);--tw-prose-invert-td-borders:oklch(37.2% .044 257.287)}@media (hover:hover){.group-hover\:fill-slate-500:is(:where(.group):hover *){fill:var(--color-slate-500)}}.before\:pointer-events-none:before{content:var(--tw-content);pointer-events:none}.before\:absolute:before{content:var(--tw-content);position:absolute}.before\:top-1\/2:before{content:var(--tw-content);top:50%}.before\:-left-1:before{content:var(--tw-content);left:calc(var(--spacing)*-1)}.before\:hidden:before{content:var(--tw-content);display:none}.before\:h-1\.5:before{content:var(--tw-content);height:calc(var(--spacing)*1.5)}.before\:w-1\.5:before{content:var(--tw-content);width:calc(var(--spacing)*1.5)}.before\:-translate-y-1\/2:before{content:var(--tw-content);--tw-translate-y:calc(calc(1/2*100%)*-1);translate:var(--tw-translate-x)var(--tw-translate-y)}.before\:rounded-full:before{content:var(--tw-content);border-radius:3.40282e38px}.before\:bg-sky-500:before{content:var(--tw-content);background-color:var(--color-sky-500)}.before\:bg-slate-300:before{content:var(--tw-content);background-color:var(--color-slate-300)}@media (hover:hover){.hover\:bg-green-700:hover{background-color:var(--color-green-700)}.hover\:bg-slate-100:hover{background-color:var(--color-slate-100)}.hover\:text-slate-600:hover{color:var(--color-slate-600)}.hover\:text-zinc-600:hover{color:var(--color-zinc-600)}.hover\:before\:block:hover:before{content:var(--tw-content);display:block}}@media (min-width:40rem){.sm\:gap-8{gap:calc(var(--spacing)*8)}.sm\:px-2{padding-inline:calc(var(--spacing)*2)}.sm\:px-6{padding-inline:calc(var(--spacing)*6)}}@media (min-width:48rem){.md\:grow{flex-grow:1}}@media (min-width:64rem){.lg\:relative{position:relative}.lg\:mt-4{margin-top:calc(var(--spacing)*4)}.lg\:block{display:block}.lg\:hidden{display:none}.lg\:max-w-none{max-width:none}.lg\:flex-none{flex:none}:where(.lg\:space-y-4>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*4)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*4)*calc(1 - var(--tw-space-y-reverse)))}.lg\:border-slate-200{border-color:var(--color-slate-200)}.lg\:px-8{padding-inline:calc(var(--spacing)*8)}.lg\:pr-0{padding-right:calc(var(--spacing)*0)}.lg\:pl-8{padding-left:calc(var(--spacing)*8)}.lg\:text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}}@media (min-width:80rem){.xl\:sticky{position:sticky}.xl\:top-\[4\.75rem\]{top:4.75rem}.xl\:-mr-6{margin-right:calc(var(--spacing)*-6)}.xl\:block{display:block}.xl\:h-\[calc\(100vh-4\.75rem\)\]{height:calc(100vh - 4.75rem)}.xl\:w-72{width:calc(var(--spacing)*72)}.xl\:flex-none{flex:none}.xl\:overflow-y-auto{overflow-y:auto}.xl\:px-12{padding-inline:calc(var(--spacing)*12)}.xl\:px-16{padding-inline:calc(var(--spacing)*16)}.xl\:py-12{padding-block:calc(var(--spacing)*12)}.xl\:pr-6{padding-right:calc(var(--spacing)*6)}.xl\:pr-16{padding-right:calc(var(--spacing)*16)}}.dark\:block:where(.dark,.dark *){display:block}.dark\:hidden:where(.dark,.dark *){display:none}.dark\:border-slate-800:where(.dark,.dark *){border-color:var(--color-slate-800)}.dark\:bg-slate-800:where(.dark,.dark *){background-color:var(--color-slate-800)}.dark\:bg-slate-900:where(.dark,.dark *){background-color:var(--color-slate-900)}.dark\:bg-slate-900\/75:where(.dark,.dark *){background-color:#0f172bbf}@supports (color:color-mix(in lab, red, red)){.dark\:bg-slate-900\/75:where(.dark,.dark *){background-color:color-mix(in oklab,var(--color-slate-900)75%,transparent)}}.dark\:text-slate-300:where(.dark,.dark *){color:var(--color-slate-300)}.dark\:text-slate-400:where(.dark,.dark *){color:var(--color-slate-400)}.dark\:text-white:where(.dark,.dark *){color:var(--color-white)}.dark\:text-zinc-400:where(.dark,.dark *){color:var(--color-zinc-400)}.dark\:shadow-none:where(.dark,.dark *){--tw-shadow:0 0 #0000;box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.dark\:ring-0:where(.dark,.dark *){--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(0px + var(--tw-ring-offset-width))var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.dark\:ring-white\/10:where(.dark,.dark *){--tw-ring-color:#ffffff1a}@supports (color:color-mix(in lab, red, red)){.dark\:ring-white\/10:where(.dark,.dark *){--tw-ring-color:color-mix(in oklab,var(--color-white)10%,transparent)}}.dark\:backdrop-blur:where(.dark,.dark *){--tw-backdrop-blur:blur(8px);-webkit-backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,)}.dark\:prose-invert:where(.dark,.dark *){--tw-prose-body:var(--tw-prose-invert-body);--tw-prose-headings:var(--tw-prose-invert-headings);--tw-prose-lead:var(--tw-prose-invert-lead);--tw-prose-links:var(--tw-prose-invert-links);--tw-prose-bold:var(--tw-prose-invert-bold);--tw-prose-counters:var(--tw-prose-invert-counters);--tw-prose-bullets:var(--tw-prose-invert-bullets);--tw-prose-hr:var(--tw-prose-invert-hr);--tw-prose-quotes:var(--tw-prose-invert-quotes);--tw-prose-quote-borders:var(--tw-prose-invert-quote-borders);--tw-prose-captions:var(--tw-prose-invert-captions);--tw-prose-kbd:var(--tw-prose-invert-kbd);--tw-prose-kbd-shadows:var(--tw-prose-invert-kbd-shadows);--tw-prose-code:var(--tw-prose-invert-code);--tw-prose-pre-code:var(--tw-prose-invert-pre-code);--tw-prose-pre-bg:var(--tw-prose-invert-pre-bg);--tw-prose-th-borders:var(--tw-prose-invert-th-borders);--tw-prose-td-borders:var(--tw-prose-invert-td-borders)}@media (hover:hover){.dark\:group-hover\:fill-slate-300:where(.dark,.dark *):is(:where(.group):hover *){fill:var(--color-slate-300)}}.dark\:before\:bg-slate-700:where(.dark,.dark *):before{content:var(--tw-content);background-color:var(--color-slate-700)}@media (hover:hover){.dark\:hover\:bg-slate-700:where(.dark,.dark *):hover{background-color:var(--color-slate-700)}.dark\:hover\:bg-slate-700\/50:where(.dark,.dark *):hover{background-color:#31415880}@supports (color:color-mix(in lab, red, red)){.dark\:hover\:bg-slate-700\/50:where(.dark,.dark *):hover{background-color:color-mix(in oklab,var(--color-slate-700)50%,transparent)}}.dark\:hover\:text-slate-300:where(.dark,.dark *):hover{color:var(--color-slate-300)}.dark\:hover\:text-zinc-300:where(.dark,.dark *):hover{color:var(--color-zinc-300)}}.\[\&_h1\]\:scroll-mt-28 h1,.\[\&_h2\]\:scroll-mt-28 h2,.\[\&_h3\]\:scroll-mt-28 h3{scroll-margin-top:calc(var(--spacing)*28)}}[x-cloak]{display:none!important}.prose pre{background-color:#0f172a;border:1px solid #1e293b;border-radius:.75rem;position:relative;overflow-x:auto;box-shadow:0 20px 25px -5px #0000001a,0 8px 10px -6px #0000001a}.prose pre code{color:#e2e8f0;border-radius:0;padding:0;font-family:Fira Code,JetBrains Mono,Monaco,Cascadia Code,Segoe UI Mono,Roboto Mono,Oxygen Mono,Ubuntu Monospace,Source Code Pro,Fira Mono,Droid Sans Mono,Courier New,monospace;font-size:.875rem;line-height:1.7;background-color:#0000!important}.prose pre code .hljs-keyword{color:#a855f7}.prose pre code .hljs-string{color:#22c55e}.prose pre code .hljs-comment{color:#94a3b8;font-style:italic}.prose pre code .hljs-function{color:#3b82f6}.prose pre code .hljs-number{color:#fb923c}.prose :not(pre)>code{color:#e2e8f0!important;background-color:#334155!important;border-radius:.375rem!important;padding:.25rem .5rem!important;font-size:.875rem!important;font-weight:500!important}.prose code:before,.prose code:after{content:none!important}@property --tw-rotate-x{syntax:"*";inherits:false}@property --tw-rotate-y{syntax:"*";inherits:false}@property --tw-rotate-z{syntax:"*";inherits:false}@property --tw-skew-x{syntax:"*";inherits:false}@property --tw-skew-y{syntax:"*";inherits:false}@property --tw-space-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-space-x-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-tracking{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-backdrop-blur{syntax:"*";inherits:false}@property --tw-backdrop-brightness{syntax:"*";inherits:false}@property --tw-backdrop-contrast{syntax:"*";inherits:false}@property --tw-backdrop-grayscale{syntax:"*";inherits:false}@property --tw-backdrop-hue-rotate{syntax:"*";inherits:false}@property --tw-backdrop-invert{syntax:"*";inherits:false}@property --tw-backdrop-opacity{syntax:"*";inherits:false}@property --tw-backdrop-saturate{syntax:"*";inherits:false}@property --tw-backdrop-sepia{syntax:"*";inherits:false}@property --tw-duration{syntax:"*";inherits:false}@property --tw-content{syntax:"*";inherits:false;initial-value:""}@property --tw-translate-x{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-y{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-z{syntax:"*";inherits:false;initial-value:0}
\ No newline at end of file
+/*! tailwindcss v4.3.3 | MIT License | https://tailwindcss.com */
+@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-scale-x:1;--tw-scale-y:1;--tw-scale-z:1;--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-space-y-reverse:0;--tw-divide-y-reverse:0;--tw-border-style:solid;--tw-font-weight:initial;--tw-tracking:initial;--tw-ordinal:initial;--tw-slashed-zero:initial;--tw-numeric-figure:initial;--tw-numeric-spacing:initial;--tw-numeric-fraction:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-backdrop-blur:initial;--tw-backdrop-brightness:initial;--tw-backdrop-contrast:initial;--tw-backdrop-grayscale:initial;--tw-backdrop-hue-rotate:initial;--tw-backdrop-invert:initial;--tw-backdrop-opacity:initial;--tw-backdrop-saturate:initial;--tw-backdrop-sepia:initial;--tw-duration:initial;--tw-content:"";--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0}}}@layer theme{:root,:host{--font-sans:-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";--font-mono:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;--color-sky-400:oklch(74.6% .16 232.661);--color-sky-500:oklch(68.5% .169 237.323);--color-sky-600:oklch(58.8% .158 241.966);--color-blue-500:oklch(62.3% .214 259.815);--color-blue-600:oklch(54.6% .245 262.881);--color-slate-50:oklch(98.4% .003 247.858);--color-slate-100:oklch(96.8% .007 247.896);--color-slate-200:oklch(92.9% .013 255.508);--color-slate-300:oklch(86.9% .022 252.894);--color-slate-400:oklch(70.4% .04 256.788);--color-slate-500:oklch(55.4% .046 257.417);--color-slate-600:oklch(44.6% .043 257.281);--color-slate-700:oklch(37.2% .044 257.287);--color-slate-800:oklch(27.9% .041 260.031);--color-slate-900:oklch(20.8% .042 265.755);--color-slate-950:oklch(12.9% .042 264.695);--color-gray-100:oklch(96.7% .003 264.542);--color-zinc-300:oklch(87.1% .006 286.286);--color-zinc-400:oklch(70.5% .015 286.067);--color-zinc-500:oklch(55.2% .016 285.938);--color-zinc-600:oklch(44.2% .017 285.786);--color-zinc-900:oklch(21% .006 285.885);--color-black:#000;--color-white:#fff;--spacing:.25rem;--container-xs:20rem;--container-3xl:48rem;--text-xs:.75rem;--text-xs--line-height:calc(1 / .75);--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--text-base:1rem;--text-base--line-height:calc(1.5 / 1);--text-3xl:1.875rem;--text-3xl--line-height:calc(2.25 / 1.875);--font-weight-medium:500;--font-weight-semibold:600;--font-weight-bold:700;--tracking-wider:.05em;--radius-lg:.5rem;--radius-xl:.75rem;--blur-sm:8px;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono)}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring:where(:not(iframe)){outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab, red, red)){::placeholder{color:color-mix(in oklab, currentcolor 50%, transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}}@layer components;@layer utilities{.collapse{visibility:collapse}.visible{visibility:visible}.absolute{position:absolute}.fixed{position:fixed}.relative{position:relative}.static{position:static}.sticky{position:sticky}.inset-0{inset:0}.inset-y-0{inset-block:0}.top-0{top:0}.top-\[4\.75rem\]{top:4.75rem}.top-full{top:100%}.right-0{right:0}.left-0{left:0}.z-10{z-index:10}.z-50{z-index:50}.z-\[70\]{z-index:70}.container{width:100%}@media (min-width:40rem){.container{max-width:40rem}}@media (min-width:48rem){.container{max-width:48rem}}@media (min-width:64rem){.container{max-width:64rem}}@media (min-width:80rem){.container{max-width:80rem}}@media (min-width:96rem){.container{max-width:96rem}}.mx-auto{margin-inline:auto}.my-8{margin-block:calc(var(--spacing) * 8)}.prose{color:var(--tw-prose-body);max-width:65ch}.prose :where(p):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:1.25em;margin-bottom:1.25em}.prose :where([class~=lead]):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-lead);margin-top:1.2em;margin-bottom:1.2em;font-size:1.25em;line-height:1.6}.prose :where(a):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-links);font-weight:500;text-decoration:underline}.prose :where(strong):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-bold);font-weight:600}.prose :where(a strong):not(:where([class~=not-prose],[class~=not-prose] *)),.prose :where(blockquote strong):not(:where([class~=not-prose],[class~=not-prose] *)),.prose :where(thead th strong):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit}.prose :where(ol):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:1.25em;margin-bottom:1.25em;padding-inline-start:1.625em;list-style-type:decimal}.prose :where(ol[type=A]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:upper-alpha}.prose :where(ol[type=a]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:lower-alpha}.prose :where(ol[type=A s]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:upper-alpha}.prose :where(ol[type=a s]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:lower-alpha}.prose :where(ol[type=I]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:upper-roman}.prose :where(ol[type=i]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:lower-roman}.prose :where(ol[type=I s]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:upper-roman}.prose :where(ol[type=i s]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:lower-roman}.prose :where(ol[type="1"]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:decimal}.prose :where(ul):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:1.25em;margin-bottom:1.25em;padding-inline-start:1.625em;list-style-type:disc}.prose :where(ol>li):not(:where([class~=not-prose],[class~=not-prose] *))::marker{color:var(--tw-prose-counters);font-weight:400}.prose :where(ul>li):not(:where([class~=not-prose],[class~=not-prose] *))::marker{color:var(--tw-prose-bullets)}.prose :where(dt):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-headings);margin-top:1.25em;font-weight:600}.prose :where(hr):not(:where([class~=not-prose],[class~=not-prose] *)){border-color:var(--tw-prose-hr);border-top-width:1px;margin-top:3em;margin-bottom:3em}.prose :where(blockquote):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-quotes);border-inline-start-width:.25rem;border-inline-start-color:var(--tw-prose-quote-borders);quotes:"“""”""‘""’";margin-top:1.6em;margin-bottom:1.6em;padding-inline-start:1em;font-style:italic;font-weight:500}.prose :where(blockquote p:first-of-type):not(:where([class~=not-prose],[class~=not-prose] *)):before{content:open-quote}.prose :where(blockquote p:last-of-type):not(:where([class~=not-prose],[class~=not-prose] *)):after{content:close-quote}.prose :where(h1):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-headings);margin-top:0;margin-bottom:.888889em;font-size:2.25em;font-weight:800;line-height:1.11111}.prose :where(h1 strong):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit;font-weight:900}.prose :where(h2):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-headings);margin-top:2em;margin-bottom:1em;font-size:1.5em;font-weight:700;line-height:1.33333}.prose :where(h2 strong):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit;font-weight:800}.prose :where(h3):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-headings);margin-top:1.6em;margin-bottom:.6em;font-size:1.25em;font-weight:600;line-height:1.6}.prose :where(h3 strong):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit;font-weight:700}.prose :where(h4):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-headings);margin-top:1.5em;margin-bottom:.5em;font-weight:600;line-height:1.5}.prose :where(h4 strong):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit;font-weight:700}.prose :where(img):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:2em;margin-bottom:2em}.prose :where(picture):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:2em;margin-bottom:2em;display:block}.prose :where(video):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:2em;margin-bottom:2em}.prose :where(kbd):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-kbd);box-shadow:0 0 0 1px var(--tw-prose-kbd-shadows), 0 3px 0 var(--tw-prose-kbd-shadows);padding-top:.1875em;padding-inline-end:.375em;padding-bottom:.1875em;border-radius:.3125rem;padding-inline-start:.375em;font-family:inherit;font-size:.875em;font-weight:500}.prose :where(code):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-code);font-size:.875em;font-weight:600}.prose :where(code):not(:where([class~=not-prose],[class~=not-prose] *)):before,.prose :where(code):not(:where([class~=not-prose],[class~=not-prose] *)):after{content:"`"}.prose :where(a code):not(:where([class~=not-prose],[class~=not-prose] *)),.prose :where(h1 code):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit}.prose :where(h2 code):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit;font-size:.875em}.prose :where(h3 code):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit;font-size:.9em}.prose :where(h4 code):not(:where([class~=not-prose],[class~=not-prose] *)),.prose :where(blockquote code):not(:where([class~=not-prose],[class~=not-prose] *)),.prose :where(thead th code):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit}.prose :where(pre):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-pre-code);background-color:var(--tw-prose-pre-bg);padding-top:.857143em;padding-inline-end:1.14286em;padding-bottom:.857143em;border-radius:.375rem;margin-top:1.71429em;margin-bottom:1.71429em;padding-inline-start:1.14286em;font-size:.875em;font-weight:400;line-height:1.71429;overflow-x:auto}.prose :where(pre code):not(:where([class~=not-prose],[class~=not-prose] *)){font-weight:inherit;color:inherit;font-size:inherit;font-family:inherit;line-height:inherit;background-color:#0000;border-width:0;border-radius:0;padding:0}.prose :where(pre code):not(:where([class~=not-prose],[class~=not-prose] *)):before,.prose :where(pre code):not(:where([class~=not-prose],[class~=not-prose] *)):after{content:none}.prose :where(table):not(:where([class~=not-prose],[class~=not-prose] *)){table-layout:auto;width:100%;margin-top:2em;margin-bottom:2em;font-size:.875em;line-height:1.71429}.prose :where(thead):not(:where([class~=not-prose],[class~=not-prose] *)){border-bottom-width:1px;border-bottom-color:var(--tw-prose-th-borders)}.prose :where(thead th):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-headings);vertical-align:bottom;padding-inline-end:.571429em;padding-bottom:.571429em;padding-inline-start:.571429em;font-weight:600}.prose :where(tbody tr):not(:where([class~=not-prose],[class~=not-prose] *)){border-bottom-width:1px;border-bottom-color:var(--tw-prose-td-borders)}.prose :where(tbody tr:last-child):not(:where([class~=not-prose],[class~=not-prose] *)){border-bottom-width:0}.prose :where(tbody td):not(:where([class~=not-prose],[class~=not-prose] *)){vertical-align:baseline}.prose :where(tfoot):not(:where([class~=not-prose],[class~=not-prose] *)){border-top-width:1px;border-top-color:var(--tw-prose-th-borders)}.prose :where(tfoot td):not(:where([class~=not-prose],[class~=not-prose] *)){vertical-align:top}.prose :where(th,td):not(:where([class~=not-prose],[class~=not-prose] *)){text-align:start}.prose :where(figure>*):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:0;margin-bottom:0}.prose :where(figcaption):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-captions);margin-top:.857143em;font-size:.875em;line-height:1.42857}.prose{--tw-prose-body:oklch(37.3% .034 259.733);--tw-prose-headings:oklch(21% .034 264.665);--tw-prose-lead:oklch(44.6% .03 256.802);--tw-prose-links:oklch(21% .034 264.665);--tw-prose-bold:oklch(21% .034 264.665);--tw-prose-counters:oklch(55.1% .027 264.364);--tw-prose-bullets:oklch(87.2% .01 258.338);--tw-prose-hr:oklch(92.8% .006 264.531);--tw-prose-quotes:oklch(21% .034 264.665);--tw-prose-quote-borders:oklch(92.8% .006 264.531);--tw-prose-captions:oklch(55.1% .027 264.364);--tw-prose-kbd:oklch(21% .034 264.665);--tw-prose-kbd-shadows:oklab(21% -.00316127 -.0338527/.1);--tw-prose-code:oklch(21% .034 264.665);--tw-prose-pre-code:oklch(92.8% .006 264.531);--tw-prose-pre-bg:oklch(27.8% .033 256.848);--tw-prose-th-borders:oklch(87.2% .01 258.338);--tw-prose-td-borders:oklch(92.8% .006 264.531);--tw-prose-invert-body:oklch(87.2% .01 258.338);--tw-prose-invert-headings:#fff;--tw-prose-invert-lead:oklch(70.7% .022 261.325);--tw-prose-invert-links:#fff;--tw-prose-invert-bold:#fff;--tw-prose-invert-counters:oklch(70.7% .022 261.325);--tw-prose-invert-bullets:oklch(44.6% .03 256.802);--tw-prose-invert-hr:oklch(37.3% .034 259.733);--tw-prose-invert-quotes:oklch(96.7% .003 264.542);--tw-prose-invert-quote-borders:oklch(37.3% .034 259.733);--tw-prose-invert-captions:oklch(70.7% .022 261.325);--tw-prose-invert-kbd:#fff;--tw-prose-invert-kbd-shadows:#ffffff1a;--tw-prose-invert-code:#fff;--tw-prose-invert-pre-code:oklch(87.2% .01 258.338);--tw-prose-invert-pre-bg:#00000080;--tw-prose-invert-th-borders:oklch(44.6% .03 256.802);--tw-prose-invert-td-borders:oklch(37.3% .034 259.733);font-size:1rem;line-height:1.75}.prose :where(picture>img):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:0;margin-bottom:0}.prose :where(li):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:.5em;margin-bottom:.5em}.prose :where(ol>li):not(:where([class~=not-prose],[class~=not-prose] *)),.prose :where(ul>li):not(:where([class~=not-prose],[class~=not-prose] *)){padding-inline-start:.375em}.prose :where(.prose>ul>li p):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:.75em;margin-bottom:.75em}.prose :where(.prose>ul>li>p:first-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:1.25em}.prose :where(.prose>ul>li>p:last-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-bottom:1.25em}.prose :where(.prose>ol>li>p:first-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:1.25em}.prose :where(.prose>ol>li>p:last-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-bottom:1.25em}.prose :where(ul ul,ul ol,ol ul,ol ol):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:.75em;margin-bottom:.75em}.prose :where(dl):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:1.25em;margin-bottom:1.25em}.prose :where(dd):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:.5em;padding-inline-start:1.625em}.prose :where(hr+*):not(:where([class~=not-prose],[class~=not-prose] *)),.prose :where(h2+*):not(:where([class~=not-prose],[class~=not-prose] *)),.prose :where(h3+*):not(:where([class~=not-prose],[class~=not-prose] *)),.prose :where(h4+*):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:0}.prose :where(thead th:first-child):not(:where([class~=not-prose],[class~=not-prose] *)){padding-inline-start:0}.prose :where(thead th:last-child):not(:where([class~=not-prose],[class~=not-prose] *)){padding-inline-end:0}.prose :where(tbody td,tfoot td):not(:where([class~=not-prose],[class~=not-prose] *)){padding-top:.571429em;padding-inline-end:.571429em;padding-bottom:.571429em;padding-inline-start:.571429em}.prose :where(tbody td:first-child,tfoot td:first-child):not(:where([class~=not-prose],[class~=not-prose] *)){padding-inline-start:0}.prose :where(tbody td:last-child,tfoot td:last-child):not(:where([class~=not-prose],[class~=not-prose] *)){padding-inline-end:0}.prose :where(figure):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:2em;margin-bottom:2em}.prose :where(.prose>:first-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:0}.prose :where(.prose>:last-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-bottom:0}.mt-1{margin-top:var(--spacing)}.mt-2{margin-top:calc(var(--spacing) * 2)}.mt-3{margin-top:calc(var(--spacing) * 3)}.mt-4{margin-top:calc(var(--spacing) * 4)}.mt-6{margin-top:calc(var(--spacing) * 6)}.-mr-6{margin-right:calc(var(--spacing) * -6)}.mb-2{margin-bottom:calc(var(--spacing) * 2)}.mb-4{margin-bottom:calc(var(--spacing) * 4)}.mb-6{margin-bottom:calc(var(--spacing) * 6)}.mb-8{margin-bottom:calc(var(--spacing) * 8)}.-ml-0\.5{margin-left:calc(var(--spacing) * -.5)}.block{display:block}.flex{display:flex}.hidden{display:none}.inline{display:inline}.table{display:table}.h-3{height:calc(var(--spacing) * 3)}.h-5{height:calc(var(--spacing) * 5)}.h-6{height:calc(var(--spacing) * 6)}.h-8{height:calc(var(--spacing) * 8)}.h-\[calc\(100vh-4\.75rem\)\]{height:calc(100vh - 4.75rem)}.h-full{height:100%}.min-h-full{min-height:100%}.w-5{width:calc(var(--spacing) * 5)}.w-6{width:calc(var(--spacing) * 6)}.w-8{width:calc(var(--spacing) * 8)}.w-36{width:calc(var(--spacing) * 36)}.w-56{width:calc(var(--spacing) * 56)}.w-64{width:calc(var(--spacing) * 64)}.w-full{width:100%}.max-w-3xl{max-width:var(--container-3xl)}.max-w-none{max-width:none}.max-w-xs{max-width:var(--container-xs)}.min-w-0{min-width:0}.min-w-full{min-width:100%}.flex-auto{flex:auto}.flex-none{flex:none}.basis-0{flex-basis:0}.scale-90{--tw-scale-x:90%;--tw-scale-y:90%;--tw-scale-z:90%;scale:var(--tw-scale-x) var(--tw-scale-y)}.transform{transform:var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,)}.cursor-pointer{cursor:pointer}.flex-wrap{flex-wrap:wrap}.items-baseline{align-items:baseline}.items-center{align-items:center}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.justify-end{justify-content:flex-end}.gap-2{gap:calc(var(--spacing) * 2)}.gap-4{gap:calc(var(--spacing) * 4)}.gap-6{gap:calc(var(--spacing) * 6)}:where(.space-y-2>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 2) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 2) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-3>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 3) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 3) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-5>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 5) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 5) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-9>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 9) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 9) * calc(1 - var(--tw-space-y-reverse)))}.gap-x-4{column-gap:calc(var(--spacing) * 4)}.gap-y-1{row-gap:var(--spacing)}:where(.divide-y>:not(:last-child)){--tw-divide-y-reverse:0;border-bottom-style:var(--tw-border-style);border-top-style:var(--tw-border-style);border-top-width:calc(1px * var(--tw-divide-y-reverse));border-bottom-width:calc(1px * calc(1 - var(--tw-divide-y-reverse)))}:where(.divide-slate-100>:not(:last-child)){border-color:var(--color-slate-100)}:where(.divide-slate-200>:not(:last-child)){border-color:var(--color-slate-200)}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.rounded{border-radius:.25rem}.rounded-full{border-radius:3.40282e38px}.rounded-lg{border-radius:var(--radius-lg)}.rounded-xl{border-radius:var(--radius-xl)}.border{border-style:var(--tw-border-style);border-width:1px}.border-l-2{border-left-style:var(--tw-border-style);border-left-width:2px}.border-slate-100{border-color:var(--color-slate-100)}.border-slate-200{border-color:var(--color-slate-200)}.bg-black\/50{background-color:#00000080}@supports (color:color-mix(in lab, red, red)){.bg-black\/50{background-color:color-mix(in oklab, var(--color-black) 50%, transparent)}}.bg-blue-500{background-color:var(--color-blue-500)}.bg-gray-100{background-color:var(--color-gray-100)}.bg-sky-500{background-color:var(--color-sky-500)}.bg-slate-50{background-color:var(--color-slate-50)}.bg-slate-50\/60{background-color:#f8fafc99}@supports (color:color-mix(in lab, red, red)){.bg-slate-50\/60{background-color:color-mix(in oklab, var(--color-slate-50) 60%, transparent)}}.bg-slate-200{background-color:var(--color-slate-200)}.bg-slate-400{background-color:var(--color-slate-400)}.bg-slate-950\/60{background-color:#02061899}@supports (color:color-mix(in lab, red, red)){.bg-slate-950\/60{background-color:color-mix(in oklab, var(--color-slate-950) 60%, transparent)}}.bg-white{background-color:var(--color-white)}.bg-white\/75{background-color:#ffffffbf}@supports (color:color-mix(in lab, red, red)){.bg-white\/75{background-color:color-mix(in oklab, var(--color-white) 75%, transparent)}}.fill-slate-400{fill:var(--color-slate-400)}.stroke-sky-500{stroke:var(--color-sky-500)}.p-1{padding:var(--spacing)}.p-4{padding:calc(var(--spacing) * 4)}.p-5{padding:calc(var(--spacing) * 5)}.px-3{padding-inline:calc(var(--spacing) * 3)}.px-4{padding-inline:calc(var(--spacing) * 4)}.px-6{padding-inline:calc(var(--spacing) * 6)}.py-1{padding-block:var(--spacing)}.py-2{padding-block:calc(var(--spacing) * 2)}.py-3{padding-block:calc(var(--spacing) * 3)}.py-5{padding-block:calc(var(--spacing) * 5)}.py-16{padding-block:calc(var(--spacing) * 16)}.pt-6{padding-top:calc(var(--spacing) * 6)}.pr-6{padding-right:calc(var(--spacing) * 6)}.pr-8{padding-right:calc(var(--spacing) * 8)}.pb-12{padding-bottom:calc(var(--spacing) * 12)}.pl-0\.5{padding-left:calc(var(--spacing) * .5)}.pl-3{padding-left:calc(var(--spacing) * 3)}.pl-3\.5{padding-left:calc(var(--spacing) * 3.5)}.text-left{text-align:left}.text-right{text-align:right}.font-mono{font-family:var(--font-mono)}.text-3xl{font-size:var(--text-3xl);line-height:var(--tw-leading,var(--text-3xl--line-height))}.text-base{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.font-bold{--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold)}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.tracking-wider{--tw-tracking:var(--tracking-wider);letter-spacing:var(--tracking-wider)}.whitespace-nowrap{white-space:nowrap}.text-blue-600{color:var(--color-blue-600)}.text-sky-500{color:var(--color-sky-500)}.text-sky-600{color:var(--color-sky-600)}.text-slate-500{color:var(--color-slate-500)}.text-slate-600{color:var(--color-slate-600)}.text-slate-700{color:var(--color-slate-700)}.text-slate-900{color:var(--color-slate-900)}.text-white{color:var(--color-white)}.text-zinc-500{color:var(--color-zinc-500)}.text-zinc-900{color:var(--color-zinc-900)}.tabular-nums{--tw-numeric-spacing:tabular-nums;font-variant-numeric:var(--tw-ordinal,) var(--tw-slashed-zero,) var(--tw-numeric-figure,) var(--tw-numeric-spacing,) var(--tw-numeric-fraction,)}.antialiased{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.opacity-0{opacity:0}.shadow{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a), 0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-2xl{--tw-shadow:0 25px 50px -12px var(--tw-shadow-color,#00000040);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-lg{--tw-shadow:0 10px 15px -3px var(--tw-shadow-color,#0000001a), 0 4px 6px -4px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-md{--tw-shadow:0 4px 6px -1px var(--tw-shadow-color,#0000001a), 0 2px 4px -2px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.ring-1{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-slate-900\/5{--tw-shadow-color:#0f172b0d}@supports (color:color-mix(in lab, red, red)){.shadow-slate-900\/5{--tw-shadow-color:color-mix(in oklab, color-mix(in oklab, var(--color-slate-900) 5%, transparent) var(--tw-shadow-alpha), transparent)}}.ring-slate-200{--tw-ring-color:var(--color-slate-200)}.ring-slate-900\/10{--tw-ring-color:#0f172b1a}@supports (color:color-mix(in lab, red, red)){.ring-slate-900\/10{--tw-ring-color:color-mix(in oklab, var(--color-slate-900) 10%, transparent)}}.blur{--tw-blur:blur(8px);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.filter{filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.backdrop-blur{--tw-backdrop-blur:blur(8px);-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.backdrop-blur-sm{--tw-backdrop-blur:blur(var(--blur-sm));-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.transition{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter,display,content-visibility,overlay,pointer-events;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-colors{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.duration-500{--tw-duration:.5s;transition-duration:.5s}.prose-slate{--tw-prose-body:oklch(37.2% .044 257.287);--tw-prose-headings:oklch(20.8% .042 265.755);--tw-prose-lead:oklch(44.6% .043 257.281);--tw-prose-links:oklch(20.8% .042 265.755);--tw-prose-bold:oklch(20.8% .042 265.755);--tw-prose-counters:oklch(55.4% .046 257.417);--tw-prose-bullets:oklch(86.9% .022 252.894);--tw-prose-hr:oklch(92.9% .013 255.508);--tw-prose-quotes:oklch(20.8% .042 265.755);--tw-prose-quote-borders:oklch(92.9% .013 255.508);--tw-prose-captions:oklch(55.4% .046 257.417);--tw-prose-kbd:oklch(20.8% .042 265.755);--tw-prose-kbd-shadows:oklab(20.8% -.00310889 -.0418848/.1);--tw-prose-code:oklch(20.8% .042 265.755);--tw-prose-pre-code:oklch(92.9% .013 255.508);--tw-prose-pre-bg:oklch(27.9% .041 260.031);--tw-prose-th-borders:oklch(86.9% .022 252.894);--tw-prose-td-borders:oklch(92.9% .013 255.508);--tw-prose-invert-body:oklch(86.9% .022 252.894);--tw-prose-invert-headings:#fff;--tw-prose-invert-lead:oklch(70.4% .04 256.788);--tw-prose-invert-links:#fff;--tw-prose-invert-bold:#fff;--tw-prose-invert-counters:oklch(70.4% .04 256.788);--tw-prose-invert-bullets:oklch(44.6% .043 257.281);--tw-prose-invert-hr:oklch(37.2% .044 257.287);--tw-prose-invert-quotes:oklch(96.8% .007 247.896);--tw-prose-invert-quote-borders:oklch(37.2% .044 257.287);--tw-prose-invert-captions:oklch(70.4% .04 256.788);--tw-prose-invert-kbd:#fff;--tw-prose-invert-kbd-shadows:#ffffff1a;--tw-prose-invert-code:#fff;--tw-prose-invert-pre-code:oklch(86.9% .022 252.894);--tw-prose-invert-pre-bg:#00000080;--tw-prose-invert-th-borders:oklch(44.6% .043 257.281);--tw-prose-invert-td-borders:oklch(37.2% .044 257.287)}@media (hover:hover){.group-hover\:fill-slate-500:is(:where(.group):hover *){fill:var(--color-slate-500)}}.before\:pointer-events-none:before{content:var(--tw-content);pointer-events:none}.before\:absolute:before{content:var(--tw-content);position:absolute}.before\:top-1\/2:before{content:var(--tw-content);top:50%}.before\:-left-1:before{content:var(--tw-content);left:calc(var(--spacing) * -1)}.before\:hidden:before{content:var(--tw-content);display:none}.before\:h-1\.5:before{content:var(--tw-content);height:calc(var(--spacing) * 1.5)}.before\:w-1\.5:before{content:var(--tw-content);width:calc(var(--spacing) * 1.5)}.before\:-translate-y-1\/2:before{content:var(--tw-content);--tw-translate-y:calc(calc(1 / 2 * 100%) * -1);translate:var(--tw-translate-x) var(--tw-translate-y)}.before\:rounded-full:before{content:var(--tw-content);border-radius:3.40282e38px}.before\:bg-sky-500:before{content:var(--tw-content);background-color:var(--color-sky-500)}.before\:bg-slate-300:before{content:var(--tw-content);background-color:var(--color-slate-300)}@media (hover:hover){.hover\:bg-slate-50\/70:hover{background-color:#f8fafcb3}@supports (color:color-mix(in lab, red, red)){.hover\:bg-slate-50\/70:hover{background-color:color-mix(in oklab, var(--color-slate-50) 70%, transparent)}}.hover\:bg-slate-100:hover{background-color:var(--color-slate-100)}.hover\:text-slate-600:hover{color:var(--color-slate-600)}.hover\:text-zinc-600:hover{color:var(--color-zinc-600)}.hover\:before\:block:hover:before{content:var(--tw-content);display:block}}@media (min-width:40rem){.sm\:gap-8{gap:calc(var(--spacing) * 8)}.sm\:p-6{padding:calc(var(--spacing) * 6)}.sm\:px-2{padding-inline:calc(var(--spacing) * 2)}.sm\:px-6{padding-inline:calc(var(--spacing) * 6)}}@media (min-width:48rem){.md\:grow{flex-grow:1}}@media (min-width:64rem){.lg\:relative{position:relative}.lg\:mt-4{margin-top:calc(var(--spacing) * 4)}.lg\:block{display:block}.lg\:hidden{display:none}.lg\:max-w-none{max-width:none}.lg\:flex-none{flex:none}:where(.lg\:space-y-4>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 4) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 4) * calc(1 - var(--tw-space-y-reverse)))}.lg\:border-slate-200{border-color:var(--color-slate-200)}.lg\:px-8{padding-inline:calc(var(--spacing) * 8)}.lg\:pr-0{padding-right:0}.lg\:pl-8{padding-left:calc(var(--spacing) * 8)}.lg\:text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}}@media (min-width:80rem){.xl\:sticky{position:sticky}.xl\:top-\[4\.75rem\]{top:4.75rem}.xl\:-mr-6{margin-right:calc(var(--spacing) * -6)}.xl\:block{display:block}.xl\:h-\[calc\(100vh-4\.75rem\)\]{height:calc(100vh - 4.75rem)}.xl\:w-72{width:calc(var(--spacing) * 72)}.xl\:flex-none{flex:none}.xl\:overflow-y-auto{overflow-y:auto}.xl\:px-12{padding-inline:calc(var(--spacing) * 12)}.xl\:px-16{padding-inline:calc(var(--spacing) * 16)}.xl\:py-12{padding-block:calc(var(--spacing) * 12)}.xl\:pr-6{padding-right:calc(var(--spacing) * 6)}.xl\:pr-16{padding-right:calc(var(--spacing) * 16)}}.dark\:block:where(.dark,.dark *){display:block}.dark\:hidden:where(.dark,.dark *){display:none}:where(.dark\:divide-slate-700:where(.dark,.dark *)>:not(:last-child)){border-color:var(--color-slate-700)}:where(.dark\:divide-slate-800:where(.dark,.dark *)>:not(:last-child)){border-color:var(--color-slate-800)}.dark\:border-slate-700:where(.dark,.dark *){border-color:var(--color-slate-700)}.dark\:border-slate-800:where(.dark,.dark *){border-color:var(--color-slate-800)}.dark\:bg-slate-500:where(.dark,.dark *){background-color:var(--color-slate-500)}.dark\:bg-slate-700:where(.dark,.dark *){background-color:var(--color-slate-700)}.dark\:bg-slate-800:where(.dark,.dark *){background-color:var(--color-slate-800)}.dark\:bg-slate-800\/40:where(.dark,.dark *){background-color:#1d293d66}@supports (color:color-mix(in lab, red, red)){.dark\:bg-slate-800\/40:where(.dark,.dark *){background-color:color-mix(in oklab, var(--color-slate-800) 40%, transparent)}}.dark\:bg-slate-800\/60:where(.dark,.dark *){background-color:#1d293d99}@supports (color:color-mix(in lab, red, red)){.dark\:bg-slate-800\/60:where(.dark,.dark *){background-color:color-mix(in oklab, var(--color-slate-800) 60%, transparent)}}.dark\:bg-slate-900:where(.dark,.dark *){background-color:var(--color-slate-900)}.dark\:bg-slate-900\/75:where(.dark,.dark *){background-color:#0f172bbf}@supports (color:color-mix(in lab, red, red)){.dark\:bg-slate-900\/75:where(.dark,.dark *){background-color:color-mix(in oklab, var(--color-slate-900) 75%, transparent)}}.dark\:text-sky-400:where(.dark,.dark *){color:var(--color-sky-400)}.dark\:text-slate-200:where(.dark,.dark *){color:var(--color-slate-200)}.dark\:text-slate-300:where(.dark,.dark *){color:var(--color-slate-300)}.dark\:text-slate-400:where(.dark,.dark *){color:var(--color-slate-400)}.dark\:text-white:where(.dark,.dark *){color:var(--color-white)}.dark\:text-zinc-400:where(.dark,.dark *){color:var(--color-zinc-400)}.dark\:shadow-none:where(.dark,.dark *){--tw-shadow:0 0 #0000;box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.dark\:ring-0:where(.dark,.dark *){--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(0px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.dark\:ring-slate-700:where(.dark,.dark *){--tw-ring-color:var(--color-slate-700)}.dark\:ring-white\/10:where(.dark,.dark *){--tw-ring-color:#ffffff1a}@supports (color:color-mix(in lab, red, red)){.dark\:ring-white\/10:where(.dark,.dark *){--tw-ring-color:color-mix(in oklab, var(--color-white) 10%, transparent)}}.dark\:backdrop-blur:where(.dark,.dark *){--tw-backdrop-blur:blur(8px);-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.dark\:prose-invert:where(.dark,.dark *){--tw-prose-body:var(--tw-prose-invert-body);--tw-prose-headings:var(--tw-prose-invert-headings);--tw-prose-lead:var(--tw-prose-invert-lead);--tw-prose-links:var(--tw-prose-invert-links);--tw-prose-bold:var(--tw-prose-invert-bold);--tw-prose-counters:var(--tw-prose-invert-counters);--tw-prose-bullets:var(--tw-prose-invert-bullets);--tw-prose-hr:var(--tw-prose-invert-hr);--tw-prose-quotes:var(--tw-prose-invert-quotes);--tw-prose-quote-borders:var(--tw-prose-invert-quote-borders);--tw-prose-captions:var(--tw-prose-invert-captions);--tw-prose-kbd:var(--tw-prose-invert-kbd);--tw-prose-kbd-shadows:var(--tw-prose-invert-kbd-shadows);--tw-prose-code:var(--tw-prose-invert-code);--tw-prose-pre-code:var(--tw-prose-invert-pre-code);--tw-prose-pre-bg:var(--tw-prose-invert-pre-bg);--tw-prose-th-borders:var(--tw-prose-invert-th-borders);--tw-prose-td-borders:var(--tw-prose-invert-td-borders)}@media (hover:hover){.dark\:group-hover\:fill-slate-300:where(.dark,.dark *):is(:where(.group):hover *){fill:var(--color-slate-300)}}.dark\:before\:bg-slate-700:where(.dark,.dark *):before{content:var(--tw-content);background-color:var(--color-slate-700)}@media (hover:hover){.dark\:hover\:bg-slate-700:where(.dark,.dark *):hover{background-color:var(--color-slate-700)}.dark\:hover\:bg-slate-700\/50:where(.dark,.dark *):hover{background-color:#31415880}@supports (color:color-mix(in lab, red, red)){.dark\:hover\:bg-slate-700\/50:where(.dark,.dark *):hover{background-color:color-mix(in oklab, var(--color-slate-700) 50%, transparent)}}.dark\:hover\:bg-slate-800\/40:where(.dark,.dark *):hover{background-color:#1d293d66}@supports (color:color-mix(in lab, red, red)){.dark\:hover\:bg-slate-800\/40:where(.dark,.dark *):hover{background-color:color-mix(in oklab, var(--color-slate-800) 40%, transparent)}}.dark\:hover\:text-slate-300:where(.dark,.dark *):hover{color:var(--color-slate-300)}.dark\:hover\:text-zinc-300:where(.dark,.dark *):hover{color:var(--color-zinc-300)}}.\[\&_h1\]\:scroll-mt-28 h1,.\[\&_h2\]\:scroll-mt-28 h2,.\[\&_h3\]\:scroll-mt-28 h3{scroll-margin-top:calc(var(--spacing) * 28)}}[x-cloak]{display:none!important}.prose pre{background-color:#0f172a;border:1px solid #1e293b;border-radius:.75rem;position:relative;overflow-x:auto;box-shadow:0 20px 25px -5px #0000001a,0 8px 10px -6px #0000001a}.prose pre code{color:#e2e8f0;border-radius:0;padding:0;font-family:Fira Code,JetBrains Mono,Monaco,Cascadia Code,Segoe UI Mono,Roboto Mono,Oxygen Mono,Ubuntu Monospace,Source Code Pro,Fira Mono,Droid Sans Mono,Courier New,monospace;font-size:.875rem;line-height:1.7;background-color:#0000!important}.prose pre code .hljs-keyword{color:#a855f7}.prose pre code .hljs-string{color:#22c55e}.prose pre code .hljs-comment{color:#94a3b8;font-style:italic}.prose pre code .hljs-function{color:#3b82f6}.prose pre code .hljs-number{color:#fb923c}.prose :not(pre)>code{color:#e2e8f0!important;background-color:#334155!important;border-radius:.375rem!important;padding:.25rem .5rem!important;font-size:.875rem!important;font-weight:500!important}.prose code:before,.prose code:after{content:none!important}@property --tw-scale-x{syntax:"*";inherits:false;initial-value:1}@property --tw-scale-y{syntax:"*";inherits:false;initial-value:1}@property --tw-scale-z{syntax:"*";inherits:false;initial-value:1}@property --tw-rotate-x{syntax:"*";inherits:false}@property --tw-rotate-y{syntax:"*";inherits:false}@property --tw-rotate-z{syntax:"*";inherits:false}@property --tw-skew-x{syntax:"*";inherits:false}@property --tw-skew-y{syntax:"*";inherits:false}@property --tw-space-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-divide-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-tracking{syntax:"*";inherits:false}@property --tw-ordinal{syntax:"*";inherits:false}@property --tw-slashed-zero{syntax:"*";inherits:false}@property --tw-numeric-figure{syntax:"*";inherits:false}@property --tw-numeric-spacing{syntax:"*";inherits:false}@property --tw-numeric-fraction{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false}@property --tw-backdrop-blur{syntax:"*";inherits:false}@property --tw-backdrop-brightness{syntax:"*";inherits:false}@property --tw-backdrop-contrast{syntax:"*";inherits:false}@property --tw-backdrop-grayscale{syntax:"*";inherits:false}@property --tw-backdrop-hue-rotate{syntax:"*";inherits:false}@property --tw-backdrop-invert{syntax:"*";inherits:false}@property --tw-backdrop-opacity{syntax:"*";inherits:false}@property --tw-backdrop-saturate{syntax:"*";inherits:false}@property --tw-backdrop-sepia{syntax:"*";inherits:false}@property --tw-duration{syntax:"*";inherits:false}@property --tw-content{syntax:"*";inherits:false;initial-value:""}@property --tw-translate-x{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-y{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-z{syntax:"*";inherits:false;initial-value:0}
\ No newline at end of file
diff --git a/sln/src/Docs/wwwroot/scripts/alpinejs.3.15.0.min.js b/sln/src/Docs/wwwroot/scripts/alpinejs.3.15.0.min.js
deleted file mode 100644
index 0acdcef..0000000
--- a/sln/src/Docs/wwwroot/scripts/alpinejs.3.15.0.min.js
+++ /dev/null
@@ -1,5 +0,0 @@
-(()=>{var nt=!1,it=!1,W=[],ot=-1;function Ut(e){Rn(e)}function Rn(e){W.includes(e)||W.push(e),Mn()}function Wt(e){let t=W.indexOf(e);t!==-1&&t>ot&&W.splice(t,1)}function Mn(){!it&&!nt&&(nt=!0,queueMicrotask(Nn))}function Nn(){nt=!1,it=!0;for(let e=0;ee.effect(t,{scheduler:r=>{st?Ut(r):r()}}),at=e.raw}function ct(e){N=e}function Yt(e){let t=()=>{};return[n=>{let i=N(n);return e._x_effects||(e._x_effects=new Set,e._x_runEffects=()=>{e._x_effects.forEach(o=>o())}),e._x_effects.add(i),t=()=>{i!==void 0&&(e._x_effects.delete(i),$(i))},i},()=>{t()}]}function ve(e,t){let r=!0,n,i=N(()=>{let o=e();JSON.stringify(o),r?n=o:queueMicrotask(()=>{t(o,n),n=o}),r=!1});return()=>$(i)}var Xt=[],Zt=[],Qt=[];function er(e){Qt.push(e)}function te(e,t){typeof t=="function"?(e._x_cleanups||(e._x_cleanups=[]),e._x_cleanups.push(t)):(t=e,Zt.push(t))}function Ae(e){Xt.push(e)}function Oe(e,t,r){e._x_attributeCleanups||(e._x_attributeCleanups={}),e._x_attributeCleanups[t]||(e._x_attributeCleanups[t]=[]),e._x_attributeCleanups[t].push(r)}function lt(e,t){e._x_attributeCleanups&&Object.entries(e._x_attributeCleanups).forEach(([r,n])=>{(t===void 0||t.includes(r))&&(n.forEach(i=>i()),delete e._x_attributeCleanups[r])})}function tr(e){for(e._x_effects?.forEach(Wt);e._x_cleanups?.length;)e._x_cleanups.pop()()}var ut=new MutationObserver(mt),ft=!1;function ue(){ut.observe(document,{subtree:!0,childList:!0,attributes:!0,attributeOldValue:!0}),ft=!0}function dt(){kn(),ut.disconnect(),ft=!1}var le=[];function kn(){let e=ut.takeRecords();le.push(()=>e.length>0&&mt(e));let t=le.length;queueMicrotask(()=>{if(le.length===t)for(;le.length>0;)le.shift()()})}function m(e){if(!ft)return e();dt();let t=e();return ue(),t}var pt=!1,Se=[];function rr(){pt=!0}function nr(){pt=!1,mt(Se),Se=[]}function mt(e){if(pt){Se=Se.concat(e);return}let t=[],r=new Set,n=new Map,i=new Map;for(let o=0;o{s.nodeType===1&&s._x_marker&&r.add(s)}),e[o].addedNodes.forEach(s=>{if(s.nodeType===1){if(r.has(s)){r.delete(s);return}s._x_marker||t.push(s)}})),e[o].type==="attributes")){let s=e[o].target,a=e[o].attributeName,c=e[o].oldValue,l=()=>{n.has(s)||n.set(s,[]),n.get(s).push({name:a,value:s.getAttribute(a)})},u=()=>{i.has(s)||i.set(s,[]),i.get(s).push(a)};s.hasAttribute(a)&&c===null?l():s.hasAttribute(a)?(u(),l()):u()}i.forEach((o,s)=>{lt(s,o)}),n.forEach((o,s)=>{Xt.forEach(a=>a(s,o))});for(let o of r)t.some(s=>s.contains(o))||Zt.forEach(s=>s(o));for(let o of t)o.isConnected&&Qt.forEach(s=>s(o));t=null,r=null,n=null,i=null}function Ce(e){return z(B(e))}function k(e,t,r){return e._x_dataStack=[t,...B(r||e)],()=>{e._x_dataStack=e._x_dataStack.filter(n=>n!==t)}}function B(e){return e._x_dataStack?e._x_dataStack:typeof ShadowRoot=="function"&&e instanceof ShadowRoot?B(e.host):e.parentNode?B(e.parentNode):[]}function z(e){return new Proxy({objects:e},Dn)}var Dn={ownKeys({objects:e}){return Array.from(new Set(e.flatMap(t=>Object.keys(t))))},has({objects:e},t){return t==Symbol.unscopables?!1:e.some(r=>Object.prototype.hasOwnProperty.call(r,t)||Reflect.has(r,t))},get({objects:e},t,r){return t=="toJSON"?Pn:Reflect.get(e.find(n=>Reflect.has(n,t))||{},t,r)},set({objects:e},t,r,n){let i=e.find(s=>Object.prototype.hasOwnProperty.call(s,t))||e[e.length-1],o=Object.getOwnPropertyDescriptor(i,t);return o?.set&&o?.get?o.set.call(n,r)||!0:Reflect.set(i,t,r)}};function Pn(){return Reflect.ownKeys(this).reduce((t,r)=>(t[r]=Reflect.get(this,r),t),{})}function Te(e){let t=n=>typeof n=="object"&&!Array.isArray(n)&&n!==null,r=(n,i="")=>{Object.entries(Object.getOwnPropertyDescriptors(n)).forEach(([o,{value:s,enumerable:a}])=>{if(a===!1||s===void 0||typeof s=="object"&&s!==null&&s.__v_skip)return;let c=i===""?o:`${i}.${o}`;typeof s=="object"&&s!==null&&s._x_interceptor?n[o]=s.initialize(e,c,o):t(s)&&s!==n&&!(s instanceof Element)&&r(s,c)})};return r(e)}function Re(e,t=()=>{}){let r={initialValue:void 0,_x_interceptor:!0,initialize(n,i,o){return e(this.initialValue,()=>In(n,i),s=>ht(n,i,s),i,o)}};return t(r),n=>{if(typeof n=="object"&&n!==null&&n._x_interceptor){let i=r.initialize.bind(r);r.initialize=(o,s,a)=>{let c=n.initialize(o,s,a);return r.initialValue=c,i(o,s,a)}}else r.initialValue=n;return r}}function In(e,t){return t.split(".").reduce((r,n)=>r[n],e)}function ht(e,t,r){if(typeof t=="string"&&(t=t.split(".")),t.length===1)e[t[0]]=r;else{if(t.length===0)throw error;return e[t[0]]||(e[t[0]]={}),ht(e[t[0]],t.slice(1),r)}}var ir={};function y(e,t){ir[e]=t}function fe(e,t){let r=Ln(t);return Object.entries(ir).forEach(([n,i])=>{Object.defineProperty(e,`$${n}`,{get(){return i(t,r)},enumerable:!1})}),e}function Ln(e){let[t,r]=_t(e),n={interceptor:Re,...t};return te(e,r),n}function or(e,t,r,...n){try{return r(...n)}catch(i){re(i,e,t)}}function re(e,t,r=void 0){e=Object.assign(e??{message:"No error message given."},{el:t,expression:r}),console.warn(`Alpine Expression Error: ${e.message}
-
-${r?'Expression: "'+r+`"
-
-`:""}`,t),setTimeout(()=>{throw e},0)}var Me=!0;function ke(e){let t=Me;Me=!1;let r=e();return Me=t,r}function R(e,t,r={}){let n;return x(e,t)(i=>n=i,r),n}function x(...e){return sr(...e)}var sr=xt;function ar(e){sr=e}function xt(e,t){let r={};fe(r,e);let n=[r,...B(e)],i=typeof t=="function"?$n(n,t):Fn(n,t,e);return or.bind(null,e,t,i)}function $n(e,t){return(r=()=>{},{scope:n={},params:i=[],context:o}={})=>{let s=t.apply(z([n,...e]),i);Ne(r,s)}}var gt={};function jn(e,t){if(gt[e])return gt[e];let r=Object.getPrototypeOf(async function(){}).constructor,n=/^[\n\s]*if.*\(.*\)/.test(e.trim())||/^(let|const)\s/.test(e.trim())?`(async()=>{ ${e} })()`:e,o=(()=>{try{let s=new r(["__self","scope"],`with (scope) { __self.result = ${n} }; __self.finished = true; return __self.result;`);return Object.defineProperty(s,"name",{value:`[Alpine] ${e}`}),s}catch(s){return re(s,t,e),Promise.resolve()}})();return gt[e]=o,o}function Fn(e,t,r){let n=jn(t,r);return(i=()=>{},{scope:o={},params:s=[],context:a}={})=>{n.result=void 0,n.finished=!1;let c=z([o,...e]);if(typeof n=="function"){let l=n.call(a,n,c).catch(u=>re(u,r,t));n.finished?(Ne(i,n.result,c,s,r),n.result=void 0):l.then(u=>{Ne(i,u,c,s,r)}).catch(u=>re(u,r,t)).finally(()=>n.result=void 0)}}}function Ne(e,t,r,n,i){if(Me&&typeof t=="function"){let o=t.apply(r,n);o instanceof Promise?o.then(s=>Ne(e,s,r,n)).catch(s=>re(s,i,t)):e(o)}else typeof t=="object"&&t instanceof Promise?t.then(o=>e(o)):e(t)}var wt="x-";function C(e=""){return wt+e}function cr(e){wt=e}var De={};function d(e,t){return De[e]=t,{before(r){if(!De[r]){console.warn(String.raw`Cannot find directive \`${r}\`. \`${e}\` will use the default order of execution`);return}let n=G.indexOf(r);G.splice(n>=0?n:G.indexOf("DEFAULT"),0,e)}}}function lr(e){return Object.keys(De).includes(e)}function pe(e,t,r){if(t=Array.from(t),e._x_virtualDirectives){let o=Object.entries(e._x_virtualDirectives).map(([a,c])=>({name:a,value:c})),s=Et(o);o=o.map(a=>s.find(c=>c.name===a.name)?{name:`x-bind:${a.name}`,value:`"${a.value}"`}:a),t=t.concat(o)}let n={};return t.map(dr((o,s)=>n[o]=s)).filter(mr).map(zn(n,r)).sort(Kn).map(o=>Bn(e,o))}function Et(e){return Array.from(e).map(dr()).filter(t=>!mr(t))}var yt=!1,de=new Map,ur=Symbol();function fr(e){yt=!0;let t=Symbol();ur=t,de.set(t,[]);let r=()=>{for(;de.get(t).length;)de.get(t).shift()();de.delete(t)},n=()=>{yt=!1,r()};e(r),n()}function _t(e){let t=[],r=a=>t.push(a),[n,i]=Yt(e);return t.push(i),[{Alpine:K,effect:n,cleanup:r,evaluateLater:x.bind(x,e),evaluate:R.bind(R,e)},()=>t.forEach(a=>a())]}function Bn(e,t){let r=()=>{},n=De[t.type]||r,[i,o]=_t(e);Oe(e,t.original,o);let s=()=>{e._x_ignore||e._x_ignoreSelf||(n.inline&&n.inline(e,t,i),n=n.bind(n,e,t,i),yt?de.get(ur).push(n):n())};return s.runCleanups=o,s}var Pe=(e,t)=>({name:r,value:n})=>(r.startsWith(e)&&(r=r.replace(e,t)),{name:r,value:n}),Ie=e=>e;function dr(e=()=>{}){return({name:t,value:r})=>{let{name:n,value:i}=pr.reduce((o,s)=>s(o),{name:t,value:r});return n!==t&&e(n,t),{name:n,value:i}}}var pr=[];function ne(e){pr.push(e)}function mr({name:e}){return hr().test(e)}var hr=()=>new RegExp(`^${wt}([^:^.]+)\\b`);function zn(e,t){return({name:r,value:n})=>{let i=r.match(hr()),o=r.match(/:([a-zA-Z0-9\-_:]+)/),s=r.match(/\.[^.\]]+(?=[^\]]*$)/g)||[],a=t||e[r]||r;return{type:i?i[1]:null,value:o?o[1]:null,modifiers:s.map(c=>c.replace(".","")),expression:n,original:a}}}var bt="DEFAULT",G=["ignore","ref","data","id","anchor","bind","init","for","model","modelable","transition","show","if",bt,"teleport"];function Kn(e,t){let r=G.indexOf(e.type)===-1?bt:e.type,n=G.indexOf(t.type)===-1?bt:t.type;return G.indexOf(r)-G.indexOf(n)}function J(e,t,r={}){e.dispatchEvent(new CustomEvent(t,{detail:r,bubbles:!0,composed:!0,cancelable:!0}))}function D(e,t){if(typeof ShadowRoot=="function"&&e instanceof ShadowRoot){Array.from(e.children).forEach(i=>D(i,t));return}let r=!1;if(t(e,()=>r=!0),r)return;let n=e.firstElementChild;for(;n;)D(n,t,!1),n=n.nextElementSibling}function E(e,...t){console.warn(`Alpine Warning: ${e}`,...t)}var _r=!1;function gr(){_r&&E("Alpine has already been initialized on this page. Calling Alpine.start() more than once can cause problems."),_r=!0,document.body||E("Unable to initialize. Trying to load Alpine before `` is available. Did you forget to add `defer` in Alpine's `