diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml
index 10334a2..efec1f2 100644
--- a/.github/workflows/deploy.yml
+++ b/.github/workflows/deploy.yml
@@ -5,49 +5,101 @@ on:
- main
paths-ignore:
- '**.md'
+
+concurrency:
+ group: andymeier-deploy
+ cancel-in-progress: false
+
jobs:
update:
name: Update
runs-on: ubuntu-latest
+ timeout-minutes: 20
permissions:
contents: read
id-token: write
- pull-requests: write
steps:
- - uses: actions/checkout@v6
+ - uses: actions/checkout@v7
- name: Setup Node
- uses: actions/setup-node@v6
+ uses: actions/setup-node@v7
with:
node-version: 24.x
- name: Setup gcloud
- uses: google-github-actions/setup-gcloud@v2
+ uses: google-github-actions/setup-gcloud@v3
with:
install_components: gke-gcloud-auth-plugin
- name: Install Packages
working-directory: ./pulumi
- run: npm install
+ run: npm ci
- name: TypeScript Check
working-directory: ./pulumi
run: npm run check
- name: Setup Docker Buildx
- uses: docker/setup-buildx-action@v3
+ uses: docker/setup-buildx-action@v4
with:
version: latest
- name: Expose GitHub Actions cache env
- uses: actions/github-script@v7
+ uses: actions/github-script@v9
with:
script: |
core.exportVariable('ACTIONS_CACHE_URL', process.env['ACTIONS_CACHE_URL'])
core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env['ACTIONS_RUNTIME_TOKEN'])
- name: Authenticate Pulumi
- uses: pulumi/auth-actions@v1
+ uses: pulumi/auth-actions@v2
with:
organization: meiermade
requested-token-type: urn:pulumi:token-type:access_token:personal
scope: user:meiermade
- name: Update
- uses: pulumi/actions@v6
+ uses: pulumi/actions@v7
with:
work-dir: ./pulumi
command: up
stack-name: prod
+
+ e2e:
+ name: E2E
+ needs: update
+ runs-on: ubuntu-latest
+ timeout-minutes: 15
+ permissions:
+ contents: read
+ steps:
+ - uses: actions/checkout@v7
+ - name: Setup Node
+ uses: actions/setup-node@v7
+ with:
+ node-version: 24.x
+ cache: npm
+ cache-dependency-path: e2e/package-lock.json
+ - name: Install E2E packages
+ working-directory: ./e2e
+ run: npm ci
+ - name: Install Firefox
+ working-directory: ./e2e
+ run: npx playwright install --with-deps firefox
+ - name: Wait for deployed site
+ run: |
+ for attempt in $(seq 1 60); do
+ if curl --fail --silent --show-error https://andymeier.dev/health >/dev/null; then
+ exit 0
+ fi
+ sleep 5
+ done
+ echo "Andy Meier site did not become ready." >&2
+ exit 1
+ - name: Run E2E against deployed site
+ working-directory: ./e2e
+ run: npx playwright test --project=firefox --retries=0
+ env:
+ E2E_START_LOCAL: '0'
+ SITE_E2E_BASE_URL: https://andymeier.dev
+ - name: Upload E2E diagnostics
+ if: failure()
+ uses: actions/upload-artifact@v7
+ with:
+ name: andymeier-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 cecdf7a..5dacccb 100644
--- a/.github/workflows/preview.yml
+++ b/.github/workflows/preview.yml
@@ -9,10 +9,13 @@ jobs:
test:
name: Test
runs-on: ubuntu-latest
+ timeout-minutes: 15
+ permissions:
+ contents: read
steps:
- - uses: actions/checkout@v6
+ - uses: actions/checkout@v7
- name: Setup .NET
- uses: actions/setup-dotnet@v4
+ uses: actions/setup-dotnet@v6
with:
dotnet-version: 10.x
- name: Restore Tools & Dependencies
@@ -23,48 +26,82 @@ jobs:
- name: Run Tests
working-directory: ./app
run: dotnet run --project src/Tests/Tests.fsproj
+ - name: Setup Node
+ uses: actions/setup-node@v7
+ with:
+ node-version: 24.x
+ cache: npm
+ cache-dependency-path: e2e/package-lock.json
+ - name: Install E2E packages
+ working-directory: ./e2e
+ run: npm ci
+ - name: Install Firefox
+ working-directory: ./e2e
+ run: npx playwright install --with-deps firefox
+ - name: Install Tailwind CSS
+ run: |
+ curl --fail --location --silent --show-error \
+ https://github.com/tailwindlabs/tailwindcss/releases/download/v4.2.2/tailwindcss-linux-x64 \
+ --output /tmp/tailwindcss
+ echo '4ab84f2b496c402d3ec4fd25e0e5559fe1184d886dadae8fb4438344ec044c22 /tmp/tailwindcss' | sha256sum --check
+ sudo install /tmp/tailwindcss /usr/local/bin/tailwindcss
+ - name: Run local E2E
+ working-directory: ./e2e
+ run: npx playwright test --project=firefox
+ - name: Upload E2E diagnostics
+ if: failure()
+ uses: actions/upload-artifact@v7
+ with:
+ name: andymeier-preview-e2e
+ path: |
+ e2e/playwright-report
+ e2e/test-results
+ /tmp/andymeier-e2e
+ if-no-files-found: ignore
preview:
name: Preview
+ if: github.event.pull_request.head.repo.full_name == github.repository
needs: test
runs-on: ubuntu-latest
+ timeout-minutes: 20
permissions:
contents: read
id-token: write
pull-requests: write
steps:
- - uses: actions/checkout@v6
+ - uses: actions/checkout@v7
- name: Setup Node
- uses: actions/setup-node@v6
+ uses: actions/setup-node@v7
with:
node-version: 24.x
- name: Setup gcloud
- uses: google-github-actions/setup-gcloud@v2
+ uses: google-github-actions/setup-gcloud@v3
with:
install_components: gke-gcloud-auth-plugin
- name: Install Packages
working-directory: ./pulumi
- run: npm install
+ run: npm ci
- name: TypeScript Check
working-directory: ./pulumi
run: npm run check
- name: Setup Docker Buildx
- uses: docker/setup-buildx-action@v3
+ uses: docker/setup-buildx-action@v4
with:
version: latest
- name: Expose GitHub Actions cache env
- uses: actions/github-script@v7
+ uses: actions/github-script@v9
with:
script: |
core.exportVariable('ACTIONS_CACHE_URL', process.env['ACTIONS_CACHE_URL'])
core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env['ACTIONS_RUNTIME_TOKEN'])
- name: Authenticate Pulumi
- uses: pulumi/auth-actions@v1
+ uses: pulumi/auth-actions@v2
with:
organization: meiermade
requested-token-type: urn:pulumi:token-type:access_token:personal
scope: user:meiermade
- name: Preview
- uses: pulumi/actions@v6
+ uses: pulumi/actions@v7
with:
work-dir: ./pulumi
command: preview
diff --git a/.gitignore b/.gitignore
index 2ac5123..bb13511 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,3 +4,5 @@
node_modules
.pi/
app/.data/
+e2e/playwright-report/
+e2e/test-results/
diff --git a/AGENTS.md b/AGENTS.md
index 9742a52..b3b9046 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -12,6 +12,7 @@
Before creating a PR, always run:
1. `cd app && ./fake.sh Test` — all tests must pass
-2. `cd pulumi && pulumi preview` — must show expected changes with no errors
+2. `cd pulumi && npm ci && npm run check` — Pulumi TypeScript must compile
+3. Maintainers with access to the `andymeier/prod` ESC environment run `pulumi preview` when infrastructure changes
-CI runs `pulumi up` on merge to main, so do **not** run `pulumi up` manually.
+External contributors are not expected to have Pulumi credentials. CI runs `pulumi up` on merge to main, so do **not** run `pulumi up` manually.
diff --git a/CODEOWNERS b/CODEOWNERS
index 8bd270c..0e1ee24 100644
--- a/CODEOWNERS
+++ b/CODEOWNERS
@@ -1,3 +1 @@
* @andymeierdev
-* @benjimeierdev
-* @minniemeierdev
diff --git a/README.md b/README.md
index e71b5f5..50379a6 100644
--- a/README.md
+++ b/README.md
@@ -10,7 +10,7 @@ Personal website for Andy Meier built with F#, Giraffe, Datastar, and Tailwind C
- `src/App/` - Main application (Giraffe + Datastar)
- `src/Build/` - FAKE build script
- `src/Tests/` - Expecto tests
-- `pulumi/` - Infrastructure as code (AWS ECR, Cloudflare, Kubernetes)
+- `pulumi/` - Infrastructure as code (GCP Artifact Registry, Cloudflare, Kubernetes)
## Development
@@ -21,6 +21,8 @@ dotnet paket restore
./fake.sh Watch
```
+`Watch` starts a local MockNotion service with fictional articles, so normal local development does not require Notion credentials or a Pulumi ESC environment.
+
## Testing
```bash
diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md
new file mode 100644
index 0000000..2925419
--- /dev/null
+++ b/THIRD_PARTY_NOTICES.md
@@ -0,0 +1,27 @@
+# Third-party notices
+
+This repository includes browser assets from the following projects.
+
+## Datastar 1.0.0-RC.6
+
+Source: https://github.com/starfederation/datastar
+
+Copyright © Star Federation
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+## PrismJS 1.29.0
+
+Source: https://github.com/PrismJS/prism
+
+Copyright (c) 2012 Lea Verou
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/app/Dockerfile b/app/Dockerfile
index 71aa00a..255591e 100644
--- a/app/Dockerfile
+++ b/app/Dockerfile
@@ -4,7 +4,13 @@ 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}" \
+ && case "$ARCH" in \
+ arm64) SHA256=ad627e77b496cccada4a6e26eafff698ef0829081e575a4baf3af8524bb00747 ;; \
+ x64) SHA256=4ab84f2b496c402d3ec4fd25e0e5559fe1184d886dadae8fb4438344ec044c22 ;; \
+ *) echo "Unsupported architecture: $ARCH" >&2; exit 1 ;; \
+ esac \
+ && curl -fsSLo /usr/local/bin/tailwindcss "https://github.com/tailwindlabs/tailwindcss/releases/download/v4.2.2/tailwindcss-linux-${ARCH}" \
+ && echo "$SHA256 /usr/local/bin/tailwindcss" | sha256sum --check \
&& chmod +x /usr/local/bin/tailwindcss
# restore tools
diff --git a/app/andrewmeier.slnx b/app/andrewmeier.slnx
index fb43e20..1e87bf6 100644
--- a/app/andrewmeier.slnx
+++ b/app/andrewmeier.slnx
@@ -3,6 +3,7 @@
+
diff --git a/app/docker-compose.yml b/app/docker-compose.yml
index 4fc1301..c9dadf4 100644
--- a/app/docker-compose.yml
+++ b/app/docker-compose.yml
@@ -1,7 +1,16 @@
services:
+ mock-notion:
+ build:
+ context: .
+ dockerfile: src/MockNotion/Dockerfile
+ ports:
+ - "127.0.0.1:5015:5015"
+ environment:
+ MOCK_NOTION_URL: http://0.0.0.0:5015
+
seq:
image: datalust/seq:2024.3
environment:
ACCEPT_EULA: Y
ports:
- - "5341:80"
+ - "127.0.0.1:5341:80"
diff --git a/app/paket.lock b/app/paket.lock
index 6ffc133..fc9d92a 100644
--- a/app/paket.lock
+++ b/app/paket.lock
@@ -53,7 +53,7 @@ NUGET
Microsoft.AspNetCore.Http.Features (5.0.17) - restriction: && (< net8.0) (>= netstandard2.0)
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.3) - restriction: || (>= net462) (&& (>= net6.0) (< net8.0)) (&& (>= net6.0) (< netstandard2.1)) (&& (>= netstandard2.0) (< netstandard2.1))
+ Microsoft.Bcl.AsyncInterfaces (10.0.3) - restriction: || (>= net462) (&& (>= net6.0) (< net8.0)) (&& (>= net6.0) (< netstandard2.1)) (&& (< net8.0) (>= net9.0)) (&& (>= net9.0) (< netstandard2.1)) (&& (>= netstandard2.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.Data.Sqlite (10.0.3)
@@ -65,12 +65,15 @@ NUGET
Microsoft.Extensions.Configuration (10.0.3) - restriction: || (>= net462) (&& (>= net8.0) (< net9.0)) (>= netstandard2.0)
Microsoft.Extensions.Configuration.Abstractions (>= 10.0.3) - restriction: || (>= net462) (>= netstandard2.0)
Microsoft.Extensions.Primitives (>= 10.0.3) - restriction: || (>= net462) (>= netstandard2.0)
- Microsoft.Extensions.Configuration.Abstractions (10.0.3) - restriction: || (>= net462) (&& (>= net8.0) (< net9.0)) (>= netstandard2.0)
+ Microsoft.Extensions.Configuration.Abstractions (10.0.3) - restriction: || (>= net462) (&& (>= net8.0) (< net9.0)) (&& (< net8.0) (>= netstandard2.1)) (>= netstandard2.0)
Microsoft.Extensions.Primitives (>= 10.0.3) - restriction: || (>= net462) (>= netstandard2.0)
System.ValueTuple (>= 4.6.1) - restriction: >= net462
Microsoft.Extensions.Configuration.Binder (10.0.3) - restriction: || (>= net462) (&& (>= net8.0) (< net9.0)) (>= netstandard2.0)
Microsoft.Extensions.Configuration (>= 10.0.3) - restriction: || (>= net462) (>= netstandard2.0)
Microsoft.Extensions.Configuration.Abstractions (>= 10.0.3) - restriction: || (>= net462) (>= netstandard2.0)
+ Microsoft.Extensions.Configuration.EnvironmentVariables (10.0.3) - restriction: || (>= net462) (>= netstandard2.0)
+ Microsoft.Extensions.Configuration (>= 10.0.3) - restriction: || (>= net462) (>= netstandard2.0)
+ Microsoft.Extensions.Configuration.Abstractions (>= 10.0.3) - restriction: || (>= net462) (>= netstandard2.0)
Microsoft.Extensions.DependencyInjection (10.0.3) - restriction: || (>= net462) (>= netstandard2.0)
Microsoft.Bcl.AsyncInterfaces (>= 10.0.3) - restriction: || (>= net462) (&& (>= netstandard2.0) (< netstandard2.1))
Microsoft.Extensions.DependencyInjection.Abstractions (>= 10.0.3) - restriction: || (>= net462) (>= netstandard2.0)
@@ -83,13 +86,13 @@ NUGET
System.Memory (>= 4.6.3) - restriction: || (>= net462) (&& (< net8.0) (>= netstandard2.0))
System.Text.Encodings.Web (>= 10.0.3) - restriction: || (&& (< net10.0) (>= net9.0)) (>= net462) (&& (>= net8.0) (< net9.0)) (&& (< net8.0) (>= netstandard2.0))
System.Text.Json (>= 10.0.3) - restriction: || (&& (< net10.0) (>= net9.0)) (>= net462) (&& (>= net8.0) (< net9.0)) (&& (< net8.0) (>= netstandard2.0))
- Microsoft.Extensions.Diagnostics.Abstractions (10.0.3) - restriction: || (>= net462) (>= netstandard2.0)
+ Microsoft.Extensions.Diagnostics.Abstractions (10.0.3) - restriction: || (>= net462) (&& (< net8.0) (>= netstandard2.1)) (>= netstandard2.0)
Microsoft.Extensions.DependencyInjection.Abstractions (>= 10.0.3) - restriction: || (>= net462) (>= netstandard2.0)
Microsoft.Extensions.Options (>= 10.0.3) - restriction: || (>= net462) (>= netstandard2.0)
System.Buffers (>= 4.6.1) - restriction: || (>= net462) (&& (< net8.0) (>= netstandard2.0))
System.Diagnostics.DiagnosticSource (>= 10.0.3) - 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.FileProviders.Abstractions (10.0.3) - restriction: || (>= net462) (>= netstandard2.0)
+ Microsoft.Extensions.FileProviders.Abstractions (10.0.3) - restriction: || (>= net462) (&& (< net8.0) (>= netstandard2.1)) (>= netstandard2.0)
Microsoft.Extensions.Primitives (>= 10.0.3) - restriction: || (>= net462) (>= netstandard2.0)
Microsoft.Extensions.Hosting.Abstractions (10.0.3) - restriction: || (>= net462) (&& (< net8.0) (>= netstandard2.1)) (>= netstandard2.0)
Microsoft.Bcl.AsyncInterfaces (>= 10.0.3) - restriction: || (>= net462) (&& (>= netstandard2.0) (< netstandard2.1))
@@ -106,7 +109,7 @@ NUGET
Microsoft.Extensions.Options (>= 10.0.3) - restriction: || (>= net462) (>= netstandard2.0)
System.Diagnostics.DiagnosticSource (>= 10.0.3) - restriction: || (>= net462) (&& (< net8.0) (>= netstandard2.1)) (&& (>= netstandard2.0) (< netstandard2.1))
System.ValueTuple (>= 4.6.1) - restriction: >= net462
- Microsoft.Extensions.Logging.Abstractions (10.0.3) - restriction: || (>= net462) (>= netstandard2.0)
+ Microsoft.Extensions.Logging.Abstractions (10.0.3) - restriction: || (>= net462) (&& (< net8.0) (>= netstandard2.1)) (>= netstandard2.0)
Microsoft.Extensions.DependencyInjection.Abstractions (>= 10.0.3) - restriction: || (>= net462) (>= netstandard2.0)
System.Buffers (>= 4.6.1) - restriction: || (>= net462) (&& (< net8.0) (>= netstandard2.0))
System.Diagnostics.DiagnosticSource (>= 10.0.3) - restriction: || (&& (< net10.0) (>= net9.0)) (>= net462) (&& (>= net8.0) (< net9.0)) (&& (< net8.0) (>= netstandard2.0))
@@ -120,7 +123,7 @@ NUGET
Microsoft.Extensions.Logging.Abstractions (>= 10.0.3) - restriction: || (>= net462) (>= netstandard2.0)
Microsoft.Extensions.Options (>= 10.0.3) - restriction: || (>= net462) (>= netstandard2.0)
Microsoft.Extensions.Options.ConfigurationExtensions (>= 10.0.3) - restriction: || (>= net462) (>= netstandard2.0)
- Microsoft.Extensions.Options (10.0.3) - restriction: || (>= net462) (>= netstandard2.0)
+ Microsoft.Extensions.Options (10.0.3) - restriction: || (>= net462) (&& (< net8.0) (>= netstandard2.1)) (>= netstandard2.0)
Microsoft.Extensions.DependencyInjection.Abstractions (>= 10.0.3) - restriction: || (>= net462) (>= netstandard2.0)
Microsoft.Extensions.Primitives (>= 10.0.3) - restriction: || (>= net462) (>= netstandard2.0)
System.ComponentModel.Annotations (>= 5.0) - restriction: || (&& (< net462) (>= netstandard2.0) (< netstandard2.1)) (&& (< net8.0) (>= netstandard2.1))
@@ -131,50 +134,53 @@ NUGET
Microsoft.Extensions.DependencyInjection.Abstractions (>= 10.0.3) - restriction: || (>= net462) (>= netstandard2.0)
Microsoft.Extensions.Options (>= 10.0.3) - restriction: || (>= net462) (>= netstandard2.0)
Microsoft.Extensions.Primitives (>= 10.0.3) - restriction: || (>= net462) (>= netstandard2.0)
- Microsoft.Extensions.Primitives (10.0.3) - restriction: || (>= net462) (>= netstandard2.0)
+ Microsoft.Extensions.Primitives (10.0.3) - restriction: || (>= net462) (&& (>= net8.0) (< net9.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))
Microsoft.IO.RecyclableMemoryStream (3.0.1) - restriction: >= net6.0
- OpenTelemetry (1.15)
+ OpenTelemetry (1.17)
+ Microsoft.Extensions.Configuration.EnvironmentVariables (>= 8.0) - restriction: && (>= net8.0) (< net9.0)
+ Microsoft.Extensions.Configuration.EnvironmentVariables (>= 9.0) - restriction: && (< net10.0) (>= net9.0)
+ Microsoft.Extensions.Configuration.EnvironmentVariables (>= 10.0) - restriction: || (>= net10.0) (>= net462) (&& (< net8.0) (>= netstandard2.1)) (&& (>= netstandard2.0) (< netstandard2.1))
Microsoft.Extensions.Diagnostics.Abstractions (>= 8.0) - restriction: && (>= net8.0) (< net9.0)
Microsoft.Extensions.Diagnostics.Abstractions (>= 9.0) - restriction: && (< net10.0) (>= net9.0)
Microsoft.Extensions.Diagnostics.Abstractions (>= 10.0) - restriction: || (>= net10.0) (>= net462) (&& (< net8.0) (>= netstandard2.1)) (&& (>= netstandard2.0) (< netstandard2.1))
Microsoft.Extensions.Logging.Configuration (>= 8.0) - restriction: && (>= net8.0) (< net9.0)
Microsoft.Extensions.Logging.Configuration (>= 9.0) - restriction: && (< net10.0) (>= net9.0)
Microsoft.Extensions.Logging.Configuration (>= 10.0) - restriction: || (>= net10.0) (>= net462) (&& (< net8.0) (>= netstandard2.1)) (&& (>= netstandard2.0) (< netstandard2.1))
- OpenTelemetry.Api.ProviderBuilderExtensions (>= 1.15) - restriction: || (>= net462) (>= netstandard2.0)
- OpenTelemetry.Api (1.15) - restriction: >= netstandard2.0
+ OpenTelemetry.Api.ProviderBuilderExtensions (>= 1.17) - restriction: || (>= net462) (>= netstandard2.0)
+ OpenTelemetry.Api (1.17) - restriction: >= netstandard2.0
System.Diagnostics.DiagnosticSource (>= 10.0) - restriction: || (&& (< net10.0) (>= net9.0)) (>= net462) (&& (>= net8.0) (< net9.0)) (&& (< net8.0) (>= netstandard2.0))
- OpenTelemetry.Api.ProviderBuilderExtensions (1.15) - restriction: || (>= net462) (>= netstandard2.0)
+ OpenTelemetry.Api.ProviderBuilderExtensions (1.17) - restriction: || (>= net462) (>= netstandard2.0)
Microsoft.Extensions.DependencyInjection.Abstractions (>= 8.0) - restriction: && (>= net8.0) (< net9.0)
Microsoft.Extensions.DependencyInjection.Abstractions (>= 9.0) - restriction: && (< net10.0) (>= net9.0)
Microsoft.Extensions.DependencyInjection.Abstractions (>= 10.0) - restriction: || (>= net10.0) (>= net462) (&& (< net8.0) (>= netstandard2.0))
- OpenTelemetry.Api (>= 1.15) - restriction: || (>= net462) (>= netstandard2.0)
- OpenTelemetry.Exporter.Console (1.15)
- OpenTelemetry (>= 1.15) - restriction: || (>= net462) (>= netstandard2.0)
+ OpenTelemetry.Api (>= 1.17) - restriction: || (>= net462) (>= netstandard2.0)
+ OpenTelemetry.Exporter.Console (1.17)
+ OpenTelemetry (>= 1.17) - restriction: || (>= net462) (>= netstandard2.0)
System.Text.Encodings.Web (>= 4.7.2) - restriction: || (>= net462) (&& (< net8.0) (>= netstandard2.0))
System.Text.Json (>= 4.7.2) - restriction: || (>= net462) (&& (< net8.0) (>= netstandard2.0))
- OpenTelemetry.Exporter.OpenTelemetryProtocol (1.15)
+ OpenTelemetry.Exporter.OpenTelemetryProtocol (1.17)
Microsoft.Extensions.Configuration.Binder (>= 8.0.2) - restriction: && (>= net8.0) (< net9.0)
- OpenTelemetry (>= 1.15) - restriction: || (>= net462) (>= netstandard2.0)
- OpenTelemetry.Extensions.Hosting (1.15)
+ OpenTelemetry (>= 1.17) - restriction: || (>= net462) (>= netstandard2.0)
+ OpenTelemetry.Extensions.Hosting (1.17)
Microsoft.Extensions.Hosting.Abstractions (>= 8.0) - restriction: && (>= net8.0) (< net9.0)
Microsoft.Extensions.Hosting.Abstractions (>= 9.0) - restriction: && (< net10.0) (>= net9.0)
Microsoft.Extensions.Hosting.Abstractions (>= 10.0) - restriction: || (>= net10.0) (>= net462) (&& (< net8.0) (>= netstandard2.0))
- OpenTelemetry (>= 1.15) - restriction: || (>= net462) (>= netstandard2.0)
- OpenTelemetry.Instrumentation.AspNetCore (1.15)
+ OpenTelemetry (>= 1.17) - restriction: || (>= net462) (>= netstandard2.0)
+ OpenTelemetry.Instrumentation.AspNetCore (1.17)
Microsoft.AspNetCore.Http.Abstractions (>= 2.1.1 < 6.0) - restriction: && (< net8.0) (>= netstandard2.0)
Microsoft.AspNetCore.Http.Features (>= 2.1.1 < 6.0) - restriction: && (< net8.0) (>= netstandard2.0)
Microsoft.Extensions.Configuration (>= 10.0) - restriction: && (< net8.0) (>= netstandard2.0)
Microsoft.Extensions.Options (>= 10.0) - restriction: && (< net8.0) (>= netstandard2.0)
- OpenTelemetry.Api.ProviderBuilderExtensions (>= 1.15 < 2.0) - restriction: >= netstandard2.0
+ OpenTelemetry.Api.ProviderBuilderExtensions (>= 1.17 < 2.0) - restriction: >= netstandard2.0
System.Text.Encodings.Web (>= 4.7.2) - restriction: && (< net8.0) (>= netstandard2.0)
- OpenTelemetry.Instrumentation.Http (1.15)
+ OpenTelemetry.Instrumentation.Http (1.17)
Microsoft.Extensions.Configuration (>= 8.0) - restriction: && (< net10.0) (>= net8.0)
Microsoft.Extensions.Configuration (>= 10.0) - restriction: || (>= net10.0) (>= net462) (&& (< net8.0) (>= netstandard2.0))
Microsoft.Extensions.Options (>= 8.0) - restriction: && (< net10.0) (>= net8.0)
Microsoft.Extensions.Options (>= 10.0) - restriction: || (>= net10.0) (>= net462) (&& (< net8.0) (>= netstandard2.0))
- OpenTelemetry.Api.ProviderBuilderExtensions (>= 1.15 < 2.0) - restriction: || (>= net462) (>= netstandard2.0)
+ OpenTelemetry.Api.ProviderBuilderExtensions (>= 1.17 < 2.0) - restriction: || (>= net462) (>= netstandard2.0)
Ply (0.3.1) - restriction: && (>= netstandard2.0) (< netstandard2.1)
FSharp.Core (>= 4.6.2) - restriction: >= netstandard2.0
System.Threading.Tasks.Extensions (>= 4.5.4) - restriction: >= netstandard2.0
@@ -237,16 +243,16 @@ NUGET
StarFederation.Datastar.FSharp (>= 1.2) - restriction: >= net8.0
StarFederation.Datastar.FSharp (1.2) - restriction: >= net8.0
FSharp.Core (>= 6.0) - restriction: >= net8.0
- System.Buffers (4.6.1) - restriction: || (>= net462) (&& (>= net6.0) (< net8.0)) (&& (< net8.0) (>= netstandard2.0)) (&& (< netcoreapp2.1) (>= netstandard2.0) (< netstandard2.1))
+ System.Buffers (4.6.1) - restriction: || (&& (>= net45) (>= net6.0)) (&& (>= net45) (>= net9.0)) (&& (>= net461) (>= net6.0)) (&& (>= net461) (>= net8.0)) (&& (>= net461) (>= net9.0)) (&& (>= net461) (>= netstandard2.0)) (>= net462) (&& (>= net6.0) (< net8.0)) (&& (>= net6.0) (< netcoreapp2.1) (< netstandard2.1)) (&& (< net8.0) (>= net9.0)) (&& (< net8.0) (>= netstandard2.0)) (&& (>= net9.0) (< netcoreapp2.1) (< netstandard2.1)) (&& (< netcoreapp2.1) (>= netstandard2.0) (< netstandard2.1))
System.ComponentModel.Annotations (5.0) - restriction: || (&& (< net462) (>= netstandard2.0) (< netstandard2.1)) (&& (< net8.0) (>= netstandard2.1))
- System.Diagnostics.DiagnosticSource (10.0.3) - restriction: || (&& (< net10.0) (>= net9.0)) (>= net462) (&& (< net6.0) (>= netstandard2.0)) (&& (>= net8.0) (< net9.0)) (&& (< net8.0) (>= netstandard2.0)) (&& (< net8.0) (>= netstandard2.1)) (&& (>= netstandard2.0) (< netstandard2.1))
+ System.Diagnostics.DiagnosticSource (10.0.3) - restriction: || (&& (< net10.0) (>= net9.0)) (>= net462) (&& (< net6.0) (>= net8.0)) (&& (< net6.0) (>= net9.0)) (&& (< net6.0) (>= netstandard2.0)) (&& (< net6.0) (>= netstandard2.1)) (&& (>= net8.0) (< net9.0)) (&& (>= net8.0) (< netstandard2.1)) (&& (< net8.0) (>= net9.0)) (&& (< net8.0) (>= netstandard2.0)) (&& (< net8.0) (>= netstandard2.1)) (&& (>= net9.0) (< netstandard2.1)) (&& (>= netstandard2.0) (< netstandard2.1))
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.IO.Pipelines (10.0.3) - restriction: || (&& (< net10.0) (>= net9.0)) (&& (>= net461) (>= netstandard2.0)) (&& (>= net462) (>= net6.0)) (&& (>= net6.0) (< net8.0)) (&& (>= net8.0) (< net9.0)) (&& (< net8.0) (>= netstandard2.0))
+ System.IO.Pipelines (10.0.3) - restriction: || (&& (< net10.0) (>= net9.0)) (&& (>= net461) (>= net6.0)) (&& (>= net461) (>= net8.0)) (&& (>= net461) (>= net9.0)) (&& (>= net461) (>= netstandard2.0)) (&& (>= net462) (>= net6.0)) (&& (>= net462) (>= net8.0)) (&& (>= net462) (>= net9.0)) (&& (>= net462) (>= netstandard2.0)) (&& (>= net6.0) (< net8.0)) (&& (>= net8.0) (< net9.0)) (&& (< net8.0) (>= net9.0)) (&& (< net8.0) (>= netstandard2.0))
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.Memory (4.6.3) - restriction: || (&& (>= net45) (>= net6.0)) (>= net462) (&& (< net5.0) (>= net6.0)) (&& (>= net6.0) (< net8.0)) (&& (>= net6.0) (< netstandard2.1)) (>= netstandard2.0)
+ System.Memory (4.6.3) - restriction: || (&& (>= net45) (>= net6.0)) (&& (>= net45) (>= net9.0)) (&& (>= net461) (>= net6.0)) (&& (>= net461) (>= net8.0)) (&& (>= net461) (>= net9.0)) (>= net462) (&& (< net5.0) (>= net6.0)) (&& (< net5.0) (>= net9.0)) (&& (>= net6.0) (< net8.0)) (&& (>= net6.0) (< netstandard2.1)) (&& (< net8.0) (>= net9.0)) (&& (>= net9.0) (< netstandard2.1)) (>= netstandard2.0)
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))
@@ -254,9 +260,9 @@ 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) (&& (< netcoreapp2.1) (>= netstandard2.0) (< netstandard2.1))
- System.Runtime.CompilerServices.Unsafe (6.1.2) - restriction: || (&& (< net45) (>= net471)) (&& (< net45) (< net5.0) (>= netstandard2.0)) (>= net462) (&& (< net5.0) (>= net6.0)) (&& (>= net6.0) (< net8.0)) (&& (< net6.0) (>= netstandard2.0)) (&& (< net8.0) (>= netstandard2.0)) (&& (< netcoreapp2.1) (>= netstandard2.0) (< netstandard2.1))
- System.Text.Encodings.Web (10.0.3) - restriction: || (&& (< net10.0) (>= net9.0)) (>= net462) (&& (>= net6.0) (< net8.0)) (&& (>= net8.0) (< net9.0)) (&& (< net8.0) (>= netstandard2.0))
+ System.Numerics.Vectors (4.6.1) - restriction: || (&& (>= net45) (>= net6.0)) (&& (>= net45) (>= net9.0)) (>= net462) (&& (>= net6.0) (< netcoreapp2.1) (< netstandard2.1)) (&& (>= net9.0) (< netcoreapp2.1) (< netstandard2.1)) (&& (< netcoreapp2.1) (>= netstandard2.0) (< netstandard2.1))
+ System.Runtime.CompilerServices.Unsafe (6.1.2) - restriction: || (&& (>= net45) (>= net6.0)) (&& (>= net45) (>= net9.0)) (&& (< net45) (>= net471)) (&& (< net45) (< net5.0) (>= netstandard2.0)) (>= net462) (&& (>= net471) (>= net6.0)) (&& (>= net471) (>= net9.0)) (&& (< net5.0) (>= net6.0)) (&& (>= net6.0) (< net8.0)) (&& (>= net6.0) (< netcoreapp2.1) (< netstandard2.1)) (&& (< net6.0) (>= netstandard2.0)) (&& (< net8.0) (>= netstandard2.0)) (&& (>= net9.0) (< netcoreapp2.1) (< netstandard2.1)) (&& (< netcoreapp2.1) (>= netstandard2.0) (< netstandard2.1))
+ System.Text.Encodings.Web (10.0.3) - restriction: || (&& (< net10.0) (>= net9.0)) (>= net462) (&& (>= net6.0) (< net8.0)) (&& (>= net6.0) (< netstandard2.1)) (&& (>= net8.0) (< net9.0)) (&& (>= net8.0) (< netstandard2.1)) (&& (< net8.0) (>= net9.0)) (&& (< net8.0) (>= netstandard2.0)) (&& (< net8.0) (>= netstandard2.1)) (&& (>= net9.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))
@@ -268,10 +274,11 @@ NUGET
System.Runtime.CompilerServices.Unsafe (>= 6.1.2) - restriction: || (>= net462) (&& (< net8.0) (>= netstandard2.0))
System.Text.Encodings.Web (>= 10.0.3) - restriction: || (&& (< net10.0) (>= net9.0)) (>= net462) (&& (>= net8.0) (< net9.0)) (&& (< net8.0) (>= netstandard2.0))
System.Threading.Tasks.Extensions (>= 4.6.3) - restriction: || (>= net462) (&& (< net8.0) (>= netstandard2.0))
+ System.ValueTuple (>= 4.6.1) - restriction: >= net462
System.Threading.Channels (10.0.3) - restriction: || (&& (>= net462) (< netstandard2.0)) (&& (< net462) (< net6.0) (>= netstandard2.0)) (>= net471)
Microsoft.Bcl.AsyncInterfaces (>= 10.0.3) - 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) (< net8.0)) (&& (>= netstandard2.0) (< netstandard2.1))
+ System.Threading.Tasks.Extensions (4.6.3) - restriction: || (&& (>= net461) (>= net6.0)) (&& (>= net461) (>= net8.0)) (&& (>= net461) (>= net9.0)) (&& (>= net461) (>= netstandard2.0)) (>= net462) (&& (>= net6.0) (< net8.0)) (&& (>= net6.0) (< netstandard2.1)) (&& (< net8.0) (>= net9.0)) (&& (>= net9.0) (< netstandard2.1)) (&& (>= netstandard2.0) (< netstandard2.1))
System.Runtime.CompilerServices.Unsafe (>= 6.1.2) - restriction: || (>= net462) (&& (< netcoreapp2.1) (>= netstandard2.0) (< netstandard2.1))
System.ValueTuple (4.6.2) - restriction: >= net462
diff --git a/app/src/App/App.fsproj b/app/src/App/App.fsproj
index 3e10037..34f3a16 100644
--- a/app/src/App/App.fsproj
+++ b/app/src/App/App.fsproj
@@ -3,6 +3,7 @@
net10.0
$(NoWarn);NU1510
+ true
@@ -14,10 +15,6 @@
-
-
-
-
@@ -26,4 +23,4 @@
-
+
\ No newline at end of file
diff --git a/app/src/App/src/Articles/Handler.fs b/app/src/App/src/Articles/Handler.fs
index 0db339b..0b10921 100644
--- a/app/src/App/src/Articles/Handler.fs
+++ b/app/src/App/src/Articles/Handler.fs
@@ -4,24 +4,71 @@ open App.Infrastructure
open App.ServiceRegistry
open App.Articles.View
open App.Common.Handler
+open App.Common.View
open Giraffe
open Microsoft.AspNetCore.Http
open StarFederation.Datastar.DependencyInjection
+open System
+
+let private optionalQueryValue (ctx:HttpContext) name =
+ match ctx.Request.Query.TryGetValue name with
+ | true, value when not (String.IsNullOrWhiteSpace(string value)) -> Some(string value)
+ | _ -> None
+
+let private filtersFromRequest (ctx:HttpContext) =
+ { FilterState.search = optionalQueryValue ctx "search"
+ tag = optionalQueryValue ctx "tag"
+ publishedYear = optionalQueryValue ctx "year" |> Option.bind (fun value ->
+ match Int32.TryParse value with
+ | true, year -> Some year
+ | false, _ -> None) }
+
+let filterArticles (filters:FilterState) (articles:Domain.Article.Article list) =
+ let matchesSearch (article:Domain.Article.Article) =
+ filters.search
+ |> Option.forall (fun query ->
+ let query = query.Trim()
+ article.title.Contains(query, StringComparison.OrdinalIgnoreCase)
+ || article.summary.Contains(query, StringComparison.OrdinalIgnoreCase)
+ || (article.tags |> Array.exists (fun tag -> tag.Contains(query, StringComparison.OrdinalIgnoreCase))))
+
+ let matchesTag (article:Domain.Article.Article) =
+ filters.tag
+ |> Option.forall (fun selectedTag ->
+ article.tags |> Array.exists (fun tag -> String.Equals(tag, selectedTag, StringComparison.OrdinalIgnoreCase)))
+
+ let matchesYear (article:Domain.Article.Article) =
+ filters.publishedYear
+ |> Option.forall (fun year -> article.createdAt.Year = year)
+
+ articles |> List.filter (fun article -> matchesSearch article && matchesTag article && matchesYear article)
let private getArticlesPage (services:Services) : HttpHandler =
fun next ctx -> task {
use _span = services.telemetry.startActiveSpan "app.articles.get_articles_page"
- let! articles = services.article.listArticles ()
- let page = articlesPage articles
-
- if ctx.IsDatastar then
- let ds = ctx.GetService()
- do! patchSignals ds {| selectedNav = "nav-articles" |}
- do! patchElement ds page
- do! pushUrl ds "/articles"
- return Some ctx
+ let filters = filtersFromRequest ctx
+ let canonicalUrl = FilterState.url filters
+ let requestedUrl = ctx.Request.Path.Value + ctx.Request.QueryString.ToString()
+
+ if not ctx.IsDatastar && requestedUrl <> canonicalUrl then
+ return! redirectTo false canonicalUrl next ctx
else
- return! renderPage services page "nav-articles" next ctx
+ let! allArticles = services.article.listArticles ()
+ let page =
+ { articles = filterArticles filters allArticles
+ filters = filters
+ tags = allArticles |> List.collect (fun article -> article.tags |> Array.toList) |> List.distinct |> List.sort
+ years = allArticles |> List.map (fun article -> article.createdAt.Year) |> List.distinct |> List.sortDescending }
+ |> articlesPage
+
+ if ctx.IsDatastar then
+ let ds = ctx.GetService()
+ do! patchSignals ds {| selectedNav = "nav-articles" |}
+ do! patchElement ds page
+ do! pushUrl ds canonicalUrl
+ return Some ctx
+ else
+ return! renderPage services page "nav-articles" next ctx
}
let private getArticlePage (services:Services) (id:string) : HttpHandler =
@@ -30,7 +77,7 @@ let private getArticlePage (services:Services) (id:string) : HttpHandler =
match! services.article.tryGetArticle id with
| Some article ->
let page = articlePage article
- let url = $"/articles/{article.permalink}"
+ let url = SiteUrl.article article.permalink
if ctx.IsDatastar then
let ds = ctx.GetService()
diff --git a/app/src/App/src/Articles/View.fs b/app/src/App/src/Articles/View.fs
index 80fd70f..58f0930 100644
--- a/app/src/App/src/Articles/View.fs
+++ b/app/src/App/src/Articles/View.fs
@@ -3,25 +3,249 @@ module App.Articles.View
open Domain.Article
open FSharp.ViewEngine
open App.Common.View
+open System
open type Datastar
open type Html
-let articlesPage (articles:Article list) =
+type FilterState =
+ { search: string option
+ tag: string option
+ publishedYear: int option }
+
+module FilterState =
+ let url (filters:FilterState) =
+ [ "search", filters.search
+ "tag", filters.tag
+ "year", filters.publishedYear |> Option.map string ]
+ |> List.choose (fun (key, value) ->
+ value
+ |> Option.map _.Trim()
+ |> Option.filter (String.IsNullOrWhiteSpace >> not)
+ |> Option.map (fun value -> $"{Uri.EscapeDataString key}={Uri.EscapeDataString value}"))
+ |> String.concat "&"
+ |> function
+ | "" -> "/articles"
+ | query -> $"/articles?{query}"
+
+type ArticlesPageState =
+ { articles: Article list
+ filters: FilterState
+ tags: string list
+ years: int list }
+
+type FilterOption =
+ { label: string
+ url: string
+ selected: bool }
+
+module FilterControl =
+ let private optionLink (option:FilterOption) =
+ a {
+ _href option.url
+ _class "group flex w-full items-center justify-between gap-3 px-3 py-2 text-left text-sm text-gray-800 transition hover:bg-gray-100 focus:bg-gray-100 focus-visible:outline-2 focus-visible:outline-inset focus-visible:outline-emerald-600 dark:text-gray-100 dark:hover:bg-gray-700 dark:focus:bg-gray-700 dark:focus-visible:outline-emerald-400"
+ span { _class "truncate"; option.label }
+ span {
+ _class (if option.selected then "text-emerald-600 dark:text-emerald-400" else "invisible")
+ _ariaHidden "true"
+ "✓"
+ }
+ }
+
+ let disclosure (name:string) (ariaLabel:string) (selectedLabel:string) (options:FilterOption list) (buttonClass:string) =
+ div {
+ _class "contents"
+ _data ("filter-control", name)
+ Disclosure.panel {
+ id = $"filter-{name}"
+ openSignal = $"{name}FilterOpen"
+ triggerLabel = ariaLabel
+ rootClass = "relative"
+ triggerClass = buttonClass
+ triggerContent =
+ span {
+ _class "contents"
+ span { _class "block min-w-0 truncate"; selectedLabel }
+ span {
+ _class "pointer-events-none ml-2 flex size-5 shrink-0 items-center justify-center text-gray-500 dark:text-gray-400"
+ _ariaHidden "true"
+ raw """"""
+ }
+ }
+ panelLabel = ariaLabel
+ panelClass = "absolute right-0 top-full z-40 mt-1 max-h-64 w-full min-w-48 overflow-auto rounded-lg border border-gray-300 bg-white py-1 shadow-xl dark:border-gray-600 dark:bg-gray-800"
+ panelContent = options |> List.map optionLink
+ }
+ }
+
+let articlesPage (state:ArticlesPageState) =
+ let filters = state.filters
+ let hasSearch = filters.search.IsSome
+ let hasActiveFilters = filters.tag.IsSome || filters.publishedYear.IsSome
+ let hasCriteria = hasSearch || hasActiveFilters
+ let clearSearchUrl = FilterState.url { filters with search = None }
+ let clearFiltersUrl = FilterState.url { filters with tag = None; publishedYear = None }
+ let tagOptions =
+ state.tags
+ |> List.map (fun tag ->
+ { label = tag
+ url = FilterState.url { filters with tag = Some tag }
+ selected = filters.tag = Some tag })
+ let yearOptions =
+ state.years
+ |> List.map (fun year ->
+ { label = string year
+ url = FilterState.url { filters with publishedYear = Some year }
+ selected = filters.publishedYear = Some year })
+ let searchInputClass =
+ "w-full rounded-md border border-gray-300 bg-white px-3 py-2 text-sm text-gray-900 shadow-sm outline-none transition placeholder:text-gray-400 focus:border-emerald-600 focus:ring-2 focus:ring-emerald-600/20 dark:border-gray-600 dark:bg-gray-900 dark:text-gray-100 dark:placeholder:text-gray-500 dark:focus:border-emerald-400 dark:focus:ring-emerald-400/20"
+ + if hasSearch then " pr-10" else ""
+
let content =
div {
- _class "mx-auto max-w-5xl py-10 px-4"
+ _class "mx-auto max-w-5xl px-4 py-10"
header {
- h1 { _class "text-4xl text-gray-900 dark:text-gray-100 font-medium"; "Articles" }
- p { _class "mt-4 text-lg text-gray-600 dark:text-gray-400"; "My thoughts on finance and technology" }
+ h1 { _class "text-4xl font-medium text-gray-900 dark:text-gray-100"; "Articles" }
+ p {
+ _class "mt-4 max-w-2xl text-lg leading-7 text-gray-600 dark:text-gray-400"
+ "Personal notes by Andy Meier. Unless otherwise noted, they reflect my own views—not those of clients, collaborators, or organizations I work with."
+ }
}
- div {
+ section {
+ _ariaLabel "Article search and filters"
_class "mt-10"
- div {
+ form {
+ _method "get"
+ _action "/articles"
+ _class "border-b border-gray-300/60 py-4 dark:border-gray-700/60"
+ match filters.tag with
+ | Some tag -> input { _type "hidden"; _name "tag"; _value (SafeOutput.attribute tag) }
+ | None -> ()
+ match filters.publishedYear with
+ | Some year -> input { _type "hidden"; _name "year"; _value (string year) }
+ | None -> ()
div {
- _class "flex flex-col space-y-4"
- for a in articles do ArticleCard.summary a
+ _class "flex flex-col gap-2 sm:flex-row sm:items-center"
+ label {
+ _class "min-w-0 flex-1"
+ span { _class "sr-only"; "Search articles" }
+ div {
+ _class "relative"
+ input {
+ _type "text"
+ _role "searchbox"
+ _name "search"
+ _ariaLabel "Search articles"
+ _value (filters.search |> Option.defaultValue "" |> SafeOutput.attribute)
+ _placeholder "Search articles"
+ _class searchInputClass
+ }
+ if hasSearch then
+ a {
+ _href clearSearchUrl
+ _ariaLabel "Clear search"
+ _class "absolute inset-y-0 right-0 inline-flex items-center px-3 text-gray-400 transition hover:text-gray-900 focus-visible:outline-2 focus-visible:outline-inset focus-visible:outline-emerald-600 dark:text-gray-500 dark:hover:text-gray-100 dark:focus-visible:outline-emerald-400"
+ span { _class "sr-only"; "Clear search" }
+ raw """"""
+ }
+ }
+ }
+ if filters.tag.IsNone || filters.publishedYear.IsNone then
+ details {
+ _class "group relative shrink-0"
+ summary {
+ _class "flex cursor-pointer list-none items-center justify-center rounded-md border border-gray-300 bg-white px-3 py-2 text-sm font-semibold text-gray-800 shadow-sm transition hover:bg-gray-50 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-emerald-600 dark:border-gray-600 dark:bg-gray-900 dark:text-gray-100 dark:hover:bg-gray-800"
+ "+ Add filter"
+ }
+ div {
+ _class "z-30 mt-2 grid w-full gap-3 rounded-lg border border-gray-300 bg-white p-3 shadow-xl sm:absolute sm:right-0 sm:w-auto sm:min-w-64 dark:border-gray-700 dark:bg-gray-900"
+ if filters.tag.IsNone then
+ div {
+ _class "grid gap-1 text-sm"
+ span { _class "font-medium text-gray-900 dark:text-gray-100"; "Tag" }
+ FilterControl.disclosure
+ "tag"
+ "Tag filter"
+ "Select tag"
+ tagOptions
+ "inline-flex w-full items-center justify-between rounded-md border border-gray-300 bg-white px-3 py-2 text-sm text-gray-900 shadow-sm transition hover:bg-gray-50 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-emerald-600 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100 dark:hover:bg-gray-700 dark:focus-visible:outline-emerald-400"
+ }
+ if filters.publishedYear.IsNone then
+ div {
+ _class "grid gap-1 text-sm"
+ span { _class "font-medium text-gray-900 dark:text-gray-100"; "Published" }
+ FilterControl.disclosure
+ "year"
+ "Published year filter"
+ "Select year"
+ yearOptions
+ "inline-flex w-full items-center justify-between rounded-md border border-gray-300 bg-white px-3 py-2 text-sm text-gray-900 shadow-sm transition hover:bg-gray-50 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-emerald-600 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100 dark:hover:bg-gray-700 dark:focus-visible:outline-emerald-400"
+ }
+ }
+ }
+ button { _type "submit"; _class "sr-only"; "Search" }
}
+ if hasActiveFilters then
+ div {
+ _class "mt-3 flex flex-wrap items-center gap-2"
+ match filters.tag with
+ | Some tag ->
+ div {
+ _class "inline-flex items-center rounded-md border border-gray-300 bg-gray-50 text-sm dark:border-gray-600 dark:bg-gray-800"
+ span { _class "border-r border-gray-300 px-2 py-1.5 font-medium text-gray-500 dark:border-gray-600 dark:text-gray-400"; "Tag" }
+ FilterControl.disclosure
+ "tag"
+ "Tag filter"
+ tag
+ tagOptions
+ "inline-flex items-center bg-transparent py-1.5 pl-2 pr-1 font-medium text-gray-900 focus-visible:outline-2 focus-visible:outline-inset focus-visible:outline-emerald-600 dark:text-gray-100 dark:focus-visible:outline-emerald-400"
+ a {
+ _href (FilterState.url { filters with tag = None })
+ _ariaLabel "Remove tag filter"
+ _class "inline-flex self-stretch items-center border-l border-gray-300 px-2 text-gray-500 transition hover:bg-gray-100 hover:text-gray-900 dark:border-gray-600 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-gray-100"
+ "×"
+ }
+ }
+ | None -> ()
+ match filters.publishedYear with
+ | Some year ->
+ div {
+ _class "inline-flex items-center rounded-md border border-gray-300 bg-gray-50 text-sm dark:border-gray-600 dark:bg-gray-800"
+ span { _class "border-r border-gray-300 px-2 py-1.5 font-medium text-gray-500 dark:border-gray-600 dark:text-gray-400"; "Published" }
+ FilterControl.disclosure
+ "year"
+ "Published year filter"
+ (string year)
+ yearOptions
+ "inline-flex items-center bg-transparent py-1.5 pl-2 pr-1 font-medium text-gray-900 focus-visible:outline-2 focus-visible:outline-inset focus-visible:outline-emerald-600 dark:text-gray-100 dark:focus-visible:outline-emerald-400"
+ a {
+ _href (FilterState.url { filters with publishedYear = None })
+ _ariaLabel "Remove published year filter"
+ _class "inline-flex self-stretch items-center border-l border-gray-300 px-2 text-gray-500 transition hover:bg-gray-100 hover:text-gray-900 dark:border-gray-600 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-gray-100"
+ "×"
+ }
+ }
+ | None -> ()
+ a {
+ _href clearFiltersUrl
+ _class "px-1 py-1.5 text-sm font-medium text-gray-500 transition hover:text-gray-900 hover:underline dark:text-gray-400 dark:hover:text-gray-100"
+ "Clear filters"
+ }
+ }
}
+ if state.articles.IsEmpty then
+ div {
+ _class "py-12 text-center"
+ h2 { _class "text-lg font-semibold text-gray-900 dark:text-gray-100"; "No articles found" }
+ if hasCriteria then
+ p { _class "mt-2 text-sm text-gray-600 dark:text-gray-400"; "Try changing or clearing your search and filters." }
+ else
+ p { _class "mt-2 text-sm text-gray-600 dark:text-gray-400"; "Articles will appear here when they are published." }
+ }
+ else
+ div {
+ _class "flex flex-col"
+ for article in state.articles do ArticleCard.summary article
+ }
}
}
Page.primary content
@@ -49,8 +273,8 @@ module RichTextView =
t.plainText
}
- match t.href with
- | Some href -> a { _href (toAnchorHref href); inner }
+ match t.href |> Option.map toAnchorHref |> Option.bind SafeOutput.tryLinkAttribute with
+ | Some href -> a { _href href; inner }
| None -> inner
module Block =
@@ -61,7 +285,7 @@ module Block =
| _ -> Other
let rec toHtml (block:Domain.Notion.Block) : HtmlElement =
- let cleanId = block.id.Replace("-", "")
+ let cleanId = block.id.Replace("-", "") |> SafeOutput.attribute
match block.blockType with
| Domain.Notion.BlockType.Heading1 richText ->
@@ -101,15 +325,18 @@ module Block =
| "JSON" -> "json"
| "TOML" -> "toml"
| other -> other
+ let languageClass = $"language-{language}" |> SafeOutput.attribute
pre {
- _class $"language-{language}"
+ _class languageClass
code {
- _class $"language-{language}"
+ _class languageClass
for t in richText do RichTextView.toHtml t
}
}
| Domain.Notion.BlockType.Image url ->
- img { _class "drop-shadow-xl rounded"; _src url }
+ match SafeOutput.tryImageAttribute url with
+ | Some safeUrl -> img { _class "drop-shadow-xl rounded"; _src safeUrl; _alt "" }
+ | None -> empty
| Domain.Notion.BlockType.Divider ->
div { _class "border-b-2 border-gray-300/60 dark:border-gray-700/60" }
| Domain.Notion.BlockType.Quote richText ->
@@ -161,7 +388,9 @@ let articlePage (article':Article) =
div {
div {
_class "bg-cover bg-no-repeat bg-center bg-blend-overlay bg-gray-800"
- _style $"background-image: url('{article'.cover}')"
+ match SafeOutput.tryBackgroundImageStyle article'.cover with
+ | Some style -> _style style
+ | None -> ()
div {
_class "pt-28 pb-20 px-4 mx-auto max-w-5xl flex flex-col justify-end items-start text-gray-50"
time {
@@ -173,6 +402,10 @@ let articlePage (article':Article) =
_class "mt-4 text-4xl font-bold tracking-tight text-gray-50"
article'.title
}
+ div {
+ _class "mt-5"
+ ArticleCard.tags article'.tags
+ }
}
}
article {
diff --git a/app/src/App/src/Common/Handler.fs b/app/src/App/src/Common/Handler.fs
index 5e1ab93..991cf91 100644
--- a/app/src/App/src/Common/Handler.fs
+++ b/app/src/App/src/Common/Handler.fs
@@ -6,6 +6,7 @@ open Giraffe
open FSharp.ViewEngine
open Microsoft.AspNetCore.Http
open StarFederation.Datastar.DependencyInjection
+open System.Text.Json
let patchElement (ds:IDatastarService) (element:HtmlElement) = task {
let html = Render.toString element
@@ -16,10 +17,12 @@ let inline patchSignals (ds:IDatastarService) (signals:'T) = task {
do! ds.PatchSignalsAsync(signals)
}
+let historyScript (url:string) =
+ let serializedUrl = JsonSerializer.Serialize url
+ $"""window.history.pushState(null, '', {serializedUrl});window.trackSnowplowPageView&&window.trackSnowplowPageView();"""
+
let pushUrl (ds:IDatastarService) (url:string) = task {
- // language=javascript
- let js = $"""window.history.pushState(null, '', '{url}');window.trackSnowplowPageView&&window.trackSnowplowPageView();"""
- do! ds.ExecuteScriptAsync(js)
+ do! ds.ExecuteScriptAsync(historyScript url)
}
let renderPage (services:Services) (page:HtmlElement) (selectedNav:string) : HttpHandler =
diff --git a/app/src/App/src/Common/View.fs b/app/src/App/src/Common/View.fs
index c09cd88..0583af3 100644
--- a/app/src/App/src/Common/View.fs
+++ b/app/src/App/src/Common/View.fs
@@ -2,12 +2,13 @@ module App.Common.View
open FSharp.ViewEngine
open Domain.Article
+open System
open System.Collections.Generic
open System.IO
+open System.Net
open System.Text.Json
open type Html
open type Datastar
-open type Tailwind
module Asset =
let resolveWithManifest (manifest:IReadOnlyDictionary) (path:string) =
@@ -31,6 +32,52 @@ module Asset =
let fingerprinted (path:string) =
resolveWithManifest manifest.Value path
+module SafeOutput =
+ let private linkSchemes = Set.ofList [ Uri.UriSchemeHttp; Uri.UriSchemeHttps; Uri.UriSchemeMailto ]
+ let private imageSchemes = Set.ofList [ Uri.UriSchemeHttp; Uri.UriSchemeHttps ]
+
+ let attribute (value:string) =
+ WebUtility.HtmlEncode value
+
+ let private tryAbsoluteUrl (allowedSchemes:Set) (value:string) =
+ match Uri.TryCreate(value, UriKind.Absolute) with
+ | true, uri when allowedSchemes.Contains(uri.Scheme.ToLowerInvariant()) ->
+ uri.GetComponents(UriComponents.AbsoluteUri, UriFormat.UriEscaped) |> Some
+ | _ -> None
+
+ let tryLinkAttribute (value:string) =
+ if value.StartsWith("#", StringComparison.Ordinal) then
+ value |> attribute |> Some
+ elif value.StartsWith("/", StringComparison.Ordinal) && not (value.StartsWith("//", StringComparison.Ordinal)) then
+ value |> attribute |> Some
+ else
+ value
+ |> tryAbsoluteUrl linkSchemes
+ |> Option.map attribute
+
+ let tryImageAttribute (value:string) =
+ value
+ |> tryAbsoluteUrl imageSchemes
+ |> Option.map attribute
+
+ let tryBackgroundImageStyle (value:string) =
+ value
+ |> tryAbsoluteUrl imageSchemes
+ |> Option.map (fun url ->
+ let cssUrl =
+ url
+ .Replace("\\", "%5C", StringComparison.Ordinal)
+ .Replace("'", "%27", StringComparison.Ordinal)
+ .Replace("\"", "%22", StringComparison.Ordinal)
+ .Replace("(", "%28", StringComparison.Ordinal)
+ .Replace(")", "%29", StringComparison.Ordinal)
+
+ $"background-image: url('{cssUrl}')" |> attribute)
+
+module SiteUrl =
+ let article (permalink:string) =
+ $"/articles/{Uri.EscapeDataString permalink}"
+
module MiniIcon =
let github =
raw """
@@ -93,6 +140,53 @@ module MiniIcon =
let close =
raw """"""
+type DisclosureConfig =
+ { id: string
+ openSignal: string
+ triggerLabel: string
+ rootClass: string
+ triggerClass: string
+ triggerContent: HtmlElement
+ panelLabel: string
+ panelClass: string
+ panelContent: HtmlElement list }
+
+module Disclosure =
+ let panel (config:DisclosureConfig) =
+ let buttonId = config.id + "-button"
+ let panelId = config.id + "-panel"
+ let escapeExpression =
+ $"evt.key == 'Escape' && ${config.openSignal} && ((${config.openSignal} = false), document.getElementById('{buttonId}').focus(), true)"
+
+ div {
+ _class config.rootClass
+ _data ("disclosure-root", "")
+ _data ("signals", $"{{ {config.openSignal}: false }}")
+ _data ("on:keydown__window", escapeExpression)
+ button {
+ _id buttonId
+ _type "button"
+ _ariaLabel config.triggerLabel
+ _attr ("aria-controls", panelId)
+ _data ("disclosure-button", "")
+ _data ("attr:aria-expanded", $"${config.openSignal} ? 'true' : 'false'")
+ _data ("on:click__stop", $"${config.openSignal} = !${config.openSignal}")
+ _class config.triggerClass
+ config.triggerContent
+ }
+ div {
+ _id panelId
+ _role "group"
+ _ariaLabel config.panelLabel
+ _data ("disclosure-panel", "")
+ _data ("show", $"${config.openSignal}")
+ _data ("on:click__outside", $"${config.openSignal} = false")
+ _style "display:none"
+ _class config.panelClass
+ for item in config.panelContent do item
+ }
+ }
+
module ArticleCard =
let private tag (text:string) =
span {
@@ -100,10 +194,15 @@ module ArticleCard =
text
}
+ let tags (tags:string[]) =
+ div {
+ _class "flex flex-wrap gap-2"
+ for tag' in tags do tag tag'
+ }
+
let summary (article':Article) =
- let url = $"/articles/{article'.permalink}"
+ let url = SiteUrl.article article'.permalink
article {
- _id article'.permalink
_class "py-6 border-b border-gray-300/60 dark:border-gray-700/60"
div {
_class "flex items-center flex-wrap gap-x-4 gap-y-1 text-sm text-gray-400 dark:text-gray-500"
@@ -127,8 +226,8 @@ module ArticleCard =
}
p { _class "mt-2 text-base text-gray-600 dark:text-gray-400"; article'.summary }
div {
- _class "mt-4 flex flex-wrap gap-2"
- for t in article'.tags do tag t
+ _class "mt-4"
+ tags article'.tags
}
}
@@ -146,8 +245,7 @@ module Footer =
div {
_class "text-sm flex flex-col space-y-1"
a { _class "underline cursor-pointer hover:text-emerald-600 dark:hover:text-emerald-400"; _dataOn ("click", "@get('/articles')"); "Articles" }
- a { _class "underline cursor-pointer hover:text-emerald-600 dark:hover:text-emerald-400"; _dataOn ("click", "@get('/services')"); "Services" }
- a { _class "underline cursor-pointer hover:text-emerald-600 dark:hover:text-emerald-400"; _dataOn ("click", "@get('/projects')"); "Projects" }
+ a { _href "https://meiermade.com"; _class "whitespace-nowrap underline hover:text-emerald-600 dark:hover:text-emerald-400"; "Meier Made" }
}
}
@@ -171,98 +269,73 @@ module TopNav =
}
let private mobileItem (id:string, label:string, href:string) =
- button {
+ a {
_id $"{id}-mobile"
- _type "button"
- _class "block w-full text-left px-4 py-2 text-sm cursor-pointer hover:bg-gray-100 hover:text-emerald-600 dark:hover:bg-gray-700/50 dark:hover:text-emerald-400"
+ _href href
+ _class "block w-full cursor-pointer px-4 py-2 text-left text-sm transition hover:bg-gray-100 hover:text-emerald-600 focus:bg-gray-100 focus:text-emerald-600 focus-visible:outline-2 focus-visible:outline-inset focus-visible:outline-emerald-600 dark:hover:bg-gray-700/50 dark:hover:text-emerald-400 dark:focus:bg-gray-700/50 dark:focus:text-emerald-400 dark:focus-visible:outline-emerald-400"
_dataClass ("text-emerald-600", $"$selectedNav == '{id}'")
_dataClass ("dark:text-emerald-400", $"$selectedNav == '{id}'")
_dataClass ("font-semibold", $"$selectedNav == '{id}'")
_dataClass ("text-gray-700", $"$selectedNav != '{id}'")
_dataClass ("dark:text-gray-300", $"$selectedNav != '{id}'")
- _dataOn ("click", $"@get('{href}')")
+ _dataOn ("click__prevent", $"@get('{href}')")
+ text label
+ }
+
+ let private companyLink (className:string) =
+ a {
+ _href "https://meiermade.com"
+ _class className
+ text "Meier Made"
+ }
+
+ let private themeItem (value:string) (label:string) (icon:HtmlElement) =
+ button {
+ _type "button"
+ _class "flex w-full items-center gap-2 px-4 py-2 text-sm transition hover:bg-gray-100 focus:bg-gray-100 focus-visible:outline-2 focus-visible:outline-inset focus-visible:outline-emerald-600 dark:hover:bg-gray-700/50 dark:focus:bg-gray-700/50 dark:focus-visible:outline-emerald-400"
+ _dataClass ("text-emerald-600", $"$theme == '{value}'")
+ _dataClass ("dark:text-emerald-400", $"$theme == '{value}'")
+ _dataClass ("font-semibold", $"$theme == '{value}'")
+ _dataClass ("text-gray-700", $"$theme != '{value}'")
+ _dataClass ("dark:text-gray-300", $"$theme != '{value}'")
+ _dataOn ("click", $"$theme = '{value}'; $themeOpen = false; setTheme('{value}'); document.getElementById('theme-button').focus()")
+ icon
text label
}
let private themeToggle =
- elDropdown {
- _class "relative inline-block text-left"
- button {
- _id "theme-toggle"
- _type "button"
- _class [
- "inline-flex w-full items-center justify-center rounded-md p-2 text-gray-600 hover:text-emerald-600 hover:bg-gray-100"
- "dark:text-gray-400 dark:hover:text-emerald-400 dark:hover:bg-gray-800"
- "hover:cursor-pointer"
- ]
- MiniIcon.sun
- MiniIcon.moon
- }
- elMenu {
- _popover
- _anchor "bottom end"
- _class "mt-2 w-36 origin-top-right rounded-md bg-white py-1 shadow-lg ring-1 ring-black/5 dark:bg-gray-800 dark:ring-white/10"
- button {
- _type "button"
- _class "flex w-full items-center gap-2 px-4 py-2 text-sm hover:bg-gray-100 dark:hover:bg-gray-700/50"
- _dataClass ("text-emerald-600", "$theme == 'light'")
- _dataClass ("dark:text-emerald-400", "$theme == 'light'")
- _dataClass ("font-semibold", "$theme == 'light'")
- _dataClass ("text-gray-700", "$theme != 'light'")
- _dataClass ("dark:text-gray-300", "$theme != 'light'")
- _dataOn ("click", "$theme = 'light'; setTheme('light')")
- MiniIcon.sunSmall
- text "Light"
- }
- button {
- _type "button"
- _class "flex w-full items-center gap-2 px-4 py-2 text-sm hover:bg-gray-100 dark:hover:bg-gray-700/50"
- _dataClass ("text-emerald-600", "$theme == 'dark'")
- _dataClass ("dark:text-emerald-400", "$theme == 'dark'")
- _dataClass ("font-semibold", "$theme == 'dark'")
- _dataClass ("text-gray-700", "$theme != 'dark'")
- _dataClass ("dark:text-gray-300", "$theme != 'dark'")
- _dataOn ("click", "$theme = 'dark'; setTheme('dark')")
- MiniIcon.moonSmall
- text "Dark"
- }
- button {
- _type "button"
- _class "flex w-full items-center gap-2 px-4 py-2 text-sm hover:bg-gray-100 dark:hover:bg-gray-700/50"
- _dataClass ("text-emerald-600", "$theme == 'system'")
- _dataClass ("dark:text-emerald-400", "$theme == 'system'")
- _dataClass ("font-semibold", "$theme == 'system'")
- _dataClass ("text-gray-700", "$theme != 'system'")
- _dataClass ("dark:text-gray-300", "$theme != 'system'")
- _dataOn ("click", "$theme = 'system'; setTheme('system')")
- MiniIcon.monitor
- text "System"
- }
- }
+ Disclosure.panel {
+ id = "theme"
+ openSignal = "themeOpen"
+ triggerLabel = "Choose theme"
+ rootClass = "relative inline-block text-left"
+ triggerClass = "inline-flex w-full items-center justify-center rounded-md p-2 text-gray-600 hover:cursor-pointer hover:bg-gray-100 hover:text-emerald-600 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-emerald-600 dark:text-gray-400 dark:hover:bg-gray-800 dark:hover:text-emerald-400 dark:focus-visible:outline-emerald-400"
+ triggerContent = span { _class "contents"; MiniIcon.sun; MiniIcon.moon }
+ panelLabel = "Theme"
+ panelClass = "absolute right-0 top-full z-40 mt-2 w-36 origin-top-right rounded-md bg-white py-1 shadow-lg ring-1 ring-black/5 dark:bg-gray-800 dark:ring-white/10"
+ panelContent =
+ [ themeItem "light" "Light" MiniIcon.sunSmall
+ themeItem "dark" "Dark" MiniIcon.moonSmall
+ themeItem "system" "System" MiniIcon.monitor ]
}
let private mobileDropdown =
- elDropdown {
- _class "relative inline-block text-left md:hidden"
- button {
- _id "menu-toggle"
- _type "button"
- _class [
- "inline-flex w-full items-center justify-center rounded-md p-2 text-gray-600 hover:text-emerald-600 hover:bg-gray-100"
- "dark:text-gray-400 dark:hover:text-emerald-400 dark:hover:bg-gray-800"
- "hover:cursor-pointer"
- ]
- MiniIcon.hamburger
- }
- elMenu {
- _id "mobile-menu"
- _popover
- _anchor "bottom end"
- _class "mt-2 w-56 origin-top-right rounded-md bg-white py-1 shadow-lg ring-1 ring-black/5 dark:bg-gray-800 dark:ring-white/10"
- mobileItem("nav-articles", "Articles", "/articles")
- mobileItem("nav-projects", "Projects", "/projects")
- mobileItem("nav-services", "Services", "/services")
- }
+ Disclosure.panel {
+ id = "navigation"
+ openSignal = "navigationOpen"
+ triggerLabel = "Open navigation"
+ rootClass = "relative inline-block text-left md:hidden"
+ triggerClass = "inline-flex w-full items-center justify-center rounded-md p-2 text-gray-600 hover:cursor-pointer hover:bg-gray-100 hover:text-emerald-600 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-emerald-600 dark:text-gray-400 dark:hover:bg-gray-800 dark:hover:text-emerald-400 dark:focus-visible:outline-emerald-400"
+ triggerContent = span { _class "contents"; MiniIcon.hamburger }
+ panelLabel = "Navigation"
+ panelClass = "absolute right-0 top-full z-40 mt-2 w-56 origin-top-right rounded-md bg-white py-1 shadow-lg ring-1 ring-black/5 dark:bg-gray-800 dark:ring-white/10"
+ panelContent =
+ [ mobileItem("nav-articles", "Articles", "/articles")
+ a {
+ _href "https://meiermade.com"
+ _class "block w-full px-4 py-2 text-left text-sm text-gray-700 transition hover:bg-gray-100 hover:text-emerald-600 focus:bg-gray-100 focus:text-emerald-600 focus-visible:outline-2 focus-visible:outline-inset focus-visible:outline-emerald-600 dark:text-gray-300 dark:hover:bg-gray-700/50 dark:hover:text-emerald-400 dark:focus:bg-gray-700/50 dark:focus:text-emerald-400 dark:focus-visible:outline-emerald-400"
+ "Meier Made"
+ } ]
}
let primary =
@@ -278,8 +351,7 @@ module TopNav =
div {
_class "hidden md:flex items-center gap-4"
item("nav-articles", text "Articles", "/articles")
- item("nav-projects", text "Projects", "/projects")
- item("nav-services", text "Services", "/services")
+ companyLink "p-2 text-sm font-semibold text-gray-800 hover:text-emerald-600 dark:text-gray-200 dark:hover:text-emerald-400"
}
themeToggle
mobileDropdown
@@ -305,7 +377,6 @@ type Document =
}
link { _href (Asset.fingerprinted "/css/compiled.css"); _rel "stylesheet" }
link { _href (Asset.fingerprinted "/css/prism.css"); _rel "stylesheet" }
- script { _type "module"; _src (Asset.fingerprinted "/scripts/tailwindplus-elements.1.js") }
script { _type "module"; _src (Asset.fingerprinted "/scripts/datastar.1.0.0-RC.6.js") }
}
body {
@@ -319,35 +390,36 @@ type Document =
}
div {
_id "cookie-consent-banner"
- _class "hidden fixed inset-x-0 bottom-0 z-50 border-t border-gray-300 bg-white/95 p-4 shadow-2xl backdrop-blur dark:border-gray-700 dark:bg-gray-900/95"
_role "dialog"
+ _ariaLabelledby "analytics-consent-title"
+ _ariaDescribedby "analytics-consent-description"
_ariaLive "polite"
+ _class "pointer-events-none fixed inset-x-0 bottom-0 z-50 hidden px-4 pb-4 sm:px-6 sm:pb-6"
div {
- _class "mx-auto flex max-w-5xl flex-col gap-4 md:flex-row md:items-center md:justify-between"
- div {
- _class "max-w-3xl"
- p {
- _class "text-sm font-semibold text-gray-900 dark:text-gray-100"
- "Analytics cookies"
- }
- p {
- _class "mt-1 text-sm text-gray-600 dark:text-gray-300"
- "I use analytics to measure how this site is used. You can accept or reject analytics cookies, and the site will work either way."
- }
+ _class "pointer-events-auto ml-auto max-w-xl rounded-2xl border border-gray-200 bg-white p-5 shadow-2xl shadow-gray-950/15 sm:p-6 dark:border-gray-700 dark:bg-gray-900 dark:shadow-black/30"
+ h2 {
+ _id "analytics-consent-title"
+ _class "text-base font-semibold text-gray-950 dark:text-gray-50"
+ "Optional analytics"
+ }
+ p {
+ _id "analytics-consent-description"
+ _class "mt-2 text-sm/6 text-gray-600 dark:text-gray-300"
+ "Google Analytics and Snowplow are loaded only if you accept. They help me understand site usage; declining does not affect the site."
}
div {
- _class "flex flex-col gap-2 sm:flex-row"
+ _class "mt-5 flex flex-col-reverse gap-3 sm:flex-row sm:justify-end"
button {
_type "button"
- _class "inline-flex items-center justify-center rounded-md border border-gray-300 px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-100 hover:cursor-pointer dark:border-gray-600 dark:text-gray-200 dark:hover:bg-gray-800"
+ _class "rounded-full border border-gray-300 px-4 py-2.5 text-sm font-semibold text-gray-800 transition hover:bg-gray-50 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-emerald-600 dark:border-gray-600 dark:text-gray-100 dark:hover:bg-gray-800"
_onclick "setAnalyticsConsent('declined')"
- "Reject"
+ "Decline"
}
button {
_type "button"
- _class "inline-flex items-center justify-center rounded-md bg-emerald-600 px-4 py-2 text-sm font-semibold text-white hover:bg-emerald-500 hover:cursor-pointer dark:bg-emerald-500 dark:hover:bg-emerald-400"
+ _class "rounded-full bg-emerald-600 px-4 py-2.5 text-sm font-semibold text-white shadow-sm transition hover:bg-emerald-500 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-emerald-500 dark:hover:bg-emerald-400"
_onclick "setAnalyticsConsent('accepted')"
- "Accept"
+ "Accept analytics"
}
}
}
diff --git a/app/src/App/src/Index/Handler.fs b/app/src/App/src/Index/Handler.fs
index c1651ba..f019c50 100644
--- a/app/src/App/src/Index/Handler.fs
+++ b/app/src/App/src/Index/Handler.fs
@@ -28,7 +28,7 @@ let handler (services:Services) : HttpHandler =
choose [
route "/health" >=> GET >=> text "Healthy"
routex "(/?)" >=> GET >=> getHomePage services
- App.Services.Handler.handler services
- App.Projects.Handler.handler services
+ route "/services" >=> GET >=> redirectTo true "https://meiermade.com/services"
+ route "/projects" >=> GET >=> redirectTo true "https://meiermade.com/projects"
subRoute "/articles" (App.Articles.Handler.handler services)
]
diff --git a/app/src/App/src/Index/View.fs b/app/src/App/src/Index/View.fs
index d88fd73..a51c505 100644
--- a/app/src/App/src/Index/View.fs
+++ b/app/src/App/src/Index/View.fs
@@ -11,15 +11,6 @@ let aboutMe =
p {
_class "mt-4 text-lg text-gray-600 dark:text-gray-400"
"Engineer working in finance, accounting, and capital markets."
- br
- br
- "Currently working at "
- a {
- _href "https://aplazo.mx/"
- _class "underline hover:text-emerald-600 dark:hover:text-emerald-400"
- "Aplazo"
- }
- "."
}
let homePage (recentArticles:Article list) =
diff --git a/app/src/App/src/Program.fs b/app/src/App/src/Program.fs
index fd96f41..4cf03a0 100644
--- a/app/src/App/src/Program.fs
+++ b/app/src/App/src/Program.fs
@@ -3,6 +3,7 @@ open App.ServiceRegistry
open Domain
open Giraffe
open Microsoft.AspNetCore.Builder
+open Microsoft.AspNetCore.Http
open Microsoft.Extensions.DependencyInjection
open OpenTelemetry
open OpenTelemetry.Exporter
@@ -55,8 +56,18 @@ let configureServices (serviceCollection: IServiceCollection) (tracerProvider: T
.AddGiraffe()
|> ignore
+let private addSecurityHeaders (ctx:HttpContext) =
+ ctx.Response.Headers["Content-Security-Policy"] <- "base-uri 'self'; frame-ancestors 'none'; object-src 'none'"
+ ctx.Response.Headers["Permissions-Policy"] <- "camera=(), geolocation=(), microphone=()"
+ ctx.Response.Headers["Referrer-Policy"] <- "strict-origin-when-cross-origin"
+ ctx.Response.Headers["Strict-Transport-Security"] <- "max-age=31536000; includeSubDomains"
+ ctx.Response.Headers["X-Content-Type-Options"] <- "nosniff"
+
let configureApp (services: Services) (app: WebApplication) =
app
+ .Use(fun (ctx:HttpContext) (next:RequestDelegate) ->
+ addSecurityHeaders ctx
+ next.Invoke ctx)
.UseSerilogRequestLogging(fun opts ->
opts.GetLevel <- fun ctx _ _ ->
if ctx.Request.Path.Value = "/health" then LogEventLevel.Verbose
diff --git a/app/src/App/src/Projects/Handler.fs b/app/src/App/src/Projects/Handler.fs
deleted file mode 100644
index d8fb500..0000000
--- a/app/src/App/src/Projects/Handler.fs
+++ /dev/null
@@ -1,26 +0,0 @@
-module App.Projects.Handler
-
-open App.Infrastructure
-open App.Common.Handler
-open App.ServiceRegistry
-open App.Projects
-open Giraffe
-open Microsoft.AspNetCore.Http
-open StarFederation.Datastar.DependencyInjection
-
-let private getPage (services:Services) : HttpHandler =
- fun next ctx -> task {
- use _span = services.telemetry.startActiveSpan "app.projects.get_page"
- let page = View.page
- if ctx.IsDatastar then
- let ds = ctx.GetService()
- do! patchSignals ds {| selectedNav = "nav-projects" |}
- do! patchElement ds page
- do! pushUrl ds "/projects"
- return Some ctx
- else
- return! renderPage services page "nav-projects" next ctx
- }
-
-let handler (services:Services) : HttpHandler =
- route "/projects" >=> GET >=> getPage services
diff --git a/app/src/App/src/Projects/View.fs b/app/src/App/src/Projects/View.fs
deleted file mode 100644
index 190b801..0000000
--- a/app/src/App/src/Projects/View.fs
+++ /dev/null
@@ -1,58 +0,0 @@
-module App.Projects.View
-
-open FSharp.ViewEngine
-open App.Common.View
-open type Html
-
-type ProjectCardProps =
- { title: string
- description: string
- logoSrc: string
- logoAlt: string
- href: string
- label: string }
-
-let private projectCard (props: ProjectCardProps) =
- a {
- _href props.href
- _class "group block p-6 rounded-2xl border border-gray-300/60 dark:border-gray-700/60 transition-colors hover:bg-white dark:hover:bg-gray-800"
- div {
- _class "flex items-center gap-4"
- img {
- _class "h-12 w-12 rounded-xl object-cover bg-gray-100 dark:bg-gray-900 p-1"
- _src props.logoSrc
- _alt props.logoAlt
- }
- h2 { _class "text-2xl font-semibold tracking-tight text-gray-900 dark:text-gray-100"; props.title }
- }
- p { _class "mt-4 text-base text-gray-700 dark:text-gray-300"; props.description }
- p { _class "mt-5 text-sm text-gray-600 dark:text-gray-200 transition-colors group-hover:text-emerald-600 dark:group-hover:text-emerald-400"; props.label }
- }
-
-let page =
- let content =
- div {
- _class "mx-auto max-w-5xl py-10 px-4"
- h1 { _class "text-4xl text-gray-900 dark:text-gray-100 font-medium"; "Projects" }
- p { _class "mt-4 text-lg text-gray-600 dark:text-gray-400"; "Things I am working on." }
- div {
- _class "mt-10 grid gap-8 md:grid-cols-2"
- projectCard {
- title = "FSharp.ViewEngine"
- description = "A minimal, fast view engine for F# with a clean computation-expression DSL."
- logoSrc = Asset.fingerprinted "/images/fsharpviewengine.svg"
- logoAlt = "FSharp.ViewEngine logo"
- href = "https://fsharpviewengine.meiermade.com"
- label = "fsharpviewengine.meiermade.com"
- }
- projectCard {
- title = "Geldos"
- description = "A financial operating system for building modern finance and accounting workflows."
- logoSrc = Asset.fingerprinted "/images/geldos.svg"
- logoAlt = "Geldos logo"
- href = "https://geldos.com"
- label = "geldos.com"
- }
- }
- }
- Page.primary content
diff --git a/app/src/App/src/Services/Handler.fs b/app/src/App/src/Services/Handler.fs
deleted file mode 100644
index 06f453e..0000000
--- a/app/src/App/src/Services/Handler.fs
+++ /dev/null
@@ -1,28 +0,0 @@
-module App.Services.Handler
-
-open App.Infrastructure
-open App.ServiceRegistry
-open App.Common.Handler
-open Giraffe
-open Microsoft.AspNetCore.Http
-open StarFederation.Datastar.DependencyInjection
-
-let private getPage (services:Services) : HttpHandler =
- fun next ctx -> task {
- use _span = services.telemetry.startActiveSpan "app.services.get_page"
- let page = View.page
-
- if ctx.IsDatastar then
- let ds = ctx.GetService()
- do! patchSignals ds {| selectedNav = "nav-services" |}
- do! patchElement ds page
- do! pushUrl ds "/services"
- return Some ctx
- else
- return! renderPage services page "nav-services" next ctx
- }
-
-let handler (services:Services) : HttpHandler =
- choose [
- route "/services" >=> GET >=> getPage services
- ]
diff --git a/app/src/App/src/Services/View.fs b/app/src/App/src/Services/View.fs
deleted file mode 100644
index cd9578c..0000000
--- a/app/src/App/src/Services/View.fs
+++ /dev/null
@@ -1,99 +0,0 @@
-module App.Services.View
-
-open FSharp.ViewEngine
-open App.Common.View
-open type Html
-
-let private iconSecureInfrastructure =
- raw """
-
- """
-
-let private iconPrivateNetwork =
- raw """
-
- """
-
-let private iconIngestion =
- raw """
-
- """
-
-let private iconWarehouse =
- raw """
-
- """
-
-let private iconFinancialModels =
- raw """
-
- """
-
-let private iconInternalAppsAi =
- raw """
-
- """
-
-let private serviceCard (icon: HtmlElement) (title: string) (description: string) : HtmlElement =
- div {
- _class "rounded-xl border border-gray-300/60 dark:border-gray-700/60 p-4"
- div {
- _class "text-emerald-600 dark:text-emerald-400"
- icon
- }
- h2 { _class "mt-3 text-xl font-semibold text-gray-900 dark:text-gray-100"; title }
- p { _class "mt-2 text-base text-gray-600 dark:text-gray-400"; description }
- }
-
-let page =
- let content =
- div {
- _class "mx-auto max-w-5xl py-10 px-4"
- h1 { _class "text-4xl text-gray-900 dark:text-gray-100 font-medium"; "Services" }
- p {
- _class "mt-4 text-lg text-gray-600 dark:text-gray-400"
- "I help businesses build and scale their finance, accounting, and capital markets infrastructure."
- }
- div {
- _class "mt-10 grid grid-cols-1 gap-4 md:grid-cols-2"
- serviceCard iconSecureInfrastructure "Secure private infrastructure" "Design and deploy secure, cost-efficient infrastructure with Kubernetes and Pulumi. Enable private networking and access controls with Cloudflare Zero Trust."
- serviceCard iconWarehouse "Data engineering and analytics" "Build reliable ingestion and orchestration pipelines with tools like Airbyte and Dagster. Model analytics layers with dbt in BigQuery, Redshift, Snowflake or other warehouses."
- serviceCard iconFinancialModels "Financial models" "Develop operating models, borrowing base reports, investor reporting, and more in Excel and Google Sheets. Create bespoke F# and Python models for complex scenarios."
- serviceCard iconInternalAppsAi "Internal apps and AI tooling" "Build secure internal web apps and MCP servers in F# or Python. Help set up AI coding agents and skills to automate workflows."
- }
- section {
- _class "mt-10 max-w-3xl mx-auto rounded-xl border border-gray-300/60 dark:border-gray-700/60 p-6"
- div {
- _class "flex flex-col items-start gap-4 sm:flex-row sm:items-center sm:justify-between sm:gap-6"
- div {
- _class "min-w-0"
- h2 { _class "text-2xl text-gray-900 dark:text-gray-100 font-medium"; "Get in touch" }
- p {
- _class "mt-2 text-gray-600 dark:text-gray-400"
- "Schedule a quick 30-minute call to see if I can help."
- }
- }
- a {
- _href "https://calendar.app.google/wQPWAYBda4r2S7o48"
- _target "_blank"
- _rel "noopener noreferrer"
- _class "inline-flex shrink-0 justify-center rounded-md bg-emerald-600 hover:bg-emerald-700 text-white px-4 py-2 font-medium"
- "Schedule 30 minutes"
- }
- }
- }
- }
-
- Page.primary content
diff --git a/app/src/App/wwwroot/scripts/tailwindplus-elements.1.js b/app/src/App/wwwroot/scripts/tailwindplus-elements.1.js
deleted file mode 100644
index 77a9371..0000000
--- a/app/src/App/wwwroot/scripts/tailwindplus-elements.1.js
+++ /dev/null
@@ -1,9 +0,0 @@
-/**
- * Minified by jsDelivr using Terser v5.39.0.
- * Original file: /npm/@tailwindplus/elements@1.0.22/dist/index.js
- *
- * Do NOT use SRI with dynamically generated files! More information: https://www.jsdelivr.com/using-sri-with-dynamic-files
- */
-/*! @tailwindplus/elements v1.0.22 | Proprietary License | https://tailwindcss.com/plus/license */
-var Pn=Object.defineProperty,So=e=>{throw TypeError(e)},Cn=(e,t,n)=>t in e?Pn(e,t,{enumerable:!0,configurable:!0,writable:!0,value:n}):e[t]=n,M=(e,t,n)=>Cn(e,"symbol"!=typeof t?t+"":t,n),Ot=(e,t,n)=>t.has(e)||So("Cannot "+n),d=(e,t,n)=>(Ot(e,t,"read from private field"),n?n.call(e):t.get(e)),L=(e,t,n)=>t.has(e)?So("Cannot add the same private member more than once"):t instanceof WeakSet?t.add(e):t.set(e,n),I=(e,t,n,o)=>(Ot(e,t,"write to private field"),o?o.call(e,n):t.set(e,n),n),H=(e,t,n)=>(Ot(e,t,"access private method"),n);if(typeof globalThis.window<"u"){let e=!1;document.addEventListener("submit",(t=>{if(e)return void(e=!1);let n=t.target;if(n&&"dialog"===n.method){let o=n.closest("el-dialog");if(!o||!("beforeClose"in o))return;let r=t.submitter?.value??"",i=o.beforeClose(r);if(!0===i||(t.preventDefault(),t.stopImmediatePropagation(),!1===i))return;i.then((o=>{o&&(e=!0,n.dispatchEvent(t))})).catch(console.error)}}),!0)}var at=class extends Event{constructor(e,{oldState:t="",newState:n="",...o}={}){super(e,o),M(this,"oldState"),M(this,"newState"),this.oldState=String(t||""),this.newState=String(n||"")}},Lo=new WeakMap;function Mo(e,t,n){Lo.set(e,setTimeout((()=>{Lo.has(e)&&e.dispatchEvent(new at("toggle",{cancelable:!1,oldState:t,newState:n}))}),0))}var Nt=globalThis.ShadowRoot||function(){},On=globalThis.HTMLDialogElement||function(){},rt=new WeakMap,X=new WeakMap,q=new WeakMap,xe=new WeakMap;function it(e){return xe.get(e)||"hidden"}var st=new WeakMap;function qe(e){return[...e].pop()}function Dn(e){let t=e.popoverTargetElement;if(!(t instanceof HTMLElement))return;let n=it(t);"show"===e.popoverTargetAction&&"showing"===n||"hide"===e.popoverTargetAction&&"hidden"===n||("showing"===n?Se(t,!0,!0):pe(t,!1)&&(st.set(t,e),Rt(t)))}function pe(e,t){return!("auto"!==e.popover&&"manual"!==e.popover&&"hint"!==e.popover||!e.isConnected||t&&"showing"!==it(e)||!t&&"hidden"!==it(e)||e instanceof On&&e.hasAttribute("open")||document.fullscreenElement===e)}function Io(e){if(!e)return 0;let t=X.get(document)||new Set,n=q.get(document)||new Set;return n.has(e)?[...n].indexOf(e)+t.size+1:t.has(e)?[...t].indexOf(e)+1:0}function Mn(e){let t=Ho(e),n=Hn(e);return Io(t)>Io(n)?t:n}function Ve(e){let t,n=q.get(e)||new Set,o=X.get(e)||new Set,r=n.size>0?n:o.size>0?o:null;return r?(t=qe(r),t.isConnected?t:(r.delete(t),Ve(e))):null}function ko(e){for(let t of e||[]){if(t.isConnected)return t;e.delete(t)}return null}function Te(e){return"function"==typeof e.getRootNode?e.getRootNode():e.parentNode?Te(e.parentNode):e}function Ho(e){for(;e;){if(e instanceof HTMLElement&&"auto"===e.popover&&"showing"===xe.get(e))return e;if((e=e instanceof Element&&e.assignedSlot||e.parentElement||Te(e))instanceof Nt&&(e=e.host),e instanceof Document)return}}function Hn(e){for(;e;){let t=e.popoverTargetElement;if(t instanceof HTMLElement)return t;if((e=e.parentElement||Te(e))instanceof Nt&&(e=e.host),e instanceof Document)return}}function Po(e,t){let n=new Map,o=0;for(let e of t||[])n.set(e,o),o+=1;n.set(e,o),o+=1;let r=null;return function(t){if(!t)return;let o=!1,i=null,a=null;for(;!o;){if(i=Ho(t)||null,null===i||!n.has(i))return;("hint"===e.popover||"auto"===i.popover)&&(o=!0),o||(t=i.parentElement)}a=n.get(i),(null===r||n.get(r)"u")return;function e(e){return e?.includes(":popover-open")&&(e=e.replace(qn,"$1.\\:popover-open")),e}window.ToggleEvent=window.ToggleEvent||at,de(Document.prototype,"querySelector",e),de(Document.prototype,"querySelectorAll",e),de(Element.prototype,"querySelector",e),de(Element.prototype,"querySelectorAll",e),de(Element.prototype,"matches",e),de(Element.prototype,"closest",e),de(DocumentFragment.prototype,"querySelectorAll",e),Object.defineProperties(HTMLElement.prototype,{popover:{enumerable:!0,configurable:!0,get(){if(!this.hasAttribute("popover"))return null;let e=(this.getAttribute("popover")||"").toLowerCase();return""===e||"auto"==e?"auto":"hint"==e?"hint":"manual"},set(e){null===e?this.removeAttribute("popover"):this.setAttribute("popover",e)}},showPopover:{enumerable:!0,configurable:!0,value(e={}){Rt(this)}},hidePopover:{enumerable:!0,configurable:!0,value(){Se(this,!0,!0)}},togglePopover:{enumerable:!0,configurable:!0,value(e={}){return"boolean"==typeof e&&(e={force:e}),"showing"===xe.get(this)&&void 0===e.force||!1===e.force?Se(this,!0,!0):(void 0===e.force||!0===e.force)&&Rt(this),"showing"===xe.get(this)}}});let t=Element.prototype.attachShadow;t&&Object.defineProperties(Element.prototype,{attachShadow:{enumerable:!0,configurable:!0,writable:!0,value(e){let n=t.call(this,e);return Ht(n),n}}});let n=HTMLElement.prototype.attachInternals;n&&Object.defineProperties(HTMLElement.prototype,{attachInternals:{enumerable:!0,configurable:!0,writable:!0,value(){let e=n.call(this);return e.shadowRoot&&Ht(e.shadowRoot),e}}});let o=new WeakMap;function r(e){Object.defineProperties(e.prototype,{popoverTargetElement:{enumerable:!0,configurable:!0,set(e){if(null===e)this.removeAttribute("popovertarget"),o.delete(this);else{if(!(e instanceof Element))throw new TypeError("popoverTargetElement must be an element or null");this.setAttribute("popovertarget",""),o.set(this,e)}},get(){if("button"!==this.localName&&"input"!==this.localName||"input"===this.localName&&"reset"!==this.type&&"image"!==this.type&&"button"!==this.type||this.disabled||this.form&&"submit"===this.type)return null;let e=o.get(this);if(e&&e.isConnected)return e;if(e&&!e.isConnected)return o.delete(this),null;let t=Te(this),n=this.getAttribute("popovertarget");return(t instanceof Document||t instanceof Do)&&n&&t.getElementById(n)||null}},popoverTargetAction:{enumerable:!0,configurable:!0,get(){let e=(this.getAttribute("popovertargetaction")||"").toLowerCase();return"show"===e||"hide"===e?e:"toggle"},set(e){this.setAttribute("popovertargetaction",e)}}})}r(HTMLButtonElement),r(HTMLInputElement);let i=e=>{if(e.defaultPrevented)return;let t=e.composedPath(),n=t[0];if(!(n instanceof Element)||n?.shadowRoot)return;let o=Te(n);if(!(o instanceof Do||o instanceof Document))return;let r=t.find((e=>{var t;return null==(t=e.matches)?void 0:t.call(e,"[popovertargetaction],[popovertarget]")}));return r?(Dn(r),void e.preventDefault()):void 0},a=e=>{let t=e.key,n=e.target;!e.defaultPrevented&&n&&("Escape"===t||"Esc"===t)&&me(n.ownerDocument,!0,!0)};var l;(l=document).addEventListener("click",i),l.addEventListener("keydown",a),l.addEventListener("pointerdown",Oo),l.addEventListener("pointerup",Oo),Ht(document)}function Fo(){return typeof HTMLButtonElement<"u"&&"command"in HTMLButtonElement.prototype&&"source"in((globalThis.CommandEvent||{}).prototype||{})}function qo(){function e(e,t,n=!0){Object.defineProperty(e,t,{...Object.getOwnPropertyDescriptor(e,t),enumerable:n})}function t(e){return e&&"function"==typeof e.getRootNode?e.getRootNode():e&&e.parentNode?t(e.parentNode):e}document.addEventListener("invoke",(e=>{"invoke"==e.type&&e.isTrusted&&(e.stopImmediatePropagation(),e.preventDefault())}),!0),document.addEventListener("command",(e=>{"command"==e.type&&e.isTrusted&&(e.stopImmediatePropagation(),e.preventDefault())}),!0);let n=new WeakMap,o=new WeakMap;class r extends Event{constructor(e,t={}){super(e,t);let{source:r,command:i}=t;if(null!=r&&!(r instanceof Element))throw new TypeError("source must be an element");n.set(this,r||null),o.set(this,void 0!==i?String(i):"")}get[Symbol.toStringTag](){return"CommandEvent"}get source(){if(!n.has(this))throw new TypeError("illegal invocation");let e=n.get(this);if(!(e instanceof Element))return null;let o=t(e);return o!==t(this.target||document)?o.host:e}get command(){if(!o.has(this))throw new TypeError("illegal invocation");return o.get(this)}get action(){throw new Error("CommandEvent#action was renamed to CommandEvent#command")}get invoker(){throw new Error("CommandEvent#invoker was renamed to CommandEvent#source")}}e(r.prototype,"source"),e(r.prototype,"command");class i extends Event{constructor(e,t={}){throw super(e,t),new Error("InvokeEvent has been deprecated, it has been renamed to `CommandEvent`")}}let a=new WeakMap;let l=new WeakMap;function s(e){for(let t of e)t.oncommand=new Function("event",t.getAttribute("oncommand"))}Object.defineProperties(HTMLElement.prototype,{oncommand:{enumerable:!0,configurable:!0,get(){return u.takeRecords(),l.get(this)||null},set(e){let t=l.get(this)||null;t&&this.removeEventListener("command",t),l.set(this,"object"==typeof e||"function"==typeof e?e:null),"function"==typeof e&&this.addEventListener("command",e)}}});let u=new MutationObserver((e=>{for(let t of e){let{target:e}=t;"childList"===t.type?s(e.querySelectorAll("[oncommand]")):s([e])}}));function c(e){if(e.defaultPrevented||"click"!==e.type)return;let t=e.target.closest("button[invoketarget], button[invokeaction], input[invoketarget], input[invokeaction]");if(t&&(console.warn("Elements with `invoketarget` or `invokeaction` are deprecated and should be renamed to use `commandfor` and `command` respectively"),t.matches("input")))throw new Error("Input elements no longer support `commandfor`");let n=e.target.closest("button[commandfor], button[command]");if(!n)return;if(n.form&&"button"!==n.getAttribute("type"))throw e.preventDefault(),new Error("Element with `commandFor` is a form participant. It should explicitly set `type=button` in order for `commandFor` to work. In order for it to act as a Submit button, it must not have command or commandfor attributes");if(n.hasAttribute("command")!==n.hasAttribute("commandfor")){let e=n.hasAttribute("command")?"command":"commandfor",t=n.hasAttribute("command")?"commandfor":"command";throw new Error(`Element with ${e} attribute must also have a ${t} attribute to function.`)}if("show-popover"!==n.command&&"hide-popover"!==n.command&&"toggle-popover"!==n.command&&"show-modal"!==n.command&&"close"!==n.command&&!n.command.startsWith("--"))return void console.warn(`"${n.command}" is not a valid command value. Custom commands must begin with --`);let o=n.commandForElement;if(!o)return;let i=new r("command",{command:n.command,source:n,cancelable:!0});if(o.dispatchEvent(i),i.defaultPrevented)return;let a=i.command.toLowerCase();if(o.popover){let e=!o.matches(":popover-open");!e||"toggle-popover"!==a&&"show-popover"!==a?!e&&"hide-popover"===a&&o.hidePopover():o.showPopover({source:n})}else if("dialog"===o.localName){let e=!o.hasAttribute("open");e&&"show-modal"===a?o.showModal():!e&&"close"===a&&o.close()}}function d(e){e.addEventListener("click",c,!0)}var h;u.observe(document,{subtree:!0,childList:!0,attributeFilter:["oncommand"]}),s(document.querySelectorAll("[oncommand]")),h=HTMLButtonElement,Object.defineProperties(h.prototype,{commandForElement:{enumerable:!0,configurable:!0,set(e){if(this.hasAttribute("invokeaction"))throw new TypeError("Element has deprecated `invokeaction` attribute, replace with `command`");if(this.hasAttribute("invoketarget"))throw new TypeError("Element has deprecated `invoketarget` attribute, replace with `commandfor`");if(null===e)this.removeAttribute("commandfor"),a.delete(this);else{if(!(e instanceof Element))throw new TypeError("commandForElement must be an element or null");{this.setAttribute("commandfor","");let n=t(e);t(this)===n||n===this.ownerDocument?a.set(this,e):a.delete(this)}}},get(){if("button"!==this.localName)return null;if(this.hasAttribute("invokeaction")||this.hasAttribute("invoketarget"))return console.warn("Element has deprecated `invoketarget` or `invokeaction` attribute, use `commandfor` and `command` instead"),null;if(this.disabled)return null;if(this.form&&"button"!==this.getAttribute("type"))return console.warn("Element with `commandFor` is a form participant. It should explicitly set `type=button` in order for `commandFor` to work"),null;let e=a.get(this);if(e)return e.isConnected?e:(a.delete(this),null);let n=t(this),o=this.getAttribute("commandfor");return(n instanceof Document||n instanceof ShadowRoot)&&o&&n.getElementById(o)||null}},command:{enumerable:!0,configurable:!0,get(){let e=this.getAttribute("command")||"";if(e.startsWith("--"))return e;let t=e.toLowerCase();switch(t){case"show-modal":case"close":case"toggle-popover":case"hide-popover":case"show-popover":return t}return""},set(e){this.setAttribute("command",e)}},invokeAction:{enumerable:!1,configurable:!0,get(){throw new Error("invokeAction is deprecated. It has been renamed to command")},set(e){throw new Error("invokeAction is deprecated. It has been renamed to command")}},invokeTargetElement:{enumerable:!1,configurable:!0,get(){throw new Error("invokeTargetElement is deprecated. It has been renamed to command")},set(e){throw new Error("invokeTargetElement is deprecated. It has been renamed to command")}}}),function(e,t){let n=e.prototype.attachShadow;e.prototype.attachShadow=function(e){let o=n.call(this,e);return t(o),o};let o=e.prototype.attachInternals;e.prototype.attachInternals=function(){let e=o.call(this);return e.shadowRoot&&t(e.shadowRoot),e}}(HTMLElement,(e=>{d(e),u.observe(e,{attributeFilter:["oncommand"]}),s(e.querySelectorAll("[oncommand]"))})),d(document),Object.assign(globalThis,{CommandEvent:r,InvokeEvent:i})}function Vo(){if("function"!=typeof HTMLDialogElement)return!1;let e=!1,t=document.createElement("dialog");return t.addEventListener("beforetoggle",(t=>{e=!0,t.preventDefault()})),t.show(),e}function Wo(){let e=new WeakMap;function t(t){let n=t.open?"closed":"open",o=t.open?"open":"closed";if(e.has(t)){let n=e.get(t);o=n.oldState,clearTimeout(n.id)}e.set(t,{oldState:o,id:setTimeout((()=>{t.dispatchEvent(new ToggleEvent("toggle",{newState:n,oldState:o}))}))})}let n=HTMLDialogElement.prototype.show,o=HTMLDialogElement.prototype.showModal,r=HTMLDialogElement.prototype.close;function i(e){let n=new ToggleEvent("beforetoggle",{newState:"closed",oldState:"open",cancelable:!1});e.dispatchEvent(n),e.open&&t(e)}document.addEventListener("submit",(e=>{let t=e.target;if("dialog"===t.method){let e=t.closest("dialog");e instanceof HTMLDialogElement&&i(e)}}),!0),Object.defineProperties(HTMLDialogElement.prototype,{show:{value(){if(this.open||this.matches(":popover-open, :modal")||!this.ownerDocument)return n.apply(this,arguments);let e=new ToggleEvent("beforetoggle",{newState:"open",oldState:"closed",cancelable:!0});this.dispatchEvent(e)&&(t(this),n.apply(this,arguments))}},showModal:{value(){if(this.open||this.matches(":popover-open, :modal")||!this.isConnected||!this.ownerDocument)return o.apply(this,arguments);let e=new ToggleEvent("beforetoggle",{newState:"open",oldState:"closed",cancelable:!0});return this.dispatchEvent(e)?(t(this),o.apply(this,arguments)):void 0}},close:{value(){return this.open||this.matches(":popover-open, :modal")?(i(this),r.apply(this,arguments)):r.apply(this,arguments)}}})}function ut(e){function t(){"loading"!==document.readyState&&(e(),document.removeEventListener("DOMContentLoaded",t))}typeof window<"u"&&typeof document<"u"&&(document.addEventListener("DOMContentLoaded",t),t())}async function $n(e){await _n();for(let t of document.styleSheets)try{for(let n of t.rules)if("CSSLayerStatementRule"===n.constructor.name&&"nameList"in n&&n.nameList.includes(e))return!0}catch{}return!1}async function _n(){await Promise.all(Array.from(document.querySelectorAll('link[rel="stylesheet"]')).map((e=>e.sheet?Promise.resolve():new Promise((t=>{e.addEventListener("load",(()=>t()),{once:!0}),e.addEventListener("error",(()=>t()),{once:!0})})))))}function Le(e){"focus"in e&&e.focus({focusVisible:ct})}typeof globalThis.window<"u"&&(Bo()||(No(),ut((async()=>{if(await $n("popover-polyfill"))return;let e=document.createElement("style");e.textContent="@layer popover-polyfill;",e.setAttribute("suppressHydrationWarning",""),e.addEventListener("securitypolicyviolation",(()=>{console.log("CSP rules on this website prevented @tailwindplus/elements from defining `popover-polyfill` as the first CSS layer. This is necessary to ensure the popover polyfill behaves correctly with CSS layers. To fix this, please manually add the following CSS to the top of your first stylesheet:\n\n```\n@layer popover-polyfill;\n```")})),document.documentElement.prepend(e)}))),Fo()||qo(),Vo()||Wo());var ct=!1;if(typeof globalThis.window<"u"){let e;(e=>{e[e.Keyboard=0]="Keyboard",e[e.Mouse=1]="Mouse"})(e||(e={})),document.addEventListener("keydown",(e=>{e.metaKey||e.altKey||e.ctrlKey||(ct=!0,document.documentElement.dataset.focusVisible="")}),!0),document.addEventListener("click",(e=>{1===e.detail?(ct=!1,delete document.documentElement.dataset.focusVisible):0===e.detail&&(ct=!0,document.documentElement.dataset.focusVisible="")}),!0)}typeof globalThis.HTMLElement>"u"&&(globalThis.HTMLElement=class{});var Ie,We,se,A=class extends HTMLElement{constructor(){super(...arguments),L(this,Ie,new AbortController),L(this,We,!1),L(this,se,!1)}connectedCallback(){if("observedAttributes"in this.constructor&&"object"==typeof this.constructor.observedAttributes&&Array.isArray(this.constructor.observedAttributes))for(let e of this.constructor.observedAttributes)"string"==typeof e&&(e in this||Object.defineProperty(this,e,{get(){return this.getAttribute(e)},set(t){null!=t&&!1!==t?this.setAttribute(e,t.toString()):this.removeAttribute(e)}}));I(this,We,!0);let e=d(this,Ie).signal;queueMicrotask((()=>{if(!e.aborted)try{this.mount?.(e)}catch(e){console.error(e)}}))}disconnectedCallback(){d(this,Ie).abort(),I(this,Ie,new AbortController)}setAttributeNoCallbacks(e,t){try{I(this,se,!0),this.setAttribute(e,t)}finally{I(this,se,!1)}}removeAttributeNoCallbacks(e){try{I(this,se,!0),this.removeAttribute(e)}finally{I(this,se,!1)}}attributeChangedCallback(e,t,n){d(this,We)&&(d(this,se)||t!==n&&this.onAttributeChange?.(e,t,n))}};function x(e,t){typeof globalThis.customElements>"u"||customElements.get(e)===t||customElements.define(e,t)}function J(){let e=[],t={addEventListener:(e,n,o,r)=>(e.addEventListener(n,o,r),t.add((()=>e.removeEventListener(n,o,r)))),requestAnimationFrame(...e){let n=requestAnimationFrame(...e);return t.add((()=>cancelAnimationFrame(n)))},nextFrame:(...e)=>t.requestAnimationFrame((()=>t.requestAnimationFrame(...e))),setTimeout(...e){let n=setTimeout(...e);return t.add((()=>clearTimeout(n)))},microTask(...e){let n={current:!0};return queueMicrotask((()=>{n.current&&e[0]()})),t.add((()=>{n.current=!1}))},style(e,t,n){let o=e.style.getPropertyValue(t);return t.startsWith("--")?e.style.setProperty(t,n):Object.assign(e.style,{[t]:n}),this.add((()=>{t.startsWith("--")?e.style.setProperty(t,o):Object.assign(e.style,{[t]:o})}))},add:t=>(e.includes(t)||e.push(t),()=>{let n=e.indexOf(t);if(n>=0)for(let t of e.splice(n,1))t()}),dispose(){for(let t of e.splice(0))t()}};return t}function ke(e,t=()=>[]){let n=!1,o=null,r=J();return{start(i,a){let l=[e,...t()];n=!n&&(null!==o&&o!==i),o=i;for(let e of l)Kn(e,(()=>{n||("in"===i?(e.dataset.transition="",e.dataset.enter="",e.dataset.closed="",delete e.dataset.leave):"out"===i&&(e.dataset.transition="",e.dataset.leave="",delete e.dataset.enter))}),null!==o);r.nextFrame((()=>{for(let e of l)n?"in"===i?(delete e.dataset.enter,delete e.dataset.closed,e.dataset.leave=""):"out"===i&&(delete e.dataset.leave,e.dataset.enter="",e.dataset.closed=""):"in"===i?delete e.dataset.closed:"out"===i&&(e.dataset.closed="");r.requestAnimationFrame((()=>{r.add(Ft(e,(()=>{if(!n||!l.some((e=>qt(e)))){for(let e of l)delete e.dataset.transition,delete e.dataset.enter,delete e.dataset.closed,delete e.dataset.leave;o=null,a?.()}})))}))}))},abort(){r.dispose(),n=!1,o=null}}}function Kn(e,t,n=!1){if(n)return void t();let o=e.style.transition;e.style.transition="none",t(),e.offsetHeight,e.style.transition=o}function Ft(e,t){let n=J();if(!e)return n.dispose;let o=!1;n.add((()=>{o=!0}));let r=e.getAnimations?.({subtree:!0}).filter((e=>e instanceof CSSTransition))??[];return 0===r.length?(t(),n.dispose):(Promise.allSettled(r.map((e=>e.finished))).then((()=>{o||t()})),n.dispose)}function qt(e){return(e.getAnimations?.()??[]).some((e=>e instanceof CSSTransition&&"finished"!==e.playState))}Ie=new WeakMap,We=new WeakMap,se=new WeakMap;var Pe=Math.min,Z=Math.max,_e=Math.round,Ke=Math.floor,K=e=>({x:e,y:e}),Un={left:"right",right:"left",bottom:"top",top:"bottom"},jn={start:"end",end:"start"};function Vt(e,t,n){return Z(e,Pe(t,n))}function dt(e,t){return"function"==typeof e?e(t):e}function he(e){return e.split("-")[0]}function pt(e){return e.split("-")[1]}function Wt(e){return"x"===e?"y":"x"}function $t(e){return"y"===e?"height":"width"}var zn=new Set(["top","bottom"]);function le(e){return zn.has(he(e))?"y":"x"}function _t(e){return Wt(le(e))}function Ko(e,t,n){void 0===n&&(n=!1);let o=pt(e),r=_t(e),i=$t(r),a="x"===r?o===(n?"end":"start")?"right":"left":"start"===o?"bottom":"top";return t.reference[i]>t.floating[i]&&(a=$e(a)),[a,$e(a)]}function Uo(e){let t=$e(e);return[ft(e),t,ft(t)]}function ft(e){return e.replace(/start|end/g,(e=>jn[e]))}var $o=["left","right"],_o=["right","left"],Gn=["top","bottom"],Yn=["bottom","top"];function Qn(e,t,n){switch(e){case"top":case"bottom":return n?t?_o:$o:t?$o:_o;case"left":case"right":return t?Gn:Yn;default:return[]}}function jo(e,t,n,o){let r=pt(e),i=Qn(he(e),"start"===n,o);return r&&(i=i.map((e=>e+"-"+r)),t&&(i=i.concat(i.map(ft)))),i}function $e(e){return e.replace(/left|right|bottom|top/g,(e=>Un[e]))}function Xn(e){return{top:0,right:0,bottom:0,left:0,...e}}function zo(e){return"number"!=typeof e?Xn(e):{top:e,right:e,bottom:e,left:e}}function ge(e){let{x:t,y:n,width:o,height:r}=e;return{width:o,height:r,top:n,left:t,right:t+o,bottom:n+r,x:t,y:n}}function Go(e,t,n){let o,{reference:r,floating:i}=e,a=le(t),l=_t(t),s=$t(l),u=he(t),c="y"===a,d=r.x+r.width/2-i.width/2,h=r.y+r.height/2-i.height/2,p=r[s]/2-i[s]/2;switch(u){case"top":o={x:d,y:r.y-i.height};break;case"bottom":o={x:d,y:r.y+r.height};break;case"right":o={x:r.x+r.width,y:h};break;case"left":o={x:r.x-i.width,y:h};break;default:o={x:r.x,y:r.y}}switch(pt(t)){case"start":o[l]-=p*(n&&c?-1:1);break;case"end":o[l]+=p*(n&&c?-1:1)}return o}var Yo=async(e,t,n)=>{let{placement:o="bottom",strategy:r="absolute",middleware:i=[],platform:a}=n,l=i.filter(Boolean),s=await(null==a.isRTL?void 0:a.isRTL(t)),u=await a.getElementRects({reference:e,floating:t,strategy:r}),{x:c,y:d}=Go(u,o,s),h=o,p={},f=0;for(let n=0;ne<=0))){var I,k;let e=((null==(I=i.flip)?void 0:I.index)||0)+1,t=x[e];if(t&&("alignment"!==d||v===le(t)||L.every((e=>le(e.placement)!==v||e.overflows[0]>0))))return{data:{index:e,overflows:L},reset:{placement:t}};let n=null==(k=L.filter((e=>e.overflows[0]<=0)).sort(((e,t)=>e.overflows[1]-t.overflows[1]))[0])?void 0:k.placement;if(!n)switch(p){case"bestFit":{var P;let e=null==(P=L.filter((e=>{if(E){let t=le(e.placement);return t===v||"y"===t}return!0})).map((e=>[e.placement,e.overflows.filter((e=>e>0)).reduce(((e,t)=>e+t),0)])).sort(((e,t)=>e[1]-t[1]))[0])?void 0:P[0];e&&(n=e);break}case"initialPlacement":n=l}if(r!==n)return{reset:{placement:n}}}return{}}}},Xo=function(e){return void 0===e&&(e={}),{name:"shift",options:e,async fn(t){let{x:n,y:o,placement:r}=t,{mainAxis:i=!0,crossAxis:a=!1,limiter:l={fn:e=>{let{x:t,y:n}=e;return{x:t,y:n}}},...s}=dt(e,t),u={x:n,y:o},c=await Kt(t,s),d=le(he(r)),h=Wt(d),p=u[h],f=u[d];if(i){let e="y"===h?"bottom":"right";p=Vt(p+c["y"===h?"top":"left"],p,p-c[e])}if(a){let e="y"===d?"bottom":"right";f=Vt(f+c["y"===d?"top":"left"],f,f-c[e])}let m=l.fn({...t,[h]:p,[d]:f});return{...m,data:{x:m.x-n,y:m.y-o,enabled:{[h]:i,[d]:a}}}}}};function mt(){return typeof window<"u"}function be(e){return Zo(e)?(e.nodeName||"").toLowerCase():"#document"}function R(e){var t;return(null==e||null==(t=e.ownerDocument)?void 0:t.defaultView)||window}function U(e){var t;return null==(t=(Zo(e)?e.ownerDocument:e.document)||window.document)?void 0:t.documentElement}function Zo(e){return!!mt()&&(e instanceof Node||e instanceof R(e).Node)}function V(e){return!!mt()&&(e instanceof Element||e instanceof R(e).Element)}function j(e){return!!mt()&&(e instanceof HTMLElement||e instanceof R(e).HTMLElement)}function Jo(e){return!(!mt()||typeof ShadowRoot>"u")&&(e instanceof ShadowRoot||e instanceof R(e).ShadowRoot)}var Jn=new Set(["inline","contents"]);function Oe(e){let{overflow:t,overflowX:n,overflowY:o,display:r}=W(e);return/auto|scroll|overlay|hidden|clip/.test(t+o+n)&&!Jn.has(r)}var Zn=new Set(["table","td","th"]);function en(e){return Zn.has(be(e))}var er=[":popover-open",":modal"];function Ue(e){return er.some((t=>{try{return e.matches(t)}catch{return!1}}))}var tr=["transform","translate","scale","rotate","perspective"],or=["transform","translate","scale","rotate","perspective","filter"],nr=["paint","layout","strict","content"];function ht(e){let t=gt(),n=V(e)?W(e):e;return tr.some((e=>!!n[e]&&"none"!==n[e]))||!!n.containerType&&"normal"!==n.containerType||!t&&!!n.backdropFilter&&"none"!==n.backdropFilter||!t&&!!n.filter&&"none"!==n.filter||or.some((e=>(n.willChange||"").includes(e)))||nr.some((e=>(n.contain||"").includes(e)))}function tn(e){let t=ee(e);for(;j(t)&&!ve(t);){if(ht(t))return t;if(Ue(t))return null;t=ee(t)}return null}function gt(){return!(typeof CSS>"u"||!CSS.supports)&&CSS.supports("-webkit-backdrop-filter","none")}var rr=new Set(["html","body","#document"]);function ve(e){return rr.has(be(e))}function W(e){return R(e).getComputedStyle(e)}function je(e){return V(e)?{scrollLeft:e.scrollLeft,scrollTop:e.scrollTop}:{scrollLeft:e.scrollX,scrollTop:e.scrollY}}function ee(e){if("html"===be(e))return e;let t=e.assignedSlot||e.parentNode||Jo(e)&&e.host||U(e);return Jo(t)?t.host:t}function on(e){let t=ee(e);return ve(t)?e.ownerDocument?e.ownerDocument.body:e.body:j(t)&&Oe(t)?t:on(t)}function Ce(e,t,n){var o;void 0===t&&(t=[]),void 0===n&&(n=!0);let r=on(e),i=r===(null==(o=e.ownerDocument)?void 0:o.body),a=R(r);if(i){let e=bt(a);return t.concat(a,a.visualViewport||[],Oe(r)?r:[],e&&n?Ce(e):[])}return t.concat(r,Ce(r,[],n))}function bt(e){return e.parent&&Object.getPrototypeOf(e.parent)?e.frameElement:null}function sn(e){let t=W(e),n=parseFloat(t.width)||0,o=parseFloat(t.height)||0,r=j(e),i=r?e.offsetWidth:n,a=r?e.offsetHeight:o,l=_e(n)!==i||_e(o)!==a;return l&&(n=i,o=a),{width:n,height:o,$:l}}function jt(e){return V(e)?e:e.contextElement}function De(e){let t=jt(e);if(!j(t))return K(1);let n=t.getBoundingClientRect(),{width:o,height:r,$:i}=sn(t),a=(i?_e(n.width):n.width)/o,l=(i?_e(n.height):n.height)/r;return(!a||!Number.isFinite(a))&&(a=1),(!l||!Number.isFinite(l))&&(l=1),{x:a,y:l}}var ir=K(0);function ln(e){let t=R(e);return gt()&&t.visualViewport?{x:t.visualViewport.offsetLeft,y:t.visualViewport.offsetTop}:ir}function sr(e,t,n){return void 0===t&&(t=!1),!(!n||t&&n!==R(e))&&t}function we(e,t,n,o){void 0===t&&(t=!1),void 0===n&&(n=!1);let r=e.getBoundingClientRect(),i=jt(e),a=K(1);t&&(o?V(o)&&(a=De(o)):a=De(e));let l=sr(i,n,o)?ln(i):K(0),s=(r.left+l.x)/a.x,u=(r.top+l.y)/a.y,c=r.width/a.x,d=r.height/a.y;if(i){let e=R(i),t=o&&V(o)?R(o):o,n=e,r=bt(n);for(;r&&o&&t!==n;){let e=De(r),t=r.getBoundingClientRect(),o=W(r),i=t.left+(r.clientLeft+parseFloat(o.paddingLeft))*e.x,a=t.top+(r.clientTop+parseFloat(o.paddingTop))*e.y;s*=e.x,u*=e.y,c*=e.x,d*=e.y,s+=i,u+=a,n=R(r),r=bt(n)}}return ge({width:c,height:d,x:s,y:u})}function zt(e,t){let n=je(e).scrollLeft;return t?t.left+n:we(U(e)).left+n}function an(e,t,n){void 0===n&&(n=!1);let o=e.getBoundingClientRect();return{x:o.left+t.scrollLeft-(n?0:zt(e,o)),y:o.top+t.scrollTop}}function lr(e){let{elements:t,rect:n,offsetParent:o,strategy:r}=e,i="fixed"===r,a=U(o),l=!!t&&Ue(t.floating);if(o===a||l&&i)return n;let s={scrollLeft:0,scrollTop:0},u=K(1),c=K(0),d=j(o);if((d||!d&&!i)&&(("body"!==be(o)||Oe(a))&&(s=je(o)),j(o))){let e=we(o);u=De(o),c.x=e.x+o.clientLeft,c.y=e.y+o.clientTop}let h=!a||d||i?K(0):an(a,s,!0);return{width:n.width*u.x,height:n.height*u.y,x:n.x*u.x-s.scrollLeft*u.x+c.x+h.x,y:n.y*u.y-s.scrollTop*u.y+c.y+h.y}}function ar(e){return Array.from(e.getClientRects())}function ur(e){let t=U(e),n=je(e),o=e.ownerDocument.body,r=Z(t.scrollWidth,t.clientWidth,o.scrollWidth,o.clientWidth),i=Z(t.scrollHeight,t.clientHeight,o.scrollHeight,o.clientHeight),a=-n.scrollLeft+zt(e),l=-n.scrollTop;return"rtl"===W(o).direction&&(a+=Z(t.clientWidth,o.clientWidth)-r),{width:r,height:i,x:a,y:l}}function cr(e,t){let n=R(e),o=U(e),r=n.visualViewport,i=o.clientWidth,a=o.clientHeight,l=0,s=0;if(r){i=r.width,a=r.height;let e=gt();(!e||e&&"fixed"===t)&&(l=r.offsetLeft,s=r.offsetTop)}return{width:i,height:a,x:l,y:s}}var fr=new Set(["absolute","fixed"]);function dr(e,t){let n=we(e,!0,"fixed"===t),o=n.top+e.clientTop,r=n.left+e.clientLeft,i=j(e)?De(e):K(1);return{width:e.clientWidth*i.x,height:e.clientHeight*i.y,x:r*i.x,y:o*i.y}}function nn(e,t,n){let o;if("viewport"===t)o=cr(e,n);else if("document"===t)o=ur(U(e));else if(V(t))o=dr(t,n);else{let n=ln(e);o={x:t.x-n.x,y:t.y-n.y,width:t.width,height:t.height}}return ge(o)}function un(e,t){let n=ee(e);return!(n===t||!V(n)||ve(n))&&("fixed"===W(n).position||un(n,t))}function pr(e,t){let n=t.get(e);if(n)return n;let o=Ce(e,[],!1).filter((e=>V(e)&&"body"!==be(e))),r=null,i="fixed"===W(e).position,a=i?ee(e):e;for(;V(a)&&!ve(a);){let t=W(a),n=ht(a);!n&&"fixed"===t.position&&(r=null),(i?!n&&!r:!n&&"static"===t.position&&r&&fr.has(r.position)||Oe(a)&&!n&&un(e,a))?o=o.filter((e=>e!==a)):r=t,a=ee(a)}return t.set(e,o),o}function mr(e){let{element:t,boundary:n,rootBoundary:o,strategy:r}=e,i=[..."clippingAncestors"===n?Ue(t)?[]:pr(t,this._c):[].concat(n),o],a=i[0],l=i.reduce(((e,n)=>{let o=nn(t,n,r);return e.top=Z(o.top,e.top),e.right=Pe(o.right,e.right),e.bottom=Pe(o.bottom,e.bottom),e.left=Z(o.left,e.left),e}),nn(t,a,r));return{width:l.right-l.left,height:l.bottom-l.top,x:l.left,y:l.top}}function hr(e){let{width:t,height:n}=sn(e);return{width:t,height:n}}function gr(e,t,n){let o=j(t),r=U(t),i="fixed"===n,a=we(e,!0,i,t),l={scrollLeft:0,scrollTop:0},s=K(0);function u(){s.x=zt(r)}if(o||!o&&!i)if(("body"!==be(t)||Oe(r))&&(l=je(t)),o){let e=we(t,!0,i,t);s.x=e.x+t.clientLeft,s.y=e.y+t.clientTop}else r&&u();i&&!o&&r&&u();let c=!r||o||i?K(0):an(r,l);return{x:a.left+l.scrollLeft-s.x-c.x,y:a.top+l.scrollTop-s.y-c.y,width:a.width,height:a.height}}function Ut(e){return"static"===W(e).position}function rn(e,t){if(!j(e)||"fixed"===W(e).position)return null;if(t)return t(e);let n=e.offsetParent;return U(e)===n&&(n=n.ownerDocument.body),n}function cn(e,t){let n=R(e);if(Ue(e))return n;if(!j(e)){let t=ee(e);for(;t&&!ve(t);){if(V(t)&&!Ut(t))return t;t=ee(t)}return n}let o=rn(e,t);for(;o&&en(o)&&Ut(o);)o=rn(o,t);return o&&ve(o)&&Ut(o)&&!ht(o)?n:o||tn(e)||n}var br=async function(e){let t=this.getOffsetParent||cn,n=this.getDimensions,o=await n(e.floating);return{reference:gr(e.reference,await t(e.floating),e.strategy),floating:{x:0,y:0,width:o.width,height:o.height}}};function vr(e){return"rtl"===W(e).direction}var Gt={convertOffsetParentRelativeRectToViewportRelativeRect:lr,getDocumentElement:U,getClippingRect:mr,getOffsetParent:cn,getElementRects:br,getClientRects:ar,getDimensions:hr,getScale:De,isElement:V,isRTL:vr};function fn(e,t){return e.x===t.x&&e.y===t.y&&e.width===t.width&&e.height===t.height}function wr(e,t){let n,o=null,r=U(e);function i(){var e;clearTimeout(n),null==(e=o)||e.disconnect(),o=null}return function a(l,s){void 0===l&&(l=!1),void 0===s&&(s=1),i();let u=e.getBoundingClientRect(),{left:c,top:d,width:h,height:p}=u;if(l||t(),!h||!p)return;let f={rootMargin:-Ke(d)+"px "+-Ke(r.clientWidth-(c+h))+"px "+-Ke(r.clientHeight-(d+p))+"px "+-Ke(c)+"px",threshold:Z(0,Pe(1,s))||1},m=!0;function g(t){let o=t[0].intersectionRatio;if(o!==s){if(!m)return a();o?a(!1,o):n=setTimeout((()=>{a(!1,1e-7)}),1e3)}1===o&&!fn(u,e.getBoundingClientRect())&&a(),m=!1}try{o=new IntersectionObserver(g,{...f,root:r.ownerDocument})}catch{o=new IntersectionObserver(g,f)}o.observe(e)}(!0),i}function dn(e,t,n,o){void 0===o&&(o={});let{ancestorScroll:r=!0,ancestorResize:i=!0,elementResize:a="function"==typeof ResizeObserver,layoutShift:l="function"==typeof IntersectionObserver,animationFrame:s=!1}=o,u=jt(e),c=r||i?[...u?Ce(u):[],...Ce(t)]:[];c.forEach((e=>{r&&e.addEventListener("scroll",n,{passive:!0}),i&&e.addEventListener("resize",n)}));let d=u&&l?wr(u,n):null,h=-1,p=null;a&&(p=new ResizeObserver((e=>{let[o]=e;o&&o.target===u&&p&&(p.unobserve(t),cancelAnimationFrame(h),h=requestAnimationFrame((()=>{var e;null==(e=p)||e.observe(t)}))),n()})),u&&!s&&p.observe(u),p.observe(t));let f,m=s?we(e):null;return s&&function t(){let o=we(e);m&&!fn(m,o)&&n(),m=o,f=requestAnimationFrame(t)}(),n(),()=>{var e;c.forEach((e=>{r&&e.removeEventListener("scroll",n),i&&e.removeEventListener("resize",n)})),d?.(),null==(e=p)||e.disconnect(),p=null,s&&cancelAnimationFrame(f)}}var pn=Xo,mn=Qo,hn=(e,t,n)=>{let o=new Map,r={platform:Gt,...n},i={...r.platform,_c:o};return Yo(e,t,{...r,platform:i})};function bn(e){let t=()=>{};return function(n,o){if(t(),!n)return void requestAnimationFrame((()=>{qt(e)?Ft(e,(()=>{e.style.removeProperty("position")})):e.style.removeProperty("position")}));if(!o||!e.hasAttribute("anchor"))return;let r=e.getAttribute("anchor"),i=e.getAttribute("anchor-strategy")||"absolute";"absolute"!==i&&"fixed"!==i&&(console.warn(`[createAnchorUpdater] Invalid anchor strategy "${i}" for element:`,e),i="absolute"),t=dn(o,e,(()=>{let t=gn(window.getComputedStyle(e).getPropertyValue("--anchor-gap"),e),n=gn(window.getComputedStyle(e).getPropertyValue("--anchor-offset"),e),a={};switch(r.split(" ")[0]){case"top":case"bottom":a={top:t,left:-1*n,right:n,bottom:t};break;case"left":case"right":a={top:-1*n,bottom:n,left:t,right:t}}hn(o,e,{strategy:i,placement:r.replace(" ","-"),middleware:[mn({padding:a}),pn({padding:a})]}).then((async({x:t,y:n,placement:o})=>{if(!yr()&&"absolute"===i){let o=null;for(let t=e.parentElement;t;t=t.parentElement){let e=getComputedStyle(t).position;if("relative"===e||"absolute"===e||"fixed"===e||"sticky"===e){o=t;break}}if(o){let e=o.getBoundingClientRect();t-=e.left+window.scrollX,n-=e.top+window.scrollY}}let r=`${t}px`,a=`${n}px`;switch(o.split("-")[0]){case"top":a=`calc(${n}px - var(--anchor-gap, 0px))`,r=`calc(${t}px + var(--anchor-offset, 0px))`;break;case"right":r=`calc(${t}px + var(--anchor-gap, 0px))`,a=`calc(${n}px + var(--anchor-offset, 0px))`;break;case"bottom":a=`calc(${n}px + var(--anchor-gap, 0px))`,r=`calc(${t}px + var(--anchor-offset, 0px))`;break;case"left":r=`calc(${t}px - var(--anchor-gap, 0px))`,a=`calc(${n}px + var(--anchor-offset, 0px))`}{let t=e.getBoundingClientRect();if(0===t.x&&0===t.y&&0===t.width&&0===t.height)return}Object.assign(e.style,{left:r,top:a,position:i}),await(Gt.isRTL?.(e))&&Object.assign(e.style,{right:"unset",bottom:"unset"})}))}))}}function yr(){return"showPopover"in HTMLElement.prototype&&HTMLElement.prototype.showPopover.toString().includes("[native code]")}function gn(e,t){let n=document.createElement("div");t.appendChild(n),n.style.setProperty("margin-top","0px","important"),n.style.setProperty("margin-top",e,"important");let o=parseFloat(window.getComputedStyle(n).marginTop)||0;return t.removeChild(n),o}function ze(e){return Yt(e)&&"tabIndex"in e}function Yt(e){return Er(e)&&"tagName"in e}function Qt(e){return Yt(e)&&"accessKey"in e}function Er(e){return"object"==typeof e&&null!==e&&"nodeType"in e}function vn(e){return Yt(e)&&"style"in e}function wn(e){return Qt(e)&&"INPUT"===e.nodeName}function B(e){let t=e.getBoundingClientRect();return!(0===t.x&&0===t.y&&0===t.width&&0===t.height||"hidden"===(e.ownerDocument.defaultView||window).getComputedStyle(e).visibility)}var yn={get selectRequired(){let e=document.createElement("select");e.setAttribute("required","true");let t=e.validationMessage;return Object.defineProperty(this,"selectRequired",{value:t}),t}};function Ge(e,t,n){function o(){if(!B(e)){for(let t of e.children)if(B(t))return;n()}}if(typeof ResizeObserver<"u"){let n=new ResizeObserver(o);n.observe(e),t.addEventListener("abort",(()=>n.disconnect()))}if(typeof IntersectionObserver<"u"){let n=new IntersectionObserver(o);n.observe(e),t.addEventListener("abort",(()=>n.disconnect()))}}var ae=["[contentEditable=true]","[tabindex]","a[href]","area[href]","button:not([disabled])","iframe","input:not([disabled])","select:not([disabled])","textarea:not([disabled])"].map((e=>`${e}:not([tabindex='-1'])`)).join(",");function Y(e,t,n){let o=t?e.indexOf(t):null;switch(-1===o&&(o=null),n){case 0:for(let t=0;t=0;t--)if(B(e[t]))return e[t];return null;case 2:if(null===o)return Y(e,t,1);for(let t=o-1;t>=0;t--)if(B(e[t]))return e[t];return null;case 3:if(null===o)return Y(e,t,0);for(let t=o+1;t{function e(e){if(e.target===document.body||N[0]===e.target)return;let t=e.target;t&&"closest"in t&&(t=t.closest(ae),N.unshift(t??e.target),(N=N.filter((e=>null!=e&&e.isConnected))).splice(10))}window.addEventListener("click",e,{capture:!0}),window.addEventListener("pointerdown",e,{capture:!0}),window.addEventListener("focus",e,{capture:!0}),document.body.addEventListener("click",e,{capture:!0}),document.body.addEventListener("pointerdown",e,{capture:!0}),document.body.addEventListener("focus",e,{capture:!0})}));var Ye=!1,Xt=!1;typeof navigator<"u"&&(Ye=/^((?!chrome|android).)*safari/i.test(navigator.userAgent),Xt=navigator.userAgent.toLowerCase().includes("firefox"));var Jt=!1;function Me(e,t,n,o,r,i){xr(e.ownerDocument);let a=ke(e),l=bn(e),s=J();e.hasAttribute("popover")||e.setAttribute("popover","");let u=n();for(let t of u)t.setAttribute("type","button"),t.setAttribute("aria-haspopup","true"),t.setAttribute("aria-controls",e.id),t.setAttribute("aria-expanded","false");function c(){e.hasAttribute("open")&&e.hidePopover()}e.hasAttribute("open")&&queueMicrotask((()=>e.showPopover()));let d=new AbortController;t.addEventListener("abort",(()=>d.abort())),e.addEventListener("beforetoggle",(t=>{let n=vt(t,u);l("open"===t.newState,o?.()??n),"open"===t.newState?n&&(Ge(n,d.signal,c),Ge(e,d.signal,c)):(d.abort(),d=new AbortController);let h=e.hasAttribute("open");"open"!==t.newState||h?"closed"===t.newState&&h&&e.removeAttributeNoCallbacks("open"):e.setAttributeNoCallbacks("open",""),"open"===t.newState?(n?.setAttribute("aria-expanded","true"),n?.id&&e.setAttribute("aria-labelledby",n.id),r?.(),Jt=""===e.getAttribute("popover")):(n?.setAttribute("aria-expanded","false"),e.removeAttribute("aria-labelledby"),i?.(),Jt=!1),"closed"===t.oldState&&"open"===t.newState?(Ye&&(s.dispose(),s=J()),a.start("in")):"open"===t.oldState&&"closed"===t.newState&&(Ye&&s.style(e,"transition-property","none"),a.start("out"))}),{signal:t}),t.addEventListener("abort",(()=>a.abort()))}function vt(e,t){return e.source??N.find((e=>t.includes(e)))??t[0]??null}var En=new WeakSet;function xr(e){if(Xt||Ye||En.has(e))return;En.add(e);let t=null;e.addEventListener("mousedown",(()=>{Jt&&(e.body.setAttribute("tabindex","-1"),t&&clearTimeout(t),t=setTimeout((()=>e.body.removeAttribute("tabindex"))))}),{capture:!0})}function z(e,t,n,o){function r(){let n=e.getBoundingClientRect();o.style.setProperty(t,n.width+"px")}let i=e.ownerDocument,a=new ResizeObserver(r);a.observe(e),i.addEventListener("transitionend",r,{signal:n}),n.addEventListener("abort",(()=>a.disconnect()))}var Tr=0;function P(e){return`${e}-${Tr++}`}var Sr=200;function ue(e,t,n,o){Lr(),e.addEventListener(t,(e=>{null!==Zt&&Date.now()-Zt{Zt=Date.now()}),{capture:!0}))}var wt=class extends Map{constructor(e){super(),this.factory=e}get(e){let t=super.get(e);return void 0===t&&(t=this.factory(e,this),this.set(e,t)),t}},Tn=new wt((()=>({referenceCounter:0,d:J()})));function yt(e){let t=Tn.get(e);if(t.referenceCounter++,1===t.referenceCounter){let n=[Cr(),kr(),Ir()];n.forEach((({before:n})=>n({doc:e,d:t.d}))),n.forEach((({after:n})=>n({doc:e,d:t.d})))}let n=!1;return()=>{n||(n=!0,t.referenceCounter--,!(t.referenceCounter>0)&&(t.d.dispose(),Tn.delete(e)))}}function Ir(){return{before({doc:e,d:t}){t.style(e.documentElement,"overflow","hidden")},after(){}}}function kr(){let e;return{before({doc:t}){let n=t.documentElement,o=t.defaultView??window;e=Math.max(0,o.innerWidth-n.clientWidth),n.style.setProperty("--el-top-layer-scrollbar-offset","0px")},after({doc:t,d:n}){let o=t.documentElement,r=Math.max(0,o.clientWidth-o.offsetWidth),i=Math.max(0,e-r);n.style(o,"paddingRight",`${i}px`),n.add((()=>{o.style.setProperty("--el-top-layer-scrollbar-offset",`-${i}px`)}))}}}function Pr(){return/iPhone/gi.test(window.navigator.platform)||/Mac/gi.test(window.navigator.platform)&&window.navigator.maxTouchPoints>0}function Cr(){return Pr()?{before({doc:e,d:t}){function n(e){return!!e.closest("[popover], dialog > *")}t.microTask((()=>{if("auto"!==window.getComputedStyle(e.documentElement).scrollBehavior){let n=J();n.style(e.documentElement,"scrollBehavior","auto"),t.add((()=>t.microTask((()=>n.dispose()))))}let o=window.scrollY??window.pageYOffset,r=null;t.addEventListener(e,"click",(t=>{if(ze(t.target))try{let o=t.target.closest("a");if(!o)return;let{hash:i}=new URL(o.href),a=e.querySelector(i);ze(a)&&!n(a)&&(r=a)}catch{}}),!0),t.addEventListener(e,"touchstart",(e=>{if(ze(e.target)&&vn(e.target))if(n(e.target)){let o=e.target;for(;o.parentElement&&n(o.parentElement);)o=o.parentElement;t.style(o,"overscrollBehavior","contain")}else t.style(e.target,"touchAction","none")})),t.addEventListener(e,"touchmove",(e=>{if(ze(e.target)){if(wn(e.target))return;if(n(e.target)){let t=e.target;for(;t.parentElement&&""!==t.dataset.tailwindplusPortal&&!(t.scrollHeight>t.clientHeight||t.scrollWidth>t.clientWidth);)t=t.parentElement;""===t.dataset.tailwindplusPortal&&e.preventDefault()}else e.preventDefault()}}),{passive:!1}),t.add((()=>{let e=window.scrollY??window.pageYOffset;o!==e&&window.scrollTo(0,o),r&&r.isConnected&&(r.scrollIntoView({block:"nearest"}),r=null)}))}))},after(){}}:{before(){},after(){}}}function Et(e,t){let n=null;e.addEventListener("toggle",(t=>{"open"===t.newState?n||(n=yt(e.ownerDocument)):n&&(n(),n=null)}),{signal:t}),t.addEventListener("abort",(()=>{n&&(n(),n=null)}))}var Sn=/([\u2700-\u27BF]|[\uE000-\uF8FF]|\uD83C[\uDC00-\uDFFF]|\uD83D[\uDC00-\uDFFF]|[\u2011-\u26FF]|\uD83E[\uDD10-\uDDFF])/g;function At(e){let t=e.innerText??"",n=e.cloneNode(!0);if(!Qt(n))return t;let o=!1;for(let e of n.querySelectorAll('[hidden],[aria-hidden],[role="img"]'))e.remove(),o=!0;let r=o?n.innerText??"":t;return Sn.test(r)&&(r=r.replace(Sn,"")),r}function Ln(e){let t=e.getAttribute("aria-label");if("string"==typeof t)return t.trim();let n=e.getAttribute("aria-labelledby");if(n){let e=n.split(" ").map((e=>{let t=document.getElementById(e);if(t){let e=t.getAttribute("aria-label");return"string"==typeof e?e.trim():At(t).trim()}return null})).filter(Boolean);if(e.length>0)return e.join(", ")}return At(e).trim()}var F,Qe,eo=class extends A{constructor(){super(...arguments),L(this,F,[]),L(this,Qe,null)}mount(e){let t=this.getInput(),n=this.getButton(),o=this.getOptions();t.id||(t.id=P("autocomplete-input")),n&&(n.id||(n.id=P("autocomplete-button"))),o.id||(o.id=P("autocomplete-listbox")),Me(o,e,(()=>{let e=this.getButton();return e?[e]:[]}),(()=>this.getInput()),(()=>this.onBeforeOpen()),(()=>this.onBeforeClose())),Et(o,e),t.setAttribute("role","combobox"),t.setAttribute("aria-autocomplete","list"),t.setAttribute("aria-expanded","false"),t.setAttribute("aria-controls",o.id),t.setAttribute("aria-activedescendant",""),t.setAttribute("autocomplete","off"),n&&(n.setAttribute("type","button"),n.setAttribute("tabindex","-1"),n.setAttribute("aria-expanded","false"),n.setAttribute("aria-haspopup","listbox"),n.setAttribute("popovertarget",o.id)),o.setAttribute("role","listbox"),o.setAttribute("popover","manual");let r=this,i=new WeakSet;function a(){for(let t of o.getItems())i.has(t)||(i.add(t),t.id||(t.id=P("option")),t.setAttribute("role","option"),t.setAttribute("aria-selected","false"),t.setAttribute("tabIndex","-1"),t.addEventListener("mousedown",(e=>{0===e.button&&(e.preventDefault(),r.selectOption(t))}),{signal:e}),ue(t,"mouseover",e,(()=>r.setActiveItem(t,!1))),ue(t,"mouseout",e,(()=>r.clearActiveItem())));r.filterOptions()}a();let l=new MutationObserver(a);l.observe(this,{attributes:!1,childList:!0,subtree:!0}),n&&z(n,"--button-width",e,this),z(t,"--input-width",e,this),t.addEventListener("input",(()=>{t.matches(":disabled")||(this.filterOptions(),d(this,F).length>0?o.hasAttribute("open")||o.showPopover():o.hidePopover())}),{signal:e});let s=()=>{t.matches(":disabled")||(t.focus(),o.hasAttribute("open")?o.hidePopover():(this.filterOptions(),d(this,F).length>0&&o.showPopover()))};t.addEventListener("pointerdown",s,{signal:e}),n&&(n.addEventListener("pointerdown",(e=>{e.preventDefault(),s()}),{signal:e}),n.addEventListener("click",(e=>{e.preventDefault(),e.stopImmediatePropagation()}),{signal:e})),t.addEventListener("blur",(({relatedTarget:e})=>{e&&this.contains(e)||o.hidePopover()}),{signal:e}),t.addEventListener("keydown",(e=>{if(!t.matches(":disabled"))switch(e.key){case"ArrowDown":e.preventDefault(),o.hasAttribute("open")||(0===d(this,F).length&&this.filterOptions(),d(this,F).length>0&&o.showPopover()),this.goToItem(3);break;case"ArrowUp":e.preventDefault(),o.hasAttribute("open")||(0===d(this,F).length&&this.filterOptions(),d(this,F).length>0&&o.showPopover()),this.goToItem(2);break;case"Home":case"PageUp":return o.hasAttribute("open")?(e.preventDefault(),e.stopPropagation(),this.goToItem(0)):void 0;case"End":case"PageDown":return o.hasAttribute("open")?(e.preventDefault(),e.stopPropagation(),this.goToItem(1)):void 0;case"Enter":{let t=this.getActiveItem();t&&(e.preventDefault(),this.selectOption(t)),o.hasAttribute("open")&&(e.preventDefault(),o.hidePopover());break}case"Escape":if(!o.hasAttribute("open"))return;e.preventDefault(),o.hidePopover();break;case"Tab":o.hidePopover()}}),{signal:e});let u=Array.from(o.querySelectorAll("el-option[disabled]"));for(let e of u)e.setAttribute("aria-disabled","true"),e.setAttribute("aria-selected","false");e.addEventListener("abort",(()=>{l.disconnect()}))}getInput(){let e=this.querySelector("input");if(!e)throw new Error("`` must contain an input element.");return e}getButton(){return this.querySelector("button")}getOptions(){let e=this.querySelector("el-options");if(!e)throw new Error("`` must contain a `` element.");return e}filterOptions(){let e=this.getInput().value.toLowerCase();d(this,Qe)!==e&&(this.clearActiveItem(),I(this,Qe,e)),I(this,F,[]);for(let t of this.getOptions().getItems()){let n=t.getAttribute("value")?.toLowerCase()||"",o=At(t)?.trim().toLowerCase()??"";""===e||n.includes(e)||o.includes(e)?(d(this,F).push(t),t.removeAttribute("hidden"),t.removeAttribute("aria-hidden")):(t.setAttribute("hidden",""),t.setAttribute("aria-hidden","true"))}}getActiveItem(){let e=this.getInput().getAttribute("aria-activedescendant");return e?document.getElementById(e):null}goToItem(e){if(0===d(this,F).length)return;let t=this.getActiveItem(),n=Y(d(this,F),t,e);n&&this.setActiveItem(n)}setActiveItem(e,t=!0){let n=this.getInput(),o=this.getActiveItem();null!==o&&o.setAttribute("aria-selected","false"),e.setAttribute("aria-selected","true"),n.setAttribute("aria-activedescendant",e.id),t&&e.scrollIntoView({block:"nearest"})}clearActiveItem(){let e=this.getInput(),t=this.getActiveItem();null!==t&&t.setAttribute("aria-selected","false"),e.setAttribute("aria-activedescendant","")}selectOption(e){let t=this.getInput(),n=e.getAttribute("value");n&&(t.value=n,t.dispatchEvent(new Event("input",{bubbles:!0,cancelable:!0})),t.dispatchEvent(new Event("change",{bubbles:!0,cancelable:!0})),this.getOptions().hidePopover())}onBeforeOpen(){let e=this.getInput(),t=this.getButton();e.setAttribute("aria-expanded","true"),t?.setAttribute("aria-expanded","true")}onBeforeClose(){let e=this.getInput(),t=this.getButton();e.setAttribute("aria-expanded","false"),t?.setAttribute("aria-expanded","false"),this.clearActiveItem()}};F=new WeakMap,Qe=new WeakMap,x("el-autocomplete",eo);var G,Xe,Je,te,xt,no,oo=class extends A{constructor(){super(...arguments),L(this,te),L(this,G,[]),L(this,Xe,null),L(this,Je,(({query:e,content:t})=>t.toLocaleLowerCase().includes(e.toLocaleLowerCase().trim())))}mount(e){let t=this.getInput(),n=this.getItems();t.id||(t.id=P("command-input")),n.id||(n.id=P("command-items")),t.setAttribute("role","combobox"),t.setAttribute("aria-autocomplete","list"),t.setAttribute("autocomplete","off"),t.setAttribute("aria-controls",n.id),n.setAttribute("role","listbox");let o=this,r=new WeakSet;function i(t=!1){var i;for(let t of n.getItems())r.has(t)||(r.add(t),t.id||(t.id=P("item")),t.setAttribute("role","option"),t.setAttribute("tabIndex","-1"),t.setAttribute("aria-selected","false"),t.hasAttribute("disabled")&&t.setAttribute("aria-disabled","true"),ue(t,"mouseover",e,(()=>{var e;return H(e=o,te,no).call(e,t,!1)})));H(i=o,te,xt).call(i,t),t||o.goToItem(0)}i(!0);let a=new MutationObserver((()=>i(!1)));a.observe(this,{attributes:!1,childList:!0,subtree:!0}),z(t,"--input-width",e,this),t.addEventListener("input",(()=>H(this,te,xt).call(this)),{signal:e}),t.addEventListener("keydown",(e=>{switch(e.key){case"ArrowDown":e.preventDefault(),this.goToItem(3);break;case"ArrowUp":e.preventDefault(),this.goToItem(2);break;case"Home":case"PageUp":return e.preventDefault(),e.stopPropagation(),this.goToItem(0);case"End":case"PageDown":return e.preventDefault(),e.stopPropagation(),this.goToItem(1);case"Enter":{let t=this.getActiveItem();t&&(e.preventDefault(),t.click());break}}}),{signal:e}),e.addEventListener("abort",(()=>{a.disconnect()}))}getInput(){let e=this.querySelector("input");if(!e)throw new Error("`` must contain an input element.");return e}getItems(){let e=this.querySelector("el-command-list");if(!e)throw new Error("`` must contain a `` element.");return e}getGroups(){return this.getItems().querySelectorAll("el-command-group")}getSuggestions(){return this.querySelector("el-defaults")}getActiveItem(){let e=this.getInput().getAttribute("aria-activedescendant");return e?document.getElementById(e):null}goToItem(e){if(0===d(this,G).length)return;let t=this.getActiveItem(),n=Y(d(this,G),t,e);n&&H(this,te,no).call(this,n)}clearActiveItem(){let e=this.getInput(),t=this.getActiveItem();if(null!==t){t.setAttribute("aria-selected","false");let e=this.querySelector(`el-command-preview[for="${t.id}"]`);e&&e.setAttribute("hidden","")}e.removeAttribute("aria-activedescendant"),this.dispatchEvent(new CustomEvent("change",{detail:{relatedTarget:null},bubbles:!1,cancelable:!1}))}reset(){let e=this.getInput();e.value="",e.dispatchEvent(new Event("input",{bubbles:!0,cancelable:!0})),e.dispatchEvent(new Event("change",{bubbles:!0,cancelable:!0})),H(this,te,xt).call(this,!0),this.clearActiveItem()}setFilterCallback(e){I(this,Je,e)}};G=new WeakMap,Xe=new WeakMap,Je=new WeakMap,te=new WeakSet,xt=function(e=!1){let t=this.getItems(),n=this.getInput().value??"";I(this,G,[]);for(let e of t.getItems()){if(e.closest("el-defaults"))continue;let t=Ln(e)??"";""!==n&&d(this,Je).call(this,{query:n,node:e,content:t})?(d(this,G).push(e),e.removeAttribute("hidden"),e.removeAttribute("aria-hidden")):(e.setAttribute("hidden",""),e.setAttribute("aria-hidden","true"))}for(let e of this.getGroups())e.getItems().some((e=>!e.hasAttribute("hidden")))?e.removeAttribute("hidden"):e.setAttribute("hidden","");let o=this.getSuggestions();o&&(""===n?(o.removeAttribute("hidden"),I(this,G,o.getItems())):o.setAttribute("hidden",""));let r=this.querySelector("el-no-results");r&&(""===n||d(this,G).length>0?r.setAttribute("hidden",""):r.removeAttribute("hidden")),0===d(this,G).length?t.setAttribute("hidden",""):t.removeAttribute("hidden"),(!e||""!==n)&&(0===d(this,G).length?this.clearActiveItem():d(this,Xe)!==n&&this.goToItem(0),I(this,Xe,n))},no=function(e,t=!0){let n=this.getInput(),o=this.getActiveItem();if(e===o)return;if(null!==o){o.setAttribute("aria-selected","false");let e=this.querySelector(`el-command-preview[for="${o.id}"]`);e&&e.setAttribute("hidden","")}e.setAttribute("aria-selected","true"),n.setAttribute("aria-activedescendant",e.id);let r=this.querySelector(`el-command-preview[for="${e.id}"]`);r&&r.removeAttribute("hidden"),t&&e.scrollIntoView({block:"nearest"}),this.dispatchEvent(new CustomEvent("change",{detail:{relatedTarget:e},bubbles:!1,cancelable:!1}))};var ro=class extends A{getItems(){return Array.from(this.querySelectorAll(`${ae},[role="option"]`))}},io=class extends A{getItems(){return Array.from(this.querySelectorAll(`${ae},[role="option"]`))}},so=class extends A{},lo=class extends A{},ao=class extends A{getItems(){return Array.from(this.querySelectorAll(`${ae},[role="option"]`))}};x("el-command-palette",oo),x("el-command-list",ro),x("el-defaults",io),x("el-no-results",so),x("el-command-group",ao),x("el-command-preview",lo);var He=null;typeof globalThis.window<"u"&&(He=HTMLDialogElement.prototype.close,Object.defineProperties(HTMLDialogElement.prototype,{close:{value(e){let t=this.closest("el-dialog");if(!(t instanceof Re))return He?.call(this,e);let n=t.beforeClose(e);if(!0===n)return He?.call(this,e);!1!==n&&n.then((t=>t?He?.call(this,e):null)).catch(console.error)}}}),document.addEventListener("command",(e=>{let t=e.target;if(!(t instanceof HTMLDialogElement&&"command"in e&&"close"===e.command))return;let n=t.closest("el-dialog");if(!(n instanceof Re))return;let o="BUTTON"===e.source?.tagName?e.source.value:void 0,r=n.beforeClose(o);!0!==r&&(e.stopImmediatePropagation(),e.preventDefault(),!1!==r&&r.then((e=>e?He?.call(t,o):null)).catch(console.error))}),!0));var Q,oe,Be,ye,Ze,uo,Re=class extends A{constructor(){super(...arguments),L(this,Ze),L(this,Q,null),L(this,oe,null),L(this,Be,!0),L(this,ye,ke(this,(()=>Array.from(this.querySelectorAll("el-dialog-panel,el-dialog-backdrop")))))}mount(e){let t=this.getNativeDialog();t.removeAttribute("open"),t.style.setProperty("right","var(--el-top-layer-scrollbar-offset, 0px)");let n=this.hasAttribute("open");for(let e of H(this,Ze,uo).call(this))e.setAttribute("aria-expanded",n.toString());Dr(t,e,(e=>{e.preventDefault();let n=new Event("cancel",{bubbles:!1,cancelable:!0});!this.dispatchEvent(n)||t.close("")})),Ge(this.querySelector("el-dialog-panel")??t,e,(()=>{this.hasAttribute("open")&&t.close("")}));let o=null;t.addEventListener("beforetoggle",(e=>{let t=e;"open"===t.newState&&"closed"===t.oldState&&this.beforeOpen();let n=this.hasAttribute("open");if("open"!==t.newState||n?"closed"===t.newState&&n&&(this.dispatchEvent(new CustomEvent("close",{bubbles:!1,cancelable:!1})),this.removeAttribute("open")):(this.dispatchEvent(new CustomEvent("open",{bubbles:!1,cancelable:!1})),this.setAttribute("open","")),"open"===t.newState&&"closed"===t.oldState)N.length>0&&!o&&(o=N[0]);else if("closed"===t.newState&&"open"===t.oldState){let e=d(this,Be);setTimeout((()=>{e?(o&&o!==document.activeElement&&o.isConnected&&Le(o),o=null):o&&o===document.activeElement&&o.isConnected&&"blur"in o&&"function"==typeof o.blur&&o.blur()}))}}),{signal:e}),t.addEventListener("focusout",(e=>{null===e.relatedTarget&&queueMicrotask((()=>{t.contains(t.ownerDocument.activeElement)||t.focus()}))}),{signal:e}),e.addEventListener("abort",(()=>{var e;d(this,ye).abort(),null==(e=d(this,oe))||e.call(this)})),this.hasAttribute("open")&&t.showModal()}onAttributeChange(e,t,n){switch(e){case"open":{let e=this.getNativeDialog();for(let e of H(this,Ze,uo).call(this))e.setAttribute("aria-expanded",null!==n?"true":"false");null===n?e.close():e.showModal();break}}}getNativeDialog(){let e=this.querySelector("dialog");if(!e)throw new Error("[ElDialog] No `