diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..5b41b8b
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,39 @@
+name: CI
+
+on:
+ pull_request:
+ branches: [master]
+
+permissions:
+ contents: read
+
+jobs:
+ ci:
+ name: Lint, Test & Build
+ runs-on: ubuntu-latest
+
+ strategy:
+ matrix:
+ node-version: [18, 20, 22]
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Setup Node.js ${{ matrix.node-version }}
+ uses: actions/setup-node@v4
+ with:
+ node-version: ${{ matrix.node-version }}
+ cache: npm
+
+ - name: Install dependencies
+ run: npm ci
+
+ - name: Lint
+ run: npm run lint
+
+ - name: Test
+ run: npm test
+
+ - name: Build
+ run: npm run build
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
new file mode 100644
index 0000000..dcde3b8
--- /dev/null
+++ b/.github/workflows/release.yml
@@ -0,0 +1,118 @@
+name: Release
+
+on:
+ push:
+ branches: [master]
+
+concurrency:
+ group: release
+ cancel-in-progress: false
+
+permissions:
+ contents: write
+
+jobs:
+ release:
+ name: Version Bump & Publish
+ runs-on: ubuntu-latest
+
+ # Skip version bump commits from the bot to avoid infinite loops
+ if: github.event.head_commit.author.username != 'github-actions[bot]'
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+ with:
+ token: ${{ secrets.GITHUB_TOKEN }}
+ fetch-depth: 0
+
+ - name: Get PR number and determine version bump
+ id: get-pr
+ uses: actions/github-script@v7
+ with:
+ script: |
+ const commits = await github.rest.repos.listPullRequestsAssociatedWithCommit({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ commit_sha: context.sha,
+ });
+ const merged = commits.data.find(pr => pr.merged_at);
+ if (!merged) {
+ core.info('No merged PR found for this commit. Skipping release.');
+ core.setOutput('skip', 'true');
+ return;
+ }
+ core.setOutput('pr_number', merged.number);
+ core.setOutput('skip', 'false');
+
+ const labels = merged.labels.map(l => l.name);
+ core.setOutput('labels', labels.join(','));
+
+ if (labels.includes('skip-release')) {
+ core.info('skip-release label found. Skipping.');
+ core.setOutput('skip', 'true');
+ return;
+ }
+
+ let bump = 'patch';
+ if (labels.includes('major')) bump = 'major';
+ else if (labels.includes('minor')) bump = 'minor';
+ core.setOutput('bump', bump);
+ core.info(`Detected bump: ${bump} from labels: ${labels.join(', ')}`);
+
+ - name: Setup Node.js
+ if: steps.get-pr.outputs.skip != 'true'
+ uses: actions/setup-node@v4
+ with:
+ node-version: 20
+ cache: npm
+ registry-url: https://registry.npmjs.org
+
+ - name: Install dependencies
+ if: steps.get-pr.outputs.skip != 'true'
+ run: npm ci
+
+ - name: Test
+ if: steps.get-pr.outputs.skip != 'true'
+ run: npm test
+
+ - name: Build
+ if: steps.get-pr.outputs.skip != 'true'
+ run: npm run build
+
+ - name: Configure git
+ if: steps.get-pr.outputs.skip != 'true'
+ run: |
+ git config user.name "github-actions[bot]"
+ git config user.email "github-actions[bot]@users.noreply.github.com"
+
+ - name: Bump version
+ if: steps.get-pr.outputs.skip != 'true'
+ id: version
+ run: |
+ npm version ${{ steps.get-pr.outputs.bump }} -m "chore: release v%s"
+ echo "new_version=$(node -p 'require("./package.json").version')" >> "$GITHUB_OUTPUT"
+
+ - name: Publish to npm
+ if: steps.get-pr.outputs.skip != 'true'
+ run: npm publish
+ env:
+ NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
+
+ - name: Push version commit and tag
+ if: steps.get-pr.outputs.skip != 'true'
+ run: git push origin master --follow-tags
+
+ - name: Create GitHub Release
+ if: steps.get-pr.outputs.skip != 'true'
+ uses: actions/github-script@v7
+ with:
+ script: |
+ const version = '${{ steps.version.outputs.new_version }}';
+ await github.rest.repos.createRelease({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ tag_name: `v${version}`,
+ name: `v${version}`,
+ generate_release_notes: true,
+ });
diff --git a/biome.json b/biome.json
new file mode 100644
index 0000000..8d782d4
--- /dev/null
+++ b/biome.json
@@ -0,0 +1,36 @@
+{
+ "$schema": "https://biomejs.dev/schemas/2.4.10/schema.json",
+ "vcs": {
+ "enabled": true,
+ "clientKind": "git",
+ "useIgnoreFile": true
+ },
+ "files": {
+ "ignoreUnknown": false
+ },
+ "formatter": {
+ "enabled": true,
+ "indentStyle": "space",
+ "indentWidth": 2,
+ "lineWidth": 100
+ },
+ "linter": {
+ "enabled": true,
+ "rules": {
+ "recommended": true
+ }
+ },
+ "javascript": {
+ "formatter": {
+ "quoteStyle": "single"
+ }
+ },
+ "assist": {
+ "enabled": true,
+ "actions": {
+ "source": {
+ "organizeImports": "on"
+ }
+ }
+ }
+}
diff --git a/index.html b/index.html
index 0ce219e..60b58de 100644
--- a/index.html
+++ b/index.html
@@ -16,7 +16,7 @@