From 8b333f6f8a23405f3ebff27d8b17855dab8ed4c4 Mon Sep 17 00:00:00 2001 From: Andy Meier Date: Thu, 6 Aug 2026 13:44:12 -0400 Subject: [PATCH 1/9] Improve correctness, documentation, packaging, and benchmarks --- .github/workflows/deploy.yml | 56 +- .github/workflows/preview.yml | 59 +- .github/workflows/publish.yml | 28 +- .gitignore | 5 + README.md | 121 +- compose.yml | 13 + e2e/package-lock.json | 76 + e2e/package.json | 12 + e2e/playwright.config.ts | 39 + e2e/scripts/start-local.sh | 36 + e2e/tests/docs.spec.ts | 100 ++ pulumi/package-lock.json | 1392 +++++++---------- pulumi/package.json | 10 +- pulumi/src/config.ts | 2 +- sln/Dockerfile | 2 +- sln/FSharp.ViewEngine.slnx | 1 + sln/fake.sh | 2 +- sln/paket.dependencies | 14 +- sln/paket.lock | 135 +- sln/scripts/verify-package.sh | 96 ++ sln/src/Benchmarks/Benchmarks.fs | 16 - sln/src/Benchmarks/Benchmarks.fsproj | 9 +- sln/src/Benchmarks/Profile.fs | 12 +- sln/src/Benchmarks/Runner.fs | 70 + sln/src/Benchmarks/Workloads.fs | 230 +++ sln/src/Benchmarks/paket.references | 5 - sln/src/Build/Program.fs | 58 +- sln/src/Docs.Tests/Docs.Tests.fsproj | 15 + sln/src/Docs.Tests/Program.fs | 268 ++++ sln/src/Docs.Tests/paket.references | 1 + sln/src/Docs/Docs.fsproj | 25 +- sln/src/Docs/Handlers.fs | 69 - sln/src/Docs/Program.fs | 83 - sln/src/Docs/Views.fs | 342 ---- sln/src/Docs/docs/alpine.md | 185 --- sln/src/Docs/docs/custom.md | 228 --- sln/src/Docs/docs/datastar.md | 298 ---- sln/src/Docs/docs/home.md | 59 - sln/src/Docs/docs/htmx.md | 206 --- sln/src/Docs/docs/installation.md | 23 - sln/src/Docs/docs/svg.md | 155 -- sln/src/Docs/docs/tailwind.md | 201 --- sln/src/Docs/docs/usage.md | 54 - sln/src/Docs/input.css | 2 +- sln/src/Docs/paket.references | 1 - sln/src/Docs/{ => src/Common}/Config.fs | 19 +- sln/src/Docs/src/Common/Handler.fs | 18 + sln/src/Docs/src/Common/Model.fs | 59 + sln/src/Docs/src/Common/View.fs | 402 +++++ sln/src/Docs/src/Pages/Alpine.fs | 159 ++ sln/src/Docs/src/Pages/Benchmarks.fs | 122 ++ sln/src/Docs/src/Pages/Changelog.fs | 133 ++ sln/src/Docs/src/Pages/Custom.fs | 165 ++ sln/src/Docs/src/Pages/Datastar.fs | 153 ++ sln/src/Docs/src/Pages/Home.fs | 64 + sln/src/Docs/src/Pages/Htmx.fs | 215 +++ sln/src/Docs/src/Pages/Installation.fs | 32 + sln/src/Docs/src/Pages/Registry.fs | 18 + sln/src/Docs/src/Pages/Svg.fs | 174 +++ sln/src/Docs/src/Pages/TailwindElements.fs | 179 +++ sln/src/Docs/src/Pages/Usage.fs | 70 + sln/src/Docs/src/Program.fs | 73 + sln/src/Docs/wwwroot/css/output.css | 4 +- .../wwwroot/scripts/alpinejs.3.15.0.min.js | 5 - .../wwwroot/scripts/alpinejs.3.15.12.min.js | 5 + sln/src/FSharp.ViewEngine/Alpine.fs | 103 +- .../CompatibilitySuppressions.xml | 127 ++ sln/src/FSharp.ViewEngine/Core.fs | 32 +- sln/src/FSharp.ViewEngine/Datastar.fs | 49 +- .../FSharp.ViewEngine.fsproj | 21 +- sln/src/FSharp.ViewEngine/Html.fs | 154 +- sln/src/FSharp.ViewEngine/Htmx.fs | 33 +- sln/src/FSharp.ViewEngine/Svg.fs | 185 ++- .../{Tailwind.fs => TailwindElements.fs} | 33 +- sln/src/FSharp.ViewEngine/paket.references | 2 - sln/src/Tests/AlpineTests.fs | 189 ++- sln/src/Tests/CoreTests.fs | 181 ++- sln/src/Tests/DatastarTests.fs | 259 +-- sln/src/Tests/HtmlCoverageTests.fs | 343 ++++ sln/src/Tests/HtmxTests.fs | 99 +- sln/src/Tests/Program.fs | 1 + sln/src/Tests/SvgTests.fs | 285 +++- sln/src/Tests/TailwindTests.fs | 140 +- sln/src/Tests/Tests.fsproj | 1 + 84 files changed, 5701 insertions(+), 3419 deletions(-) create mode 100644 compose.yml create mode 100644 e2e/package-lock.json create mode 100644 e2e/package.json create mode 100644 e2e/playwright.config.ts create mode 100755 e2e/scripts/start-local.sh create mode 100644 e2e/tests/docs.spec.ts create mode 100755 sln/scripts/verify-package.sh create mode 100644 sln/src/Benchmarks/Runner.fs create mode 100644 sln/src/Benchmarks/Workloads.fs delete mode 100644 sln/src/Benchmarks/paket.references create mode 100644 sln/src/Docs.Tests/Docs.Tests.fsproj create mode 100644 sln/src/Docs.Tests/Program.fs create mode 100644 sln/src/Docs.Tests/paket.references delete mode 100644 sln/src/Docs/Handlers.fs delete mode 100644 sln/src/Docs/Program.fs delete mode 100644 sln/src/Docs/Views.fs delete mode 100644 sln/src/Docs/docs/alpine.md delete mode 100644 sln/src/Docs/docs/custom.md delete mode 100644 sln/src/Docs/docs/datastar.md delete mode 100644 sln/src/Docs/docs/home.md delete mode 100644 sln/src/Docs/docs/htmx.md delete mode 100644 sln/src/Docs/docs/installation.md delete mode 100644 sln/src/Docs/docs/svg.md delete mode 100644 sln/src/Docs/docs/tailwind.md delete mode 100644 sln/src/Docs/docs/usage.md rename sln/src/Docs/{ => src/Common}/Config.fs (59%) create mode 100644 sln/src/Docs/src/Common/Handler.fs create mode 100644 sln/src/Docs/src/Common/Model.fs create mode 100644 sln/src/Docs/src/Common/View.fs create mode 100644 sln/src/Docs/src/Pages/Alpine.fs create mode 100644 sln/src/Docs/src/Pages/Benchmarks.fs create mode 100644 sln/src/Docs/src/Pages/Changelog.fs create mode 100644 sln/src/Docs/src/Pages/Custom.fs create mode 100644 sln/src/Docs/src/Pages/Datastar.fs create mode 100644 sln/src/Docs/src/Pages/Home.fs create mode 100644 sln/src/Docs/src/Pages/Htmx.fs create mode 100644 sln/src/Docs/src/Pages/Installation.fs create mode 100644 sln/src/Docs/src/Pages/Registry.fs create mode 100644 sln/src/Docs/src/Pages/Svg.fs create mode 100644 sln/src/Docs/src/Pages/TailwindElements.fs create mode 100644 sln/src/Docs/src/Pages/Usage.fs create mode 100644 sln/src/Docs/src/Program.fs delete mode 100644 sln/src/Docs/wwwroot/scripts/alpinejs.3.15.0.min.js create mode 100644 sln/src/Docs/wwwroot/scripts/alpinejs.3.15.12.min.js create mode 100644 sln/src/FSharp.ViewEngine/CompatibilitySuppressions.xml rename sln/src/FSharp.ViewEngine/{Tailwind.fs => TailwindElements.fs} (78%) delete mode 100644 sln/src/FSharp.ViewEngine/paket.references create mode 100644 sln/src/Tests/HtmlCoverageTests.fs diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 56c8594..a54fe01 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -1,8 +1,12 @@ name: Deploy on: + workflow_call: workflow_dispatch: - release: - types: [published] + +concurrency: + group: fsharp-viewengine-deploy + cancel-in-progress: false + jobs: deploy: name: Deploy @@ -21,7 +25,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 @@ -35,3 +39,49 @@ jobs: 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 + - 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 + run: | + for _ in $(seq 1 60); do + if curl --fail --silent --show-error https://fsharpviewengine.meiermade.com/health >/dev/null; then + exit 0 + fi + sleep 5 + done + echo "FSharp.ViewEngine Docs did not become ready." >&2 + exit 1 + - name: Test deployed Docs + run: npm run test:published + working-directory: ./e2e + env: + DOCS_E2E_BASE_URL: https://fsharpviewengine.meiermade.com + - 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..af460a9 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,11 +20,55 @@ 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 + run: scripts/verify-package.sh "$RUNNER_TEMP/package/FSharp.ViewEngine.0.0.0-preview.nupkg" + 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 runs-on: ubuntu-latest @@ -33,7 +77,8 @@ jobs: id-token: write pull-requests: write needs: - - test + - unit + - e2e steps: - uses: actions/checkout@v5 - name: Setup Node @@ -45,7 +90,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 diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index cbb79f9..d160c63 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -1,17 +1,27 @@ name: Publish on: release: - branches: - - main types: - published + +permissions: + contents: read + id-token: write + +concurrency: + group: fsharp-viewengine-release + cancel-in-progress: false + jobs: + deploy-and-verify: + name: Deploy and verify Docs + uses: ./.github/workflows/deploy.yml + secrets: inherit + publish: - name: Publish + name: Publish NuGet + needs: deploy-and-verify runs-on: ubuntu-latest - permissions: - contents: read - id-token: write steps: - uses: actions/checkout@v5 - name: Setup dotnet @@ -25,7 +35,9 @@ 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: Authenticate Pulumi uses: pulumi/auth-actions@v1 @@ -33,6 +45,6 @@ jobs: organization: meiermade requested-token-type: urn:pulumi:token-type:access_token:personal scope: user:meiermade - - name: Publish + - name: Publish verified package run: pulumi env run fsharpviewengine/nuget -- ./fake.sh PushNugets working-directory: ./sln 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..ef2448d 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,12 @@ 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 @@ -42,7 +48,7 @@ open type Html open type Htmx open type Alpine open type Datastar -open type Tailwind +open type TailwindElements html { _lang "en" @@ -101,33 +107,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..ad9398a --- /dev/null +++ b/e2e/tests/docs.spec.ts @@ -0,0 +1,100 @@ +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) + 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) + expect(await health.text()).toBe('ok') + + const css = await request.get('/css/output.css') + expect(css.status()).toBe(200) + expect(await css.text()).toContain('tailwindcss v4.3.3') + + const alpine = await request.get('/scripts/alpinejs.3.15.12.min.js') + expect(alpine.status()).toBe(200) + expect(await alpine.text()).toContain('version:"3.15.12"') +}) + +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('[x-show="mobileNavOpen"]') + await expect(drawer).toBeHidden() + await page.getByRole('button', { name: 'Open navigation' }).click() + await expect(drawer).toBeVisible() + 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' }) + await page.getByRole('button', { name: 'Choose color theme' }).click() + await page.getByRole('button', { name: 'Dark', exact: true }).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..507f84d 100644 --- a/pulumi/src/config.ts +++ b/pulumi/src/config.ts @@ -18,7 +18,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/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..cd75a4b 100644 --- a/sln/paket.dependencies +++ b/sln/paket.dependencies @@ -2,16 +2,10 @@ 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 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..172e73d 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 @@ -60,8 +43,6 @@ 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 FParsec (1.1.1) - restriction: >= netstandard2.0 FSharp.Core (>= 4.3.4) - restriction: || (>= net45) (>= netstandard2.0) System.ValueTuple (>= 4.4) - restriction: >= net45 @@ -72,14 +53,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 +74,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 +87,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 +139,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 +148,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 +188,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 +200,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 +208,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 +227,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/scripts/verify-package.sh b/sln/scripts/verify-package.sh new file mode 100755 index 0000000..d4dcff5 --- /dev/null +++ b/sln/scripts/verify-package.sh @@ -0,0 +1,96 @@ +#!/usr/bin/env bash +set -euo pipefail + +if [[ $# -ne 1 ]]; then + echo "usage: $0 path/to/FSharp.ViewEngine..nupkg" >&2 + exit 2 +fi + +package_path="$(cd "$(dirname "$1")" && pwd)/$(basename "$1")" +package_dir="$(dirname "$package_path")" +package_name="$(basename "$package_path")" +symbols_package_path="${package_path%.nupkg}.snupkg" +version="${package_name#FSharp.ViewEngine.}" +version="${version%.nupkg}" +frameworks="${PACKAGE_TEST_FRAMEWORKS:-net8.0 net9.0 net10.0}" +work_dir="$(mktemp -d "${TMPDIR:-/tmp}/fsharp-viewengine-package.XXXXXX")" + +cleanup() { + rm -rf "$work_dir" +} +trap cleanup EXIT + +packaged_frameworks="$( + unzip -Z1 "$package_path" \ + | awk -F/ '/^lib\/net[0-9]+\.[0-9]+\/FSharp\.ViewEngine\.dll$/ { print $2 }' \ + | sort -u \ + | paste -sd ' ' - +)" + +if [[ "$packaged_frameworks" != "net8.0" ]]; then + echo "expected only lib/net8.0, found: ${packaged_frameworks:-none}" >&2 + exit 1 +fi + +if unzip -Z1 "$package_path" | grep -Eq '\.pdb$'; then + echo "the main package must not contain PDB files" >&2 + exit 1 +fi + +if [[ ! -f "$symbols_package_path" ]]; then + echo "missing symbol package: $symbols_package_path" >&2 + exit 1 +fi + +symbol_frameworks="$( + unzip -Z1 "$symbols_package_path" \ + | awk -F/ '/^lib\/net[0-9]+\.[0-9]+\/FSharp\.ViewEngine\.pdb$/ { print $2 }' \ + | sort -u \ + | paste -sd ' ' - +)" + +if [[ "$symbol_frameworks" != "net8.0" ]]; then + echo "expected only lib/net8.0 symbols, found: ${symbol_frameworks:-none}" >&2 + exit 1 +fi + +nuspec="$(unzip -p "$package_path" '*.nuspec')" +repository_commit="$(sed -nE 's/.*commit="([0-9a-f]{40})".*/\1/p' <<<"$nuspec")" +if [[ -z "$repository_commit" ]] \ + || ! grep -Eq '&2 + exit 1 +fi + +source_url="https://raw.githubusercontent.com/meiermade/FSharp.ViewEngine/$repository_commit/" +if ! unzip -p "$symbols_package_path" lib/net8.0/FSharp.ViewEngine.pdb \ + | grep -aFq "$source_url"; then + echo "portable PDB does not map Source Link to the packaged repository commit" >&2 + exit 1 +fi + +for framework in $frameworks; do + project_dir="$work_dir/$framework" + dotnet new console --language F# --framework "$framework" --output "$project_dir" --no-restore >/dev/null + + cat >"$project_dir/Program.fs" <<'FSHARP' +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 +FSHARP + + dotnet add "$project_dir" package FSharp.ViewEngine --version "$version" --source "$package_dir" --no-restore >/dev/null + dotnet restore "$project_dir" --source "$package_dir" --source https://api.nuget.org/v3/index.json >/dev/null + + if ! grep -q 'lib/net8\.0/FSharp\.ViewEngine\.dll' "$project_dir/obj/project.assets.json"; then + echo "$framework did not select the net8.0 compatibility asset" >&2 + exit 1 + fi + + dotnet run --project "$project_dir" --framework "$framework" --no-restore +done 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/Program.fs b/sln/src/Build/Program.fs index 838cfc4..46671c9 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,9 @@ 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 scriptsDir = slnDir "scripts" let exec workDir cmd args = CreateProcess.fromRawCommand cmd args @@ -29,9 +34,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,6 +46,11 @@ 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+)$") @@ -55,6 +65,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 +76,52 @@ Target.create "Pack" (fun _ -> |> Async.RunSynchronously ) +Target.create "VerifyPackage" (fun _ -> + let nugets = !! $"{nugetsDir}/*.nupkg" |> Seq.toList + let package = + match nugets with + | [ package ] -> package + | _ -> failwith $"Expected exactly one package, found {nugets.Length}" + + exec slnDir "bash" [ scriptsDir "verify-package.sh"; package ] + |> Async.RunSynchronously +) + 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 +138,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/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..ff22e97 --- /dev/null +++ b/sln/src/Docs.Tests/Program.fs @@ -0,0 +1,268 @@ +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 the pinned self-hosted Alpine runtime" { + let html = Home.page |> View.document Registry.navigation |> Render.toHtmlDocString + Expect.stringContains html "/scripts/alpinejs.3.15.12.min.js" "pinned Alpine script" + Expect.isFalse (html.Contains("unpkg.com/alpinejs")) "external floating Alpine runtime" + } + + 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 59% rename from sln/src/Docs/Config.fs rename to sln/src/Docs/src/Common/Config.fs index d64009c..e0c0ab0 100644 --- a/sln/src/Docs/Config.fs +++ b/sln/src/Docs/src/Common/Config.fs @@ -1,30 +1,25 @@ -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 Config = - { debug: bool - appName: string - serverUrl: string - seq: SeqConfig } + { debug:bool + appName:string + serverUrl:string + seq:SeqConfig } module Config = let 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..176432b --- /dev/null +++ b/sln/src/Docs/src/Common/View.fs @@ -0,0 +1,402 @@ +namespace Docs.Common + +open System +open FSharp.ViewEngine +open type Alpine +open type Html + +module View = + let private menuIcon = + raw """""" + + let private xMarkIcon = + raw """""" + + let private githubIcon = + raw """""" + + let private sunIcon = + raw """""" + + let private moonIcon = + raw """""" + + let private sunIconSmall = + raw """""" + + let private moonIconSmall = + raw """""" + + let private 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: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" + _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 + } + } + 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" + _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" + _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" + _xOn ("click", "open = !open") + sunIcon + moonIcon + } + 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" + ] + 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" + _xOn ("click", $"theme = '{value}'; open = false") + _xBind ("class", $"theme === '{value}' ? 'text-sky-500' : ''") + 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 + _xOn ("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 { _src "/scripts/alpinejs.3.15.12.min.js"; _defer true } + 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" + _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" + _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" + _xOn ("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..c8d6cce --- /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 now loads a pinned, self-hosted Alpine.js 3.15.12 runtime instead of a floating external CDN URL." ]; + [ 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 deploys and tests the public Docs site before publishing the NuGet package." ]; + [ 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..ac595fd --- /dev/null +++ b/sln/src/Docs/src/Pages/Home.fs @@ -0,0 +1,64 @@ +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 "_hxGet"; 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 HTMX, Alpine.js, Datastar, 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 Htmx +open type TailwindElements + +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"""); + 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..f448320 --- /dev/null +++ b/sln/src/Docs/src/Program.fs @@ -0,0 +1,73 @@ +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 = + choose [ + GET >=> choose [ + route "/health" >=> text "ok" + 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 (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 + +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 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/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 `