diff --git a/.eslintrc b/.eslintrc deleted file mode 100644 index 3e995d5..0000000 --- a/.eslintrc +++ /dev/null @@ -1,25 +0,0 @@ -{ - "env": { - "es6": true, - "node": true - }, - "parserOptions": { - "ecmaVersion": 2020, - "sourceType": "module", - "project": "./tsconfig.json" - }, - "extends": ["eslint:recommended"], - "rules": { - "indent": ["error", "tab"], - "camelcase": ["error", {"properties": "never"}], - "linebreak-style": ["error", "unix"], - "no-constant-condition": "error", - "quotes": ["error", "double"], - "semi": "error", - "no-extra-semi": "error", - "prefer-const": "error", - "eol-last": "error", - "no-unused-vars": 0, - "no-console": 0 - } -} \ No newline at end of file diff --git a/.gitignore b/.gitignore index 867a84a..4c2f23c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,2 @@ -**/node_modules -**/pnpm-debug.log \ No newline at end of file +browser-test/test.browser.js +node_modules diff --git a/.npmrc b/.npmrc deleted file mode 100644 index c02dd08..0000000 --- a/.npmrc +++ /dev/null @@ -1 +0,0 @@ -link-workspace-packages = true \ No newline at end of file diff --git a/.pnpm-workspace.yaml b/.pnpm-workspace.yaml deleted file mode 100644 index 0f2abf0..0000000 --- a/.pnpm-workspace.yaml +++ /dev/null @@ -1,6 +0,0 @@ -{ - packages: [ - ".", - "packages/**" - ] -} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..601953d --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,4 @@ +{ + "typescript.tsdk": "node_modules/typescript/lib", + "deno.enable": false +} diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..249ab64 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) Feathers Studio + +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/README.md b/README.md index 3c4a875..786fc65 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,90 @@ -# hyperactive +
+ Hyperactive +
-Hyperactive is a suite of tools for webapps. It's still in development, so check back later. +
+

hyperactive

+
+ +Hyperactive is a powerful set of tools to build reactive web applications. + +We're currently working on a 2.0 release, which will include fully reactive client-side rendering. To try the latest version, you can get `hyper`: + +```bash +npm install https://gethyper.dev + +yarn add https://gethyper.dev + +pnpm add https://gethyper.dev + +bun install https://gethyper.dev +``` + +Hyperactive is also available on [NPM](https://www.npmjs.com/package/@hyperactive/hyper). + +This is not a release version, so expect some bugs. + +[![Hyperactive Version 2.0.0-beta.8](https://img.shields.io/static/v1?label=Version&message=2.0.0-beta.8&style=for-the-badge&labelColor=FF6A00&color=fff)](https://npmjs.com/package/@hyperactive/hyper) + +
+

Usage

+
+ +### On the server + +```TypeScript +import { renderHTML } from "@hyperactive/hyper"; +import { div, p, h1, br } from "@hyperactive/hyper/elements"; + +assertEquals( + renderHTML( + section( + { class: "container" }, + div( + img({ src: "/hero.jpg" }), + h1("Hello World"), + ), + ), + ), + `

Hello World

`, +); +``` + +### In the browser + +[![@types/web 0.0.234](https://img.shields.io/static/v1?label=@types/web&message=0.0.234&style=for-the-badge&labelColor=ff0000&color=fff)](https://npmjs.com/package/@types/web) + +Please install `@types/web` to use Hyperactive in the browser. Your package manager will automatically install the correct version of `@types/web` for you by default. See the [versions](./docs/versions.md) table for the correct version of `@types/web` for each version of Hyperactive. + +```bash +bun install @types/web +``` + +```TypeScript +import { State, renderDOM } from "@hyperactive/hyper"; +import { div, p, button } from "@hyperactive/hyper/elements"; + +const s = new State(0); + +const root = document.getElementById("root"); + +renderDOM( + root, + div( + p("You clicked ", s, " times"), + button( + { on: { click: () => s.update(s.value + 1) } }, + "Increment" + ), + ), +); + +``` + +
+

Testimonials

+
+ +
+ Thomas's testimonial +
diff --git a/bun.lock b/bun.lock new file mode 100755 index 0000000..6cd676b --- /dev/null +++ b/bun.lock @@ -0,0 +1,342 @@ +{ + "lockfileVersion": 0, + "workspaces": { + "": { + "dependencies": { + "typescript": "^5.7.2", + }, + }, + "packages/blink": { + "name": "@feathers-studio/blink", + "dependencies": { + "@hyperactive/hyper": "packages/hyper", + "cookie": "^1.0.2", + "jsdom": "^25.0.1", + "mime-types": "^2.1.35", + "nanoid": "^5.0.9", + }, + "devDependencies": { + "@types/bun": "latest", + "@types/mime-types": "^2.1.4", + }, + "peerDependencies": { + "typescript": "^5.0.0", + }, + }, + "packages/hyper": { + "name": "@hyperactive/hyper", + "version": "2.0.0-beta.1", + "devDependencies": { + "@types/bun": "latest", + "@types/semver": "^7.5.8", + "semver": "^7.6.3", + }, + "peerDependencies": { + "@types/web": "0.0.188", + }, + "optionalPeers": [ + "@types/web", + ], + }, + "packages/mark": { + "name": "@hyperactive/mark", + "devDependencies": { + "@types/bun": "latest", + }, + "peerDependencies": { + "typescript": "^5.0.0", + }, + }, + "packages/scripts": { + "name": "@hyperactive/scripts", + "dependencies": { + "jsdom": "^25.0.1", + }, + "devDependencies": { + "@types/jsdom": "^21.1.7", + }, + }, + "packages/serve": { + "name": "@hyperactive/serve", + "devDependencies": { + "@types/bun": "latest", + }, + "peerDependencies": { + "typescript": "^5.0.0", + }, + }, + "packages/todo": { + "name": "@feathers-studio/todo", + "dependencies": { + "@hyperactive/hyper": "packages/hyper", + "vite": "^6.0.5", + }, + "devDependencies": { + "@types/bun": "latest", + "@types/react": "^19.0.2", + "@types/react-dom": "^19.0.2", + "@types/web": "^0.0.188", + }, + "peerDependencies": { + "typescript": "^5.0.0", + }, + }, + "packages/url": { + "name": "@hyperactive/url", + "devDependencies": { + "@types/bun": "^1.1.14", + }, + "peerDependencies": { + "typescript": "^5.0.0", + }, + }, + "packages/web": { + "name": "@hyperactive/web", + "dependencies": { + "@hyperactive/hyper": "packages/hyper", + "@hyperactive/serve": "packages/serve", + "marked": "^15.0.5", + "mime-types": "^2.1.35", + }, + "devDependencies": { + "@types/bun": "latest", + }, + "peerDependencies": { + "typescript": "^5.7.2", + }, + }, + }, + "packages": { + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.24.0", "", { "os": "aix", "cpu": "ppc64" }, "sha512-WtKdFM7ls47zkKHFVzMz8opM7LkcsIp9amDUBIAWirg70RM71WRSjdILPsY5Uv1D42ZpUfaPILDlfactHgsRkw=="], + + "@esbuild/android-arm": ["@esbuild/android-arm@0.24.0", "", { "os": "android", "cpu": "arm" }, "sha512-arAtTPo76fJ/ICkXWetLCc9EwEHKaeya4vMrReVlEIUCAUncH7M4bhMQ+M9Vf+FFOZJdTNMXNBrWwW+OXWpSew=="], + + "@esbuild/android-arm64": ["@esbuild/android-arm64@0.24.0", "", { "os": "android", "cpu": "arm64" }, "sha512-Vsm497xFM7tTIPYK9bNTYJyF/lsP590Qc1WxJdlB6ljCbdZKU9SY8i7+Iin4kyhV/KV5J2rOKsBQbB77Ab7L/w=="], + + "@esbuild/android-x64": ["@esbuild/android-x64@0.24.0", "", { "os": "android", "cpu": "x64" }, "sha512-t8GrvnFkiIY7pa7mMgJd7p8p8qqYIz1NYiAoKc75Zyv73L3DZW++oYMSHPRarcotTKuSs6m3hTOa5CKHaS02TQ=="], + + "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.24.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-CKyDpRbK1hXwv79soeTJNHb5EiG6ct3efd/FTPdzOWdbZZfGhpbcqIpiD0+vwmpu0wTIL97ZRPZu8vUt46nBSw=="], + + "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.24.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-rgtz6flkVkh58od4PwTRqxbKH9cOjaXCMZgWD905JOzjFKW+7EiUObfd/Kav+A6Gyud6WZk9w+xu6QLytdi2OA=="], + + "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.24.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-6Mtdq5nHggwfDNLAHkPlyLBpE5L6hwsuXZX8XNmHno9JuL2+bg2BX5tRkwjyfn6sKbxZTq68suOjgWqCicvPXA=="], + + "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.24.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-D3H+xh3/zphoX8ck4S2RxKR6gHlHDXXzOf6f/9dbFt/NRBDIE33+cVa49Kil4WUjxMGW0ZIYBYtaGCa2+OsQwQ=="], + + "@esbuild/linux-arm": ["@esbuild/linux-arm@0.24.0", "", { "os": "linux", "cpu": "arm" }, "sha512-gJKIi2IjRo5G6Glxb8d3DzYXlxdEj2NlkixPsqePSZMhLudqPhtZ4BUrpIuTjJYXxvF9njql+vRjB2oaC9XpBw=="], + + "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.24.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-TDijPXTOeE3eaMkRYpcy3LarIg13dS9wWHRdwYRnzlwlA370rNdZqbcp0WTyyV/k2zSxfko52+C7jU5F9Tfj1g=="], + + "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.24.0", "", { "os": "linux", "cpu": "ia32" }, "sha512-K40ip1LAcA0byL05TbCQ4yJ4swvnbzHscRmUilrmP9Am7//0UjPreh4lpYzvThT2Quw66MhjG//20mrufm40mA=="], + + "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.24.0", "", { "os": "linux", "cpu": "none" }, "sha512-0mswrYP/9ai+CU0BzBfPMZ8RVm3RGAN/lmOMgW4aFUSOQBjA31UP8Mr6DDhWSuMwj7jaWOT0p0WoZ6jeHhrD7g=="], + + "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.24.0", "", { "os": "linux", "cpu": "none" }, "sha512-hIKvXm0/3w/5+RDtCJeXqMZGkI2s4oMUGj3/jM0QzhgIASWrGO5/RlzAzm5nNh/awHE0A19h/CvHQe6FaBNrRA=="], + + "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.24.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-HcZh5BNq0aC52UoocJxaKORfFODWXZxtBaaZNuN3PUX3MoDsChsZqopzi5UupRhPHSEHotoiptqikjN/B77mYQ=="], + + "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.24.0", "", { "os": "linux", "cpu": "none" }, "sha512-bEh7dMn/h3QxeR2KTy1DUszQjUrIHPZKyO6aN1X4BCnhfYhuQqedHaa5MxSQA/06j3GpiIlFGSsy1c7Gf9padw=="], + + "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.24.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-ZcQ6+qRkw1UcZGPyrCiHHkmBaj9SiCD8Oqd556HldP+QlpUIe2Wgn3ehQGVoPOvZvtHm8HPx+bH20c9pvbkX3g=="], + + "@esbuild/linux-x64": ["@esbuild/linux-x64@0.24.0", "", { "os": "linux", "cpu": "x64" }, "sha512-vbutsFqQ+foy3wSSbmjBXXIJ6PL3scghJoM8zCL142cGaZKAdCZHyf+Bpu/MmX9zT9Q0zFBVKb36Ma5Fzfa8xA=="], + + "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.24.0", "", { "os": "none", "cpu": "x64" }, "sha512-hjQ0R/ulkO8fCYFsG0FZoH+pWgTTDreqpqY7UnQntnaKv95uP5iW3+dChxnx7C3trQQU40S+OgWhUVwCjVFLvg=="], + + "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.24.0", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-MD9uzzkPQbYehwcN583yx3Tu5M8EIoTD+tUgKF982WYL9Pf5rKy9ltgD0eUgs8pvKnmizxjXZyLt0z6DC3rRXg=="], + + "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.24.0", "", { "os": "openbsd", "cpu": "x64" }, "sha512-4ir0aY1NGUhIC1hdoCzr1+5b43mw99uNwVzhIq1OY3QcEwPDO3B7WNXBzaKY5Nsf1+N11i1eOfFcq+D/gOS15Q=="], + + "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.24.0", "", { "os": "sunos", "cpu": "x64" }, "sha512-jVzdzsbM5xrotH+W5f1s+JtUy1UWgjU0Cf4wMvffTB8m6wP5/kx0KiaLHlbJO+dMgtxKV8RQ/JvtlFcdZ1zCPA=="], + + "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.24.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-iKc8GAslzRpBytO2/aN3d2yb2z8XTVfNV0PjGlCxKo5SgWmNXx82I/Q3aG1tFfS+A2igVCY97TJ8tnYwpUWLCA=="], + + "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.24.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-vQW36KZolfIudCcTnaTpmLQ24Ha1RjygBo39/aLkM2kmjkWmZGEJ5Gn9l5/7tzXA42QGIoWbICfg6KLLkIw6yw=="], + + "@esbuild/win32-x64": ["@esbuild/win32-x64@0.24.0", "", { "os": "win32", "cpu": "x64" }, "sha512-7IAFPrjSQIJrGsK6flwg7NFmwBoSTyF3rl7If0hNUFQU4ilTsEPL6GuMuU9BfIWVVGuRnuIidkSMC+c0Otu8IA=="], + + "@feathers-studio/blink": ["@feathers-studio/blink@workspace:packages/blink", { "dependencies": { "@hyperactive/hyper": "packages/hyper", "cookie": "^1.0.2", "jsdom": "^25.0.1", "mime-types": "^2.1.35", "nanoid": "^5.0.9" }, "devDependencies": { "@types/bun": "latest", "@types/mime-types": "^2.1.4" }, "peerDependencies": { "typescript": "^5.0.0" } }], + + "@feathers-studio/todo": ["@feathers-studio/todo@workspace:packages/todo", { "dependencies": { "@hyperactive/hyper": "packages/hyper", "vite": "^6.0.5" }, "devDependencies": { "@types/bun": "latest", "@types/react": "^19.0.2", "@types/react-dom": "^19.0.2", "@types/web": "^0.0.188" }, "peerDependencies": { "typescript": "^5.0.0" } }], + + "@hyperactive/hyper": ["@hyperactive/hyper@workspace:packages/hyper", { "devDependencies": { "@types/bun": "latest", "@types/semver": "^7.5.8", "semver": "^7.6.3" }, "peerDependencies": { "@types/web": "0.0.188" }, "optionalPeers": ["@types/web"] }], + + "@hyperactive/mark": ["@hyperactive/mark@workspace:packages/mark", { "devDependencies": { "@types/bun": "latest" }, "peerDependencies": { "typescript": "^5.0.0" } }], + + "@hyperactive/scripts": ["@hyperactive/scripts@workspace:packages/scripts", { "dependencies": { "jsdom": "^25.0.1" }, "devDependencies": { "@types/jsdom": "^21.1.7" } }], + + "@hyperactive/serve": ["@hyperactive/serve@workspace:packages/serve", { "devDependencies": { "@types/bun": "latest" }, "peerDependencies": { "typescript": "^5.0.0" } }], + + "@hyperactive/url": ["@hyperactive/url@workspace:packages/url", { "devDependencies": { "@types/bun": "^1.1.14" }, "peerDependencies": { "typescript": "^5.0.0" } }], + + "@hyperactive/web": ["@hyperactive/web@workspace:packages/web", { "dependencies": { "@hyperactive/hyper": "packages/hyper", "@hyperactive/serve": "packages/serve", "marked": "^15.0.5", "mime-types": "^2.1.35" }, "devDependencies": { "@types/bun": "latest" }, "peerDependencies": { "typescript": "^5.7.2" } }], + + "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.29.1", "", { "os": "android", "cpu": "arm" }, "sha512-ssKhA8RNltTZLpG6/QNkCSge+7mBQGUqJRisZ2MDQcEGaK93QESEgWK2iOpIDZ7k9zPVkG5AS3ksvD5ZWxmItw=="], + + "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.29.1", "", { "os": "android", "cpu": "arm64" }, "sha512-CaRfrV0cd+NIIcVVN/jx+hVLN+VRqnuzLRmfmlzpOzB87ajixsN/+9L5xNmkaUUvEbI5BmIKS+XTwXsHEb65Ew=="], + + "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.29.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-2ORr7T31Y0Mnk6qNuwtyNmy14MunTAMx06VAPI6/Ju52W10zk1i7i5U3vlDRWjhOI5quBcrvhkCHyF76bI7kEw=="], + + "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.29.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-j/Ej1oanzPjmN0tirRd5K2/nncAhS9W6ICzgxV+9Y5ZsP0hiGhHJXZ2JQ53iSSjj8m6cRY6oB1GMzNn2EUt6Ng=="], + + "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.29.1", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-91C//G6Dm/cv724tpt7nTyP+JdN12iqeXGFM1SqnljCmi5yTXriH7B1r8AD9dAZByHpKAumqP1Qy2vVNIdLZqw=="], + + "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.29.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-hEioiEQ9Dec2nIRoeHUP6hr1PSkXzQaCUyqBDQ9I9ik4gCXQZjJMIVzoNLBRGet+hIUb3CISMh9KXuCcWVW/8w=="], + + "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.29.1", "", { "os": "linux", "cpu": "arm" }, "sha512-Py5vFd5HWYN9zxBv3WMrLAXY3yYJ6Q/aVERoeUFwiDGiMOWsMs7FokXihSOaT/PMWUty/Pj60XDQndK3eAfE6A=="], + + "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.29.1", "", { "os": "linux", "cpu": "arm" }, "sha512-RiWpGgbayf7LUcuSNIbahr0ys2YnEERD4gYdISA06wa0i8RALrnzflh9Wxii7zQJEB2/Eh74dX4y/sHKLWp5uQ=="], + + "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.29.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-Z80O+taYxTQITWMjm/YqNoe9d10OX6kDh8X5/rFCMuPqsKsSyDilvfg+vd3iXIqtfmp+cnfL1UrYirkaF8SBZA=="], + + "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.29.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-fOHRtF9gahwJk3QVp01a/GqS4hBEZCV1oKglVVq13kcK3NeVlS4BwIFzOHDbmKzt3i0OuHG4zfRP0YoG5OF/rA=="], + + "@rollup/rollup-linux-loongarch64-gnu": ["@rollup/rollup-linux-loongarch64-gnu@4.29.1", "", { "os": "linux", "cpu": "none" }, "sha512-5a7q3tnlbcg0OodyxcAdrrCxFi0DgXJSoOuidFUzHZ2GixZXQs6Tc3CHmlvqKAmOs5eRde+JJxeIf9DonkmYkw=="], + + "@rollup/rollup-linux-powerpc64le-gnu": ["@rollup/rollup-linux-powerpc64le-gnu@4.29.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-9b4Mg5Yfz6mRnlSPIdROcfw1BU22FQxmfjlp/CShWwO3LilKQuMISMTtAu/bxmmrE6A902W2cZJuzx8+gJ8e9w=="], + + "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.29.1", "", { "os": "linux", "cpu": "none" }, "sha512-G5pn0NChlbRM8OJWpJFMX4/i8OEU538uiSv0P6roZcbpe/WfhEO+AT8SHVKfp8qhDQzaz7Q+1/ixMy7hBRidnQ=="], + + "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.29.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-WM9lIkNdkhVwiArmLxFXpWndFGuOka4oJOZh8EP3Vb8q5lzdSCBuhjavJsw68Q9AKDGeOOIHYzYm4ZFvmWez5g=="], + + "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.29.1", "", { "os": "linux", "cpu": "x64" }, "sha512-87xYCwb0cPGZFoGiErT1eDcssByaLX4fc0z2nRM6eMtV9njAfEE6OW3UniAoDhX4Iq5xQVpE6qO9aJbCFumKYQ=="], + + "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.29.1", "", { "os": "linux", "cpu": "x64" }, "sha512-xufkSNppNOdVRCEC4WKvlR1FBDyqCSCpQeMMgv9ZyXqqtKBfkw1yfGMTUTs9Qsl6WQbJnsGboWCp7pJGkeMhKA=="], + + "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.29.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-F2OiJ42m77lSkizZQLuC+jiZ2cgueWQL5YC9tjo3AgaEw+KJmVxHGSyQfDUoYR9cci0lAywv2Clmckzulcq6ig=="], + + "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.29.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-rYRe5S0FcjlOBZQHgbTKNrqxCBUmgDJem/VQTCcTnA2KCabYSWQDrytOzX7avb79cAAweNmMUb/Zw18RNd4mng=="], + + "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.29.1", "", { "os": "win32", "cpu": "x64" }, "sha512-+10CMg9vt1MoHj6x1pxyjPSMjHTIlqs8/tBztXvPAx24SKs9jwVnKqHJumlH/IzhaPUaj3T6T6wfZr8okdXaIg=="], + + "@types/bun": ["@types/bun@1.1.14", "", { "dependencies": { "bun-types": "1.1.37" } }, "sha512-opVYiFGtO2af0dnWBdZWlioLBoxSdDO5qokaazLhq8XQtGZbY4pY3/JxY8Zdf/hEwGubbp7ErZXoN1+h2yesxA=="], + + "@types/estree": ["@types/estree@1.0.6", "", {}, "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw=="], + + "@types/jsdom": ["@types/jsdom@21.1.7", "", { "dependencies": { "@types/node": "*", "@types/tough-cookie": "*", "parse5": "^7.0.0" } }, "sha512-yOriVnggzrnQ3a9OKOCxaVuSug3w3/SbOj5i7VwXWZEyUNl3bLF9V3MfxGbZKuwqJOQyRfqXyROBB1CoZLFWzA=="], + + "@types/mime-types": ["@types/mime-types@2.1.4", "", {}, "sha512-lfU4b34HOri+kAY5UheuFMWPDOI+OPceBSHZKp69gEyTL/mmJ4cnU6Y/rlme3UL3GyOn6Y42hyIEw0/q8sWx5w=="], + + "@types/node": ["@types/node@20.12.14", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-scnD59RpYD91xngrQQLGkE+6UrHUPzeKZWhhjBSa3HSkwjbQc38+q3RoIVEwxQGRw3M+j5hpNAM+lgV3cVormg=="], + + "@types/react": ["@types/react@19.0.2", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-USU8ZI/xyKJwFTpjSVIrSeHBVAGagkHQKPNbxeWwql/vDmnTIBgx+TJnhFnj1NXgz8XfprU0egV2dROLGpsBEg=="], + + "@types/react-dom": ["@types/react-dom@19.0.2", "", { "peerDependencies": { "@types/react": "^19.0.0" } }, "sha512-c1s+7TKFaDRRxr1TxccIX2u7sfCnc3RxkVyBIUA2lCpyqCF+QoAwQ/CBg7bsMdVwP120HEH143VQezKtef5nCg=="], + + "@types/semver": ["@types/semver@7.5.8", "", {}, "sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ=="], + + "@types/tough-cookie": ["@types/tough-cookie@4.0.5", "", {}, "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA=="], + + "@types/web": ["@types/web@0.0.188", "", {}, "sha512-dKowKMo4P0W6f2U5i66uJ+/MlSHzBnznI0ExP1xnpcF/9/CJQT4VYvIDqz7ocWDvd0pB1QPgjOe4dVSycQrbuA=="], + + "@types/ws": ["@types/ws@8.5.13", "", { "dependencies": { "@types/node": "*" } }, "sha512-osM/gWBTPKgHV8XkTunnegTRIsvF6owmf5w+JtAfOw472dptdm0dlGv4xCt6GwQRcC2XVOvvRE/0bAoQcL2QkA=="], + + "agent-base": ["agent-base@7.1.3", "", {}, "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw=="], + + "asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="], + + "bun-types": ["bun-types@1.1.37", "", { "dependencies": { "@types/node": "~20.12.8", "@types/ws": "~8.5.10" } }, "sha512-C65lv6eBr3LPJWFZ2gswyrGZ82ljnH8flVE03xeXxKhi2ZGtFiO4isRKTKnitbSqtRAcaqYSR6djt1whI66AbA=="], + + "combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="], + + "cookie": ["cookie@1.0.2", "", {}, "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA=="], + + "cssstyle": ["cssstyle@4.1.0", "", { "dependencies": { "rrweb-cssom": "^0.7.1" } }, "sha512-h66W1URKpBS5YMI/V8PyXvTMFT8SupJ1IzoIV8IeBC/ji8WVmrO8dGlTi+2dh6whmdk6BiKJLD/ZBkhWbcg6nA=="], + + "csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="], + + "data-urls": ["data-urls@5.0.0", "", { "dependencies": { "whatwg-mimetype": "^4.0.0", "whatwg-url": "^14.0.0" } }, "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg=="], + + "debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], + + "decimal.js": ["decimal.js@10.4.3", "", {}, "sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA=="], + + "delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="], + + "entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="], + + "esbuild": ["esbuild@0.24.0", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.24.0", "@esbuild/android-arm": "0.24.0", "@esbuild/android-arm64": "0.24.0", "@esbuild/android-x64": "0.24.0", "@esbuild/darwin-arm64": "0.24.0", "@esbuild/darwin-x64": "0.24.0", "@esbuild/freebsd-arm64": "0.24.0", "@esbuild/freebsd-x64": "0.24.0", "@esbuild/linux-arm": "0.24.0", "@esbuild/linux-arm64": "0.24.0", "@esbuild/linux-ia32": "0.24.0", "@esbuild/linux-loong64": "0.24.0", "@esbuild/linux-mips64el": "0.24.0", "@esbuild/linux-ppc64": "0.24.0", "@esbuild/linux-riscv64": "0.24.0", "@esbuild/linux-s390x": "0.24.0", "@esbuild/linux-x64": "0.24.0", "@esbuild/netbsd-x64": "0.24.0", "@esbuild/openbsd-arm64": "0.24.0", "@esbuild/openbsd-x64": "0.24.0", "@esbuild/sunos-x64": "0.24.0", "@esbuild/win32-arm64": "0.24.0", "@esbuild/win32-ia32": "0.24.0", "@esbuild/win32-x64": "0.24.0" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-FuLPevChGDshgSicjisSooU0cemp/sGXR841D5LHMB7mTVOmsEHcAxaH3irL53+8YDIeVNQEySh4DaYU/iuPqQ=="], + + "form-data": ["form-data@4.0.1", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "mime-types": "^2.1.12" } }, "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw=="], + + "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], + + "html-encoding-sniffer": ["html-encoding-sniffer@4.0.0", "", { "dependencies": { "whatwg-encoding": "^3.1.1" } }, "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ=="], + + "http-proxy-agent": ["http-proxy-agent@7.0.2", "", { "dependencies": { "agent-base": "^7.1.0", "debug": "^4.3.4" } }, "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig=="], + + "https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="], + + "iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], + + "is-potential-custom-element-name": ["is-potential-custom-element-name@1.0.1", "", {}, "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ=="], + + "jsdom": ["jsdom@25.0.1", "", { "dependencies": { "cssstyle": "^4.1.0", "data-urls": "^5.0.0", "decimal.js": "^10.4.3", "form-data": "^4.0.0", "html-encoding-sniffer": "^4.0.0", "http-proxy-agent": "^7.0.2", "https-proxy-agent": "^7.0.5", "is-potential-custom-element-name": "^1.0.1", "nwsapi": "^2.2.12", "parse5": "^7.1.2", "rrweb-cssom": "^0.7.1", "saxes": "^6.0.0", "symbol-tree": "^3.2.4", "tough-cookie": "^5.0.0", "w3c-xmlserializer": "^5.0.0", "webidl-conversions": "^7.0.0", "whatwg-encoding": "^3.1.1", "whatwg-mimetype": "^4.0.0", "whatwg-url": "^14.0.0", "ws": "^8.18.0", "xml-name-validator": "^5.0.0" }, "peerDependencies": { "canvas": "^2.11.2" }, "optionalPeers": ["canvas"] }, "sha512-8i7LzZj7BF8uplX+ZyOlIz86V6TAsSs+np6m1kpW9u0JWi4z/1t+FzcK1aek+ybTnAC4KhBL4uXCNT0wcUIeCw=="], + + "marked": ["marked@15.0.5", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-xN+kSuqHjxWg+Q47yhhZMUP+kO1qHobvXkkm6FX+7N6lDvanLDd8H7AQ0jWDDyq+fDt/cSrJaBGyWYHXy0KQWA=="], + + "mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], + + "mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], + + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + + "nanoid": ["nanoid@5.0.9", "", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-Aooyr6MXU6HpvvWXKoVoXwKMs/KyVakWwg7xQfv5/S/RIgJMy0Ifa45H9qqYy7pTCszrHzP21Uk4PZq2HpEM8Q=="], + + "nwsapi": ["nwsapi@2.2.16", "", {}, "sha512-F1I/bimDpj3ncaNDhfyMWuFqmQDBwDB0Fogc2qpL3BWvkQteFD/8BzWuIRl83rq0DXfm8SGt/HFhLXZyljTXcQ=="], + + "parse5": ["parse5@7.2.1", "", { "dependencies": { "entities": "^4.5.0" } }, "sha512-BuBYQYlv1ckiPdQi/ohiivi9Sagc9JG+Ozs0r7b/0iK3sKmrb0b9FdWdBbOdx6hBCM/F9Ir82ofnBhtZOjCRPQ=="], + + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], + + "postcss": ["postcss@8.4.49", "", { "dependencies": { "nanoid": "^3.3.7", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA=="], + + "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], + + "rollup": ["rollup@4.29.1", "", { "dependencies": { "@types/estree": "1.0.6" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.29.1", "@rollup/rollup-android-arm64": "4.29.1", "@rollup/rollup-darwin-arm64": "4.29.1", "@rollup/rollup-darwin-x64": "4.29.1", "@rollup/rollup-freebsd-arm64": "4.29.1", "@rollup/rollup-freebsd-x64": "4.29.1", "@rollup/rollup-linux-arm-gnueabihf": "4.29.1", "@rollup/rollup-linux-arm-musleabihf": "4.29.1", "@rollup/rollup-linux-arm64-gnu": "4.29.1", "@rollup/rollup-linux-arm64-musl": "4.29.1", "@rollup/rollup-linux-loongarch64-gnu": "4.29.1", "@rollup/rollup-linux-powerpc64le-gnu": "4.29.1", "@rollup/rollup-linux-riscv64-gnu": "4.29.1", "@rollup/rollup-linux-s390x-gnu": "4.29.1", "@rollup/rollup-linux-x64-gnu": "4.29.1", "@rollup/rollup-linux-x64-musl": "4.29.1", "@rollup/rollup-win32-arm64-msvc": "4.29.1", "@rollup/rollup-win32-ia32-msvc": "4.29.1", "@rollup/rollup-win32-x64-msvc": "4.29.1", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-RaJ45M/kmJUzSWDs1Nnd5DdV4eerC98idtUOVr6FfKcgxqvjwHmxc5upLF9qZU9EpsVzzhleFahrT3shLuJzIw=="], + + "rrweb-cssom": ["rrweb-cssom@0.7.1", "", {}, "sha512-TrEMa7JGdVm0UThDJSx7ddw5nVm3UJS9o9CCIZ72B1vSyEZoziDqBYP3XIoi/12lKrJR8rE3jeFHMok2F/Mnsg=="], + + "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], + + "saxes": ["saxes@6.0.0", "", { "dependencies": { "xmlchars": "^2.2.0" } }, "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA=="], + + "semver": ["semver@7.6.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A=="], + + "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], + + "symbol-tree": ["symbol-tree@3.2.4", "", {}, "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw=="], + + "tldts": ["tldts@6.1.68", "", { "dependencies": { "tldts-core": "^6.1.68" }, "bin": { "tldts": "bin/cli.js" } }, "sha512-JKF17jROiYkjJPT73hUTEiTp2OBCf+kAlB+1novk8i6Q6dWjHsgEjw9VLiipV4KTJavazXhY1QUXyQFSem2T7w=="], + + "tldts-core": ["tldts-core@6.1.68", "", {}, "sha512-85TdlS/DLW/gVdf2oyyzqp3ocS30WxjaL4la85EArl9cHUR/nizifKAJPziWewSZjDZS71U517/i6ciUeqtB5Q=="], + + "tough-cookie": ["tough-cookie@5.0.0", "", { "dependencies": { "tldts": "^6.1.32" } }, "sha512-FRKsF7cz96xIIeMZ82ehjC3xW2E+O2+v11udrDYewUbszngYhsGa8z6YUMMzO9QJZzzyd0nGGXnML/TReX6W8Q=="], + + "tr46": ["tr46@5.0.0", "", { "dependencies": { "punycode": "^2.3.1" } }, "sha512-tk2G5R2KRwBd+ZN0zaEXpmzdKyOYksXwywulIX95MBODjSzMIuQnQ3m8JxgbhnL1LeVo7lqQKsYa1O3Htl7K5g=="], + + "typescript": ["typescript@5.7.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg=="], + + "undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], + + "vite": ["vite@6.0.5", "", { "dependencies": { "esbuild": "0.24.0", "postcss": "^8.4.49", "rollup": "^4.23.0" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-akD5IAH/ID5imgue2DYhzsEwCi0/4VKY31uhMLEYJwPP4TiUp8pL5PIK+Wo7H8qT8JY9i+pVfPydcFPYD1EL7g=="], + + "w3c-xmlserializer": ["w3c-xmlserializer@5.0.0", "", { "dependencies": { "xml-name-validator": "^5.0.0" } }, "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA=="], + + "webidl-conversions": ["webidl-conversions@7.0.0", "", {}, "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g=="], + + "whatwg-encoding": ["whatwg-encoding@3.1.1", "", { "dependencies": { "iconv-lite": "0.6.3" } }, "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ=="], + + "whatwg-mimetype": ["whatwg-mimetype@4.0.0", "", {}, "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg=="], + + "whatwg-url": ["whatwg-url@14.1.0", "", { "dependencies": { "tr46": "^5.0.0", "webidl-conversions": "^7.0.0" } }, "sha512-jlf/foYIKywAt3x/XWKZ/3rz8OSJPiWktjmk891alJUEjiVxKX9LEO92qH3hv4aJ0mN3MWPvGMCy8jQi95xK4w=="], + + "ws": ["ws@8.18.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw=="], + + "xml-name-validator": ["xml-name-validator@5.0.0", "", {}, "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg=="], + + "xmlchars": ["xmlchars@2.2.0", "", {}, "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw=="], + + "postcss/nanoid": ["nanoid@3.3.8", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w=="], + } +} diff --git a/docs/Hyper.svg b/docs/Hyper.svg new file mode 100644 index 0000000..95a6c21 --- /dev/null +++ b/docs/Hyper.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git "a/docs/h(\342\232\241\357\270\217).svg" "b/docs/h(\342\232\241\357\270\217).svg" new file mode 100644 index 0000000..ad467e4 --- /dev/null +++ "b/docs/h(\342\232\241\357\270\217).svg" @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/docs/thomas.jpg b/docs/thomas.jpg new file mode 100644 index 0000000..05bf3a3 Binary files /dev/null and b/docs/thomas.jpg differ diff --git a/docs/versions.md b/docs/versions.md new file mode 100644 index 0000000..14302cc --- /dev/null +++ b/docs/versions.md @@ -0,0 +1,18 @@ +# Versions + +Hyperactive expects a certain version of `@types/web` to be installed. This is because the library is built from the `@types/web` package, and the version of `@types/web` is used to determine the version of the library. + +In the future, this may be relaxed, but for now, it is required if you work with Hyperactive on the client side. This decision was made because the library can be used in a server-side context, and the version of `@types/web` is not required in that case. Depending on a global `lib.dom.d.ts` pollutes the global scope and can cause unexpected type errors. + +This table shows the version of `@types/web` that Hyperactive expects to be installed. + +| Hyperactive Version | @types/web Version | +| ------------------- | ------------------ | +| 2.0.0-beta.8 | 0.0.234 | +| 2.0.0-beta.7 | 0.0.232 | +| 2.0.0-beta.6 | 0.0.232 | +| 2.0.0-beta.5 | 0.0.232 | +| 2.0.0-beta.4 | 0.0.232 | +| 2.0.0-beta.3 | 0.0.188 | +| 2.0.0-beta.2 | 0.0.188 | +| 2.0.0-beta.1 | 0.0.188 | \ No newline at end of file diff --git a/node/index.ts b/node/index.ts deleted file mode 100644 index c6f8882..0000000 --- a/node/index.ts +++ /dev/null @@ -1,110 +0,0 @@ -import { Attrs } from "./types"; -import { parseSelector } from "./parseSelector"; -import { flatCat, isIterable, collect, map, isObject } from "./util"; - -interface Node { - tag: string; - key?: string; - attrs: Attrs; - children: Iterable; -} - -type Nodes = Node | Iterable; - -interface h { - (element: string, selector: string, attr: Attrs, ...children: Nodes[]): Node; - (element: string, selector: string, ...children: Nodes[]): Node; - (selector: string, attr: Attrs, ...children: Nodes[]): Node; - (selector: string, ...children: Nodes[]): Node; - (attr: Attrs, ...children: Nodes[]): Node; - (...children: Nodes[]): Node; -} - -const composeAttributes = (a: Attrs, b: Attrs) => { - if (a.id && b.id) { - throw new Error(`Multiple IDs are not allowed: ${a.id}, ${b.id}`); - } - - return Object.assign(a, b, { class: [...(a.class || []), ...(b.class || [])] }); -}; - -const resolveChildren = (a: Nodes[], b: T | Nodes): Iterable => { - if (b instanceof Node) { - return flatCat(...a, [b]); - } else if (isIterable(b)) { - return flatCat(...a, b); - } else return flatCat(...a); -}; - -export const isNode = (x: any): x is Node => x instanceof Node; - -export const isAttr = (x: any): x is Attrs => isObject(x) && !isIterable(x) && !isNode(x); - -class Node implements Node { - constructor(a?: unknown, b?: unknown, c?: unknown, ...otherChildren: Nodes[]) { - if (typeof b === "string") { - // (string, string, ...) -> Node; - - // if c is an object, but not an instance of Node or Array, it's attrs - const attrs: Attrs = isAttr(c) ? c : {}; - // if c is an instance of Node or Array, it joins otherChildren - const children = resolveChildren(otherChildren, c); - const { tag, ...classID } = parseSelector(b); - Object.assign(this, { - tag: a, - attrs: composeAttributes(classID, attrs), - children, - }); - } else if (typeof a === "string") { - // at least a is defined - // if b is an object, but not an instance of Node or Array, it's attrs - const attrs: Attrs = isAttr(b) ? b : {}; - // if c or b are instances of Node or Array, it joins otherChildren - const children = resolveChildren(otherChildren, [c, b]); - const { tag, ...classID } = parseSelector(a, { tagMode: true }); - if (Array.isArray(c)) { - Object.assign(this, { - tag, - attrs: composeAttributes(classID, attrs), - children, - }); - } else if (c instanceof Node) { - } - } - } - - toJSON(): any { - return Object.assign({}, this, { - children: collect(this.children ? map(child => child.toJSON(), this.children) : []), - }); - } -} - -// export function h(a: string, b: string, c: Attrs, ...otherChildren: Nodes[]): Node { -// let node: Node = Object.create(Node); - -// if (typeof b === "string") { -// return Object.assign(node, { -// tag: a, -// attrs: Object.assign(parseSelector(b), c), -// children: otherChildren, -// NODE, -// }); -// } else if (typeof a === "string") { -// const { tag, ...attrs } = parseSelector(a, { tagMode: true }); -// if () -// return { tag, attrs, NODE }; -// } - -// return node; -// } - -console.log( - new Node( - "div", - "#main.container", - { "data-attr": "0" }, - new Node(".child", { "data-attr": "1" }), - ).toJSON(), - new Node(".child", { "data-attr": "1" }), -); diff --git a/node/package.json b/node/package.json deleted file mode 100644 index 0e84ca3..0000000 --- a/node/package.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "name": "@hyperactive/node", - "version": "0.0.1", - "description": "Node utility for hyperactive", - "author": "Muthu Kumar <@MKRhere> (https://mkr.pw)", - "license": "MIT", - "scripts": { - "preinstall": "node -e \"!process.env.npm_config_user_agent.startsWith('pnpm/') && !console.log('Use \\`npm i -g pnpm\\` and \\`pnpm install\\` to install dependencies in this package\\n') && process.exit(1)\"" - } -} diff --git a/node/types.ts b/node/types.ts deleted file mode 100644 index 0b9b9aa..0000000 --- a/node/types.ts +++ /dev/null @@ -1,4 +0,0 @@ -export type Attrs = Partial<{ - id: string; - class: string[]; -}>; diff --git a/node/util.ts b/node/util.ts deleted file mode 100644 index 1928b3a..0000000 --- a/node/util.ts +++ /dev/null @@ -1,22 +0,0 @@ -export const isObject = (x: any): x is object => x && typeof x === "object"; - -export const isIterable = (x: any): x is Iterable => - x && typeof x === "object" && typeof (x[Symbol.iterator] as any) === "function"; - -export function* flatCat(...xs: (T | Iterable)[]): Iterable { - for (const x of xs) { - if (isIterable(x)) { - yield* flatCat(...x); - } else yield x; - } -} - -export function* map(f: (x: T) => U, xs: Iterable) { - for (const x of xs) { - yield f(x); - } -} - -export function collect(iter: Iterable) { - return [...iter]; -} diff --git a/package.json b/package.json index fbce62c..d7fdf53 100644 --- a/package.json +++ b/package.json @@ -1,13 +1,14 @@ { "name": "hyperactive", - "version": "0.0.0", - "description": "The hyperactive suite of web application development tools", - "author": "Muthu Kumar <@MKRhere> (https://mkr.pw)", - "license": "MIT", + "private": true, + "workspaces": [ + "packages/*" + ], + "type": "module", "scripts": { - "preinstall": "node -e \"!process.env.npm_config_user_agent.startsWith('pnpm/') && !console.log('Use \\`npm i -g pnpm\\` and \\`pnpm --recursive install\\` to install dependencies in this repository\\n') && process.exit(1)\"" + "build": "cd packages/hyper && bun run build" }, - "devDependencies": { - "@types/node": "^13.7.6" + "dependencies": { + "typescript": "^5.7.2" } } diff --git a/packages/blink/.gitignore b/packages/blink/.gitignore new file mode 100644 index 0000000..ffd370c --- /dev/null +++ b/packages/blink/.gitignore @@ -0,0 +1,178 @@ +# Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore + +*.db* +config.json + +# Logs + +logs +_.log +npm-debug.log_ +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Caches + +.cache + +# Diagnostic reports (https://nodejs.org/api/report.html) + +report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json + +# Runtime data + +pids +_.pid +_.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover + +lib-cov + +# Coverage directory used by tools like istanbul + +coverage +*.lcov + +# nyc test coverage + +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) + +.grunt + +# Bower dependency directory (https://bower.io/) + +bower_components + +# node-waf configuration + +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) + +build/Release + +# Dependency directories + +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) + +web_modules/ + +# TypeScript cache + +*.tsbuildinfo + +# Optional npm cache directory + +.npm + +# Optional eslint cache + +.eslintcache + +# Optional stylelint cache + +.stylelintcache + +# Microbundle cache + +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history + +.node_repl_history + +# Output of 'npm pack' + +*.tgz + +# Yarn Integrity file + +.yarn-integrity + +# dotenv environment variable files + +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# parcel-bundler cache (https://parceljs.org/) + +.parcel-cache + +# Next.js build output + +.next +out + +# Nuxt.js build / generate output + +.nuxt +dist + +# Gatsby files + +# Comment in the public line in if your project uses Gatsby and not Next.js + +# https://nextjs.org/blog/next-9-1#public-directory-support + +# public + +# vuepress build output + +.vuepress/dist + +# vuepress v2.x temp and cache directory + +.temp + +# Docusaurus cache and generated files + +.docusaurus + +# Serverless directories + +.serverless/ + +# FuseBox cache + +.fusebox/ + +# DynamoDB Local files + +.dynamodb/ + +# TernJS port file + +.tern-port + +# Stores VSCode versions used for testing VSCode extensions + +.vscode-test + +# yarn v2 + +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* + +# IntelliJ based IDEs +.idea + +# Finder (MacOS) folder config +.DS_Store diff --git a/packages/blink/LICENSE b/packages/blink/LICENSE new file mode 100644 index 0000000..249ab64 --- /dev/null +++ b/packages/blink/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) Feathers Studio + +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/packages/blink/README.md b/packages/blink/README.md new file mode 100644 index 0000000..58a1c85 --- /dev/null +++ b/packages/blink/README.md @@ -0,0 +1,15 @@ +# @feathers-studio/blink + +To install dependencies: + +```bash +bun install +``` + +To run: + +```bash +bun run src/index.ts +``` + +This project was created using `bun init` in bun v1.1.34. [Bun](https://bun.sh) is a fast all-in-one JavaScript runtime. diff --git a/packages/blink/assets/img/1200x630.jpg b/packages/blink/assets/img/1200x630.jpg new file mode 100644 index 0000000..cbbd9ee Binary files /dev/null and b/packages/blink/assets/img/1200x630.jpg differ diff --git a/packages/blink/assets/img/favicon-96x96.png b/packages/blink/assets/img/favicon-96x96.png new file mode 100644 index 0000000..81cdbb8 Binary files /dev/null and b/packages/blink/assets/img/favicon-96x96.png differ diff --git a/packages/blink/assets/img/favicon.ico b/packages/blink/assets/img/favicon.ico new file mode 100644 index 0000000..84a06dc Binary files /dev/null and b/packages/blink/assets/img/favicon.ico differ diff --git a/packages/blink/assets/normalise.css b/packages/blink/assets/normalise.css new file mode 100644 index 0000000..f61de5c --- /dev/null +++ b/packages/blink/assets/normalise.css @@ -0,0 +1,427 @@ +/*! normalize.css v3.0.2 | MIT License | git.io/normalize */ + +/** + * 1. Set default font family to sans-serif. + * 2. Prevent iOS text size adjust after orientation change, without disabling + * user zoom. + */ + +html { + font-family: sans-serif; /* 1 */ + -ms-text-size-adjust: 100%; /* 2 */ + -webkit-text-size-adjust: 100%; /* 2 */ +} + +/** + * Remove default margin. + */ + +body { + margin: 0; +} + +/* HTML5 display definitions + ========================================================================== */ + +/** + * Correct `block` display not defined for any HTML5 element in IE 8/9. + * Correct `block` display not defined for `details` or `summary` in IE 10/11 + * and Firefox. + * Correct `block` display not defined for `main` in IE 11. + */ + +article, +aside, +details, +figcaption, +figure, +footer, +header, +hgroup, +main, +menu, +nav, +section, +summary { + display: block; +} + +/** + * 1. Correct `inline-block` display not defined in IE 8/9. + * 2. Normalize vertical alignment of `progress` in Chrome, Firefox, and Opera. + */ + +audio, +canvas, +progress, +video { + display: inline-block; /* 1 */ + vertical-align: baseline; /* 2 */ +} + +/** + * Prevent modern browsers from displaying `audio` without controls. + * Remove excess height in iOS 5 devices. + */ + +audio:not([controls]) { + display: none; + height: 0; +} + +/** + * Address `[hidden]` styling not present in IE 8/9/10. + * Hide the `template` element in IE 8/9/11, Safari, and Firefox < 22. + */ + +[hidden], +template { + display: none; +} + +/* Links + ========================================================================== */ + +/** + * Remove the gray background color from active links in IE 10. + */ + +a { + background-color: transparent; +} + +/** + * Improve readability when focused and also mouse hovered in all browsers. + */ + +a:active, +a:hover { + outline: 0; +} + +/* Text-level semantics + ========================================================================== */ + +/** + * Address styling not present in IE 8/9/10/11, Safari, and Chrome. + */ + +abbr[title] { + border-bottom: 1px dotted; +} + +/** + * Address style set to `bolder` in Firefox 4+, Safari, and Chrome. + */ + +b, +strong { + font-weight: bold; +} + +/** + * Address styling not present in Safari and Chrome. + */ + +dfn { + font-style: italic; +} + +/** + * Address variable `h1` font-size and margin within `section` and `article` + * contexts in Firefox 4+, Safari, and Chrome. + */ + +h1 { + font-size: 2em; + margin: 0.67em 0; +} + +/** + * Address styling not present in IE 8/9. + */ + +mark { + background: #ff0; + color: #000; +} + +/** + * Address inconsistent and variable font size in all browsers. + */ + +small { + font-size: 80%; +} + +/** + * Prevent `sub` and `sup` affecting `line-height` in all browsers. + */ + +sub, +sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; +} + +sup { + top: -0.5em; +} + +sub { + bottom: -0.25em; +} + +/* Embedded content + ========================================================================== */ + +/** + * Remove border when inside `a` element in IE 8/9/10. + */ + +img { + border: 0; +} + +/** + * Correct overflow not hidden in IE 9/10/11. + */ + +svg:not(:root) { + overflow: hidden; +} + +/* Grouping content + ========================================================================== */ + +/** + * Address margin not present in IE 8/9 and Safari. + */ + +figure { + margin: 1em 40px; +} + +/** + * Address differences between Firefox and other browsers. + */ + +hr { + -moz-box-sizing: content-box; + box-sizing: content-box; + height: 0; +} + +/** + * Contain overflow in all browsers. + */ + +pre { + overflow: auto; +} + +/** + * Address odd `em`-unit font size rendering in all browsers. + */ + +code, +kbd, +pre, +samp { + font-family: monospace, monospace; + font-size: 1em; +} + +/* Forms + ========================================================================== */ + +/** + * Known limitation: by default, Chrome and Safari on OS X allow very limited + * styling of `select`, unless a `border` property is set. + */ + +/** + * 1. Correct color not being inherited. + * Known issue: affects color of disabled elements. + * 2. Correct font properties not being inherited. + * 3. Address margins set differently in Firefox 4+, Safari, and Chrome. + */ + +button, +input, +optgroup, +select, +textarea { + color: inherit; /* 1 */ + font: inherit; /* 2 */ + margin: 0; /* 3 */ +} + +/** + * Address `overflow` set to `hidden` in IE 8/9/10/11. + */ + +button { + overflow: visible; +} + +/** + * Address inconsistent `text-transform` inheritance for `button` and `select`. + * All other form control elements do not inherit `text-transform` values. + * Correct `button` style inheritance in Firefox, IE 8/9/10/11, and Opera. + * Correct `select` style inheritance in Firefox. + */ + +button, +select { + text-transform: none; +} + +/** + * 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio` + * and `video` controls. + * 2. Correct inability to style clickable `input` types in iOS. + * 3. Improve usability and consistency of cursor style between image-type + * `input` and others. + */ + +button, +html input[type="button"], /* 1 */ +input[type="reset"], +input[type="submit"] { + -webkit-appearance: button; /* 2 */ + cursor: pointer; /* 3 */ +} + +/** + * Re-set default cursor for disabled elements. + */ + +button[disabled], +html input[disabled] { + cursor: default; +} + +/** + * Remove inner padding and border in Firefox 4+. + */ + +button::-moz-focus-inner, +input::-moz-focus-inner { + border: 0; + padding: 0; +} + +/** + * Address Firefox 4+ setting `line-height` on `input` using `!important` in + * the UA stylesheet. + */ + +input { + line-height: normal; +} + +/** + * It's recommended that you don't attempt to style these elements. + * Firefox's implementation doesn't respect box-sizing, padding, or width. + * + * 1. Address box sizing set to `content-box` in IE 8/9/10. + * 2. Remove excess padding in IE 8/9/10. + */ + +input[type="checkbox"], +input[type="radio"] { + box-sizing: border-box; /* 1 */ + padding: 0; /* 2 */ +} + +/** + * Fix the cursor style for Chrome's increment/decrement buttons. For certain + * `font-size` values of the `input`, it causes the cursor style of the + * decrement button to change from `default` to `text`. + */ + +input[type="number"]::-webkit-inner-spin-button, +input[type="number"]::-webkit-outer-spin-button { + height: auto; +} + +/** + * 1. Address `appearance` set to `searchfield` in Safari and Chrome. + * 2. Address `box-sizing` set to `border-box` in Safari and Chrome + * (include `-moz` to future-proof). + */ + +input[type="search"] { + -webkit-appearance: textfield; /* 1 */ + -moz-box-sizing: content-box; + -webkit-box-sizing: content-box; /* 2 */ + box-sizing: content-box; +} + +/** + * Remove inner padding and search cancel button in Safari and Chrome on OS X. + * Safari (but not Chrome) clips the cancel button when the search input has + * padding (and `textfield` appearance). + */ + +input[type="search"]::-webkit-search-cancel-button, +input[type="search"]::-webkit-search-decoration { + -webkit-appearance: none; +} + +/** + * Define consistent border, margin, and padding. + */ + +fieldset { + border: 1px solid #c0c0c0; + margin: 0 2px; + padding: 0.35em 0.625em 0.75em; +} + +/** + * 1. Correct `color` not being inherited in IE 8/9/10/11. + * 2. Remove padding so people aren't caught out if they zero out fieldsets. + */ + +legend { + border: 0; /* 1 */ + padding: 0; /* 2 */ +} + +/** + * Remove default vertical scrollbar in IE 8/9/10/11. + */ + +textarea { + overflow: auto; +} + +/** + * Don't inherit the `font-weight` (applied by a rule above). + * NOTE: the default cannot safely be changed in Chrome and Safari on OS X. + */ + +optgroup { + font-weight: bold; +} + +/* Tables + ========================================================================== */ + +/** + * Remove most spacing between table cells. + */ + +table { + border-collapse: collapse; + border-spacing: 0; +} + +td, +th { + padding: 0; +} diff --git a/packages/blink/assets/style.css b/packages/blink/assets/style.css new file mode 100644 index 0000000..1360c0f --- /dev/null +++ b/packages/blink/assets/style.css @@ -0,0 +1,2683 @@ +/*! + * Pico CSS ✨ v2.0.6 (https://picocss.com) + * Copyright 2019-2024 - Licensed under MIT + */ +:root { + --pico-font-family-emoji: "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; + --pico-font-family-sans-serif: system-ui, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, Helvetica, Arial, + "Helvetica Neue", sans-serif, var(--pico-font-family-emoji); + --pico-font-family-monospace: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace, + var(--pico-font-family-emoji); + --pico-font-family: var(--pico-font-family-sans-serif); + --pico-line-height: 1.5; + --pico-font-weight: 400; + --pico-font-size: 100%; + --pico-text-underline-offset: 0.1rem; + --pico-border-radius: 0.25rem; + --pico-border-width: 0.0625rem; + --pico-outline-width: 0.125rem; + --pico-transition: 0.2s ease-in-out; + --pico-spacing: 1rem; + --pico-typography-spacing-vertical: 1rem; + --pico-block-spacing-vertical: var(--pico-spacing); + --pico-block-spacing-horizontal: var(--pico-spacing); + --pico-grid-column-gap: var(--pico-spacing); + --pico-grid-row-gap: var(--pico-spacing); + --pico-form-element-spacing-vertical: 0.75rem; + --pico-form-element-spacing-horizontal: 1rem; + --pico-group-box-shadow: 0 0 0 rgba(0, 0, 0, 0); + --pico-group-box-shadow-focus-with-button: 0 0 0 var(--pico-outline-width) var(--pico-primary-focus); + --pico-group-box-shadow-focus-with-input: 0 0 0 0.0625rem var(--pico-form-element-border-color); + --pico-modal-overlay-backdrop-filter: blur(0.375rem); + --pico-nav-element-spacing-vertical: 1rem; + --pico-nav-element-spacing-horizontal: 0.5rem; + --pico-nav-link-spacing-vertical: 0.5rem; + --pico-nav-link-spacing-horizontal: 0.5rem; + --pico-nav-breadcrumb-divider: ">"; + --pico-icon-checkbox: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(255, 255, 255)' stroke-width='4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='20 6 9 17 4 12'%3E%3C/polyline%3E%3C/svg%3E"); + --pico-icon-minus: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(255, 255, 255)' stroke-width='4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cline x1='5' y1='12' x2='19' y2='12'%3E%3C/line%3E%3C/svg%3E"); + --pico-icon-chevron: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(136, 145, 164)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E"); + --pico-icon-date: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(136, 145, 164)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='3' y='4' width='18' height='18' rx='2' ry='2'%3E%3C/rect%3E%3Cline x1='16' y1='2' x2='16' y2='6'%3E%3C/line%3E%3Cline x1='8' y1='2' x2='8' y2='6'%3E%3C/line%3E%3Cline x1='3' y1='10' x2='21' y2='10'%3E%3C/line%3E%3C/svg%3E"); + --pico-icon-time: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(136, 145, 164)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'%3E%3C/circle%3E%3Cpolyline points='12 6 12 12 16 14'%3E%3C/polyline%3E%3C/svg%3E"); + --pico-icon-search: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(136, 145, 164)' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='11' cy='11' r='8'%3E%3C/circle%3E%3Cline x1='21' y1='21' x2='16.65' y2='16.65'%3E%3C/line%3E%3C/svg%3E"); + --pico-icon-close: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(136, 145, 164)' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'%3E%3Cline x1='18' y1='6' x2='6' y2='18'%3E%3C/line%3E%3Cline x1='6' y1='6' x2='18' y2='18'%3E%3C/line%3E%3C/svg%3E"); + --pico-icon-loading: url("data:image/svg+xml,%3Csvg fill='none' height='24' width='24' viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg' %3E%3Cstyle%3E g %7B animation: rotate 2s linear infinite; transform-origin: center center; %7D circle %7B stroke-dasharray: 75,100; stroke-dashoffset: -5; animation: dash 1.5s ease-in-out infinite; stroke-linecap: round; %7D @keyframes rotate %7B 0%25 %7B transform: rotate(0deg); %7D 100%25 %7B transform: rotate(360deg); %7D %7D @keyframes dash %7B 0%25 %7B stroke-dasharray: 1,100; stroke-dashoffset: 0; %7D 50%25 %7B stroke-dasharray: 44.5,100; stroke-dashoffset: -17.5; %7D 100%25 %7B stroke-dasharray: 44.5,100; stroke-dashoffset: -62; %7D %7D %3C/style%3E%3Cg%3E%3Ccircle cx='12' cy='12' r='10' fill='none' stroke='rgb(136, 145, 164)' stroke-width='4' /%3E%3C/g%3E%3C/svg%3E"); +} +@media (min-width: 576px) { + :root { + --pico-font-size: 106.25%; + } +} +@media (min-width: 768px) { + :root { + --pico-font-size: 112.5%; + } +} +@media (min-width: 1024px) { + :root { + --pico-font-size: 118.75%; + } +} +@media (min-width: 1280px) { + :root { + --pico-font-size: 125%; + } +} +@media (min-width: 1536px) { + :root { + --pico-font-size: 131.25%; + } +} +a { + --pico-text-decoration: underline; +} +a.contrast, +a.secondary { + --pico-text-decoration: underline; +} +small { + --pico-font-size: 0.875em; +} +h1, +h2, +h3, +h4, +h5, +h6 { + --pico-font-weight: 700; +} +h1 { + --pico-font-size: 2rem; + --pico-line-height: 1.125; + --pico-typography-spacing-top: 3rem; +} +h2 { + --pico-font-size: 1.75rem; + --pico-line-height: 1.15; + --pico-typography-spacing-top: 2.625rem; +} +h3 { + --pico-font-size: 1.5rem; + --pico-line-height: 1.175; + --pico-typography-spacing-top: 2.25rem; +} +h4 { + --pico-font-size: 1.25rem; + --pico-line-height: 1.2; + --pico-typography-spacing-top: 1.874rem; +} +h5 { + --pico-font-size: 1.125rem; + --pico-line-height: 1.225; + --pico-typography-spacing-top: 1.6875rem; +} +h6 { + --pico-font-size: 1rem; + --pico-line-height: 1.25; + --pico-typography-spacing-top: 1.5rem; +} +tfoot td, +tfoot th, +thead td, +thead th { + --pico-font-weight: 600; + --pico-border-width: 0.1875rem; +} +code, +kbd, +pre, +samp { + --pico-font-family: var(--pico-font-family-monospace); +} +kbd { + --pico-font-weight: bolder; +} +:where(select, textarea), +input:not([type="submit"], [type="button"], [type="reset"], [type="checkbox"], [type="radio"], [type="file"]) { + --pico-outline-width: 0.0625rem; +} +[type="search"] { + --pico-border-radius: 5rem; +} +[type="checkbox"], +[type="radio"] { + --pico-border-width: 0.125rem; +} +[type="checkbox"][role="switch"] { + --pico-border-width: 0.1875rem; +} +details.dropdown summary:not([role="button"]) { + --pico-outline-width: 0.0625rem; +} +nav details.dropdown summary:focus-visible { + --pico-outline-width: 0.125rem; +} +[role="search"] { + --pico-border-radius: 5rem; +} +[role="group"]:has( + button.secondary:focus, + [type="submit"].secondary:focus, + [type="button"].secondary:focus, + [role="button"].secondary:focus + ), +[role="search"]:has( + button.secondary:focus, + [type="submit"].secondary:focus, + [type="button"].secondary:focus, + [role="button"].secondary:focus + ) { + --pico-group-box-shadow-focus-with-button: 0 0 0 var(--pico-outline-width) var(--pico-secondary-focus); +} +[role="group"]:has( + button.contrast:focus, + [type="submit"].contrast:focus, + [type="button"].contrast:focus, + [role="button"].contrast:focus + ), +[role="search"]:has( + button.contrast:focus, + [type="submit"].contrast:focus, + [type="button"].contrast:focus, + [role="button"].contrast:focus + ) { + --pico-group-box-shadow-focus-with-button: 0 0 0 var(--pico-outline-width) var(--pico-contrast-focus); +} +[role="group"] [role="button"], +[role="group"] [type="button"], +[role="group"] [type="submit"], +[role="group"] button, +[role="search"] [role="button"], +[role="search"] [type="button"], +[role="search"] [type="submit"], +[role="search"] button { + --pico-form-element-spacing-horizontal: 2rem; +} +details summary[role="button"]:not(.outline)::after { + filter: brightness(0) invert(1); +} +[aria-busy="true"]:not(input, select, textarea):is( + button, + [type="submit"], + [type="button"], + [type="reset"], + [role="button"] + ):not(.outline)::before { + filter: brightness(0) invert(1); +} +:root:not([data-theme="dark"]), +[data-theme="light"] { + --pico-background-color: #fff; + --pico-color: #373c44; + --pico-text-selection-color: rgba(2, 154, 232, 0.25); + --pico-muted-color: #646b79; + --pico-muted-border-color: #e7eaf0; + --pico-primary: #0172ad; + --pico-primary-background: #0172ad; + --pico-primary-border: var(--pico-primary-background); + --pico-primary-underline: rgba(1, 114, 173, 0.5); + --pico-primary-hover: #015887; + --pico-primary-hover-background: #02659a; + --pico-primary-hover-border: var(--pico-primary-hover-background); + --pico-primary-hover-underline: var(--pico-primary-hover); + --pico-primary-focus: rgba(2, 154, 232, 0.5); + --pico-primary-inverse: #fff; + --pico-secondary: #5d6b89; + --pico-secondary-background: #525f7a; + --pico-secondary-border: var(--pico-secondary-background); + --pico-secondary-underline: rgba(93, 107, 137, 0.5); + --pico-secondary-hover: #48536b; + --pico-secondary-hover-background: #48536b; + --pico-secondary-hover-border: var(--pico-secondary-hover-background); + --pico-secondary-hover-underline: var(--pico-secondary-hover); + --pico-secondary-focus: rgba(93, 107, 137, 0.25); + --pico-secondary-inverse: #fff; + --pico-contrast: #181c25; + --pico-contrast-background: #181c25; + --pico-contrast-border: var(--pico-contrast-background); + --pico-contrast-underline: rgba(24, 28, 37, 0.5); + --pico-contrast-hover: #000; + --pico-contrast-hover-background: #000; + --pico-contrast-hover-border: var(--pico-contrast-hover-background); + --pico-contrast-hover-underline: var(--pico-secondary-hover); + --pico-contrast-focus: rgba(93, 107, 137, 0.25); + --pico-contrast-inverse: #fff; + --pico-box-shadow: 0.0145rem 0.029rem 0.174rem rgba(129, 145, 181, 0.01698), + 0.0335rem 0.067rem 0.402rem rgba(129, 145, 181, 0.024), 0.0625rem 0.125rem 0.75rem rgba(129, 145, 181, 0.03), + 0.1125rem 0.225rem 1.35rem rgba(129, 145, 181, 0.036), 0.2085rem 0.417rem 2.502rem rgba(129, 145, 181, 0.04302), + 0.5rem 1rem 6rem rgba(129, 145, 181, 0.06), 0 0 0 0.0625rem rgba(129, 145, 181, 0.015); + --pico-h1-color: #2d3138; + --pico-h2-color: #373c44; + --pico-h3-color: #424751; + --pico-h4-color: #4d535e; + --pico-h5-color: #5c6370; + --pico-h6-color: #646b79; + --pico-mark-background-color: #fde7c0; + --pico-mark-color: #0f1114; + --pico-ins-color: #1d6a54; + --pico-del-color: #883935; + --pico-blockquote-border-color: var(--pico-muted-border-color); + --pico-blockquote-footer-color: var(--pico-muted-color); + --pico-button-box-shadow: 0 0 0 rgba(0, 0, 0, 0); + --pico-button-hover-box-shadow: 0 0 0 rgba(0, 0, 0, 0); + --pico-table-border-color: var(--pico-muted-border-color); + --pico-table-row-stripped-background-color: rgba(111, 120, 135, 0.0375); + --pico-code-background-color: #f3f5f7; + --pico-code-color: #646b79; + --pico-code-kbd-background-color: var(--pico-color); + --pico-code-kbd-color: var(--pico-background-color); + --pico-form-element-background-color: #fbfcfc; + --pico-form-element-selected-background-color: #dfe3eb; + --pico-form-element-border-color: #cfd5e2; + --pico-form-element-color: #23262c; + --pico-form-element-placeholder-color: var(--pico-muted-color); + --pico-form-element-active-background-color: #fff; + --pico-form-element-active-border-color: var(--pico-primary-border); + --pico-form-element-focus-color: var(--pico-primary-border); + --pico-form-element-disabled-opacity: 0.5; + --pico-form-element-invalid-border-color: #b86a6b; + --pico-form-element-invalid-active-border-color: #c84f48; + --pico-form-element-invalid-focus-color: var(--pico-form-element-invalid-active-border-color); + --pico-form-element-valid-border-color: #4c9b8a; + --pico-form-element-valid-active-border-color: #279977; + --pico-form-element-valid-focus-color: var(--pico-form-element-valid-active-border-color); + --pico-switch-background-color: #bfc7d9; + --pico-switch-checked-background-color: var(--pico-primary-background); + --pico-switch-color: #fff; + --pico-switch-thumb-box-shadow: 0 0 0 rgba(0, 0, 0, 0); + --pico-range-border-color: #dfe3eb; + --pico-range-active-border-color: #bfc7d9; + --pico-range-thumb-border-color: var(--pico-background-color); + --pico-range-thumb-color: var(--pico-secondary-background); + --pico-range-thumb-active-color: var(--pico-primary-background); + --pico-accordion-border-color: var(--pico-muted-border-color); + --pico-accordion-active-summary-color: var(--pico-primary-hover); + --pico-accordion-close-summary-color: var(--pico-color); + --pico-accordion-open-summary-color: var(--pico-muted-color); + --pico-card-background-color: var(--pico-background-color); + --pico-card-border-color: var(--pico-muted-border-color); + --pico-card-box-shadow: var(--pico-box-shadow); + --pico-card-sectioning-background-color: #fbfcfc; + --pico-dropdown-background-color: #fff; + --pico-dropdown-border-color: #eff1f4; + --pico-dropdown-box-shadow: var(--pico-box-shadow); + --pico-dropdown-color: var(--pico-color); + --pico-dropdown-hover-background-color: #eff1f4; + --pico-loading-spinner-opacity: 0.5; + --pico-modal-overlay-background-color: rgba(232, 234, 237, 0.75); + --pico-progress-background-color: #dfe3eb; + --pico-progress-color: var(--pico-primary-background); + --pico-tooltip-background-color: var(--pico-contrast-background); + --pico-tooltip-color: var(--pico-contrast-inverse); + --pico-icon-valid: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(76, 155, 138)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='20 6 9 17 4 12'%3E%3C/polyline%3E%3C/svg%3E"); + --pico-icon-invalid: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(200, 79, 72)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'%3E%3C/circle%3E%3Cline x1='12' y1='8' x2='12' y2='12'%3E%3C/line%3E%3Cline x1='12' y1='16' x2='12.01' y2='16'%3E%3C/line%3E%3C/svg%3E"); + color-scheme: light; +} +:root:not([data-theme="dark"]) + input:is([type="submit"], [type="button"], [type="reset"], [type="checkbox"], [type="radio"], [type="file"]), +[data-theme="light"] + input:is([type="submit"], [type="button"], [type="reset"], [type="checkbox"], [type="radio"], [type="file"]) { + --pico-form-element-focus-color: var(--pico-primary-focus); +} +@media only screen and (prefers-color-scheme: dark) { + :root:not([data-theme]) { + --pico-background-color: #13171f; + --pico-color: #c2c7d0; + --pico-text-selection-color: rgba(1, 170, 255, 0.1875); + --pico-muted-color: #7b8495; + --pico-muted-border-color: #202632; + --pico-primary: #01aaff; + --pico-primary-background: #0172ad; + --pico-primary-border: var(--pico-primary-background); + --pico-primary-underline: rgba(1, 170, 255, 0.5); + --pico-primary-hover: #79c0ff; + --pico-primary-hover-background: #017fc0; + --pico-primary-hover-border: var(--pico-primary-hover-background); + --pico-primary-hover-underline: var(--pico-primary-hover); + --pico-primary-focus: rgba(1, 170, 255, 0.375); + --pico-primary-inverse: #fff; + --pico-secondary: #969eaf; + --pico-secondary-background: #525f7a; + --pico-secondary-border: var(--pico-secondary-background); + --pico-secondary-underline: rgba(150, 158, 175, 0.5); + --pico-secondary-hover: #b3b9c5; + --pico-secondary-hover-background: #5d6b89; + --pico-secondary-hover-border: var(--pico-secondary-hover-background); + --pico-secondary-hover-underline: var(--pico-secondary-hover); + --pico-secondary-focus: rgba(144, 158, 190, 0.25); + --pico-secondary-inverse: #fff; + --pico-contrast: #dfe3eb; + --pico-contrast-background: #eff1f4; + --pico-contrast-border: var(--pico-contrast-background); + --pico-contrast-underline: rgba(223, 227, 235, 0.5); + --pico-contrast-hover: #fff; + --pico-contrast-hover-background: #fff; + --pico-contrast-hover-border: var(--pico-contrast-hover-background); + --pico-contrast-hover-underline: var(--pico-contrast-hover); + --pico-contrast-focus: rgba(207, 213, 226, 0.25); + --pico-contrast-inverse: #000; + --pico-box-shadow: 0.0145rem 0.029rem 0.174rem rgba(7, 9, 12, 0.01698), + 0.0335rem 0.067rem 0.402rem rgba(7, 9, 12, 0.024), 0.0625rem 0.125rem 0.75rem rgba(7, 9, 12, 0.03), + 0.1125rem 0.225rem 1.35rem rgba(7, 9, 12, 0.036), 0.2085rem 0.417rem 2.502rem rgba(7, 9, 12, 0.04302), + 0.5rem 1rem 6rem rgba(7, 9, 12, 0.06), 0 0 0 0.0625rem rgba(7, 9, 12, 0.015); + --pico-h1-color: #f0f1f3; + --pico-h2-color: #e0e3e7; + --pico-h3-color: #c2c7d0; + --pico-h4-color: #b3b9c5; + --pico-h5-color: #a4acba; + --pico-h6-color: #8891a4; + --pico-mark-background-color: #014063; + --pico-mark-color: #fff; + --pico-ins-color: #62af9a; + --pico-del-color: #ce7e7b; + --pico-blockquote-border-color: var(--pico-muted-border-color); + --pico-blockquote-footer-color: var(--pico-muted-color); + --pico-button-box-shadow: 0 0 0 rgba(0, 0, 0, 0); + --pico-button-hover-box-shadow: 0 0 0 rgba(0, 0, 0, 0); + --pico-table-border-color: var(--pico-muted-border-color); + --pico-table-row-stripped-background-color: rgba(111, 120, 135, 0.0375); + --pico-code-background-color: #1a1f28; + --pico-code-color: #8891a4; + --pico-code-kbd-background-color: var(--pico-color); + --pico-code-kbd-color: var(--pico-background-color); + --pico-form-element-background-color: #1c212c; + --pico-form-element-selected-background-color: #2a3140; + --pico-form-element-border-color: #2a3140; + --pico-form-element-color: #e0e3e7; + --pico-form-element-placeholder-color: #8891a4; + --pico-form-element-active-background-color: #1a1f28; + --pico-form-element-active-border-color: var(--pico-primary-border); + --pico-form-element-focus-color: var(--pico-primary-border); + --pico-form-element-disabled-opacity: 0.5; + --pico-form-element-invalid-border-color: #964a50; + --pico-form-element-invalid-active-border-color: #b7403b; + --pico-form-element-invalid-focus-color: var(--pico-form-element-invalid-active-border-color); + --pico-form-element-valid-border-color: #2a7b6f; + --pico-form-element-valid-active-border-color: #16896a; + --pico-form-element-valid-focus-color: var(--pico-form-element-valid-active-border-color); + --pico-switch-background-color: #333c4e; + --pico-switch-checked-background-color: var(--pico-primary-background); + --pico-switch-color: #fff; + --pico-switch-thumb-box-shadow: 0 0 0 rgba(0, 0, 0, 0); + --pico-range-border-color: #202632; + --pico-range-active-border-color: #2a3140; + --pico-range-thumb-border-color: var(--pico-background-color); + --pico-range-thumb-color: var(--pico-secondary-background); + --pico-range-thumb-active-color: var(--pico-primary-background); + --pico-accordion-border-color: var(--pico-muted-border-color); + --pico-accordion-active-summary-color: var(--pico-primary-hover); + --pico-accordion-close-summary-color: var(--pico-color); + --pico-accordion-open-summary-color: var(--pico-muted-color); + --pico-card-background-color: #181c25; + --pico-card-border-color: var(--pico-card-background-color); + --pico-card-box-shadow: var(--pico-box-shadow); + --pico-card-sectioning-background-color: #1a1f28; + --pico-dropdown-background-color: #181c25; + --pico-dropdown-border-color: #202632; + --pico-dropdown-box-shadow: var(--pico-box-shadow); + --pico-dropdown-color: var(--pico-color); + --pico-dropdown-hover-background-color: #202632; + --pico-loading-spinner-opacity: 0.5; + --pico-modal-overlay-background-color: rgba(8, 9, 10, 0.75); + --pico-progress-background-color: #202632; + --pico-progress-color: var(--pico-primary-background); + --pico-tooltip-background-color: var(--pico-contrast-background); + --pico-tooltip-color: var(--pico-contrast-inverse); + --pico-icon-valid: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(42, 123, 111)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='20 6 9 17 4 12'%3E%3C/polyline%3E%3C/svg%3E"); + --pico-icon-invalid: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(150, 74, 80)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'%3E%3C/circle%3E%3Cline x1='12' y1='8' x2='12' y2='12'%3E%3C/line%3E%3Cline x1='12' y1='16' x2='12.01' y2='16'%3E%3C/line%3E%3C/svg%3E"); + color-scheme: dark; + } + :root:not([data-theme]) + input:is([type="submit"], [type="button"], [type="reset"], [type="checkbox"], [type="radio"], [type="file"]) { + --pico-form-element-focus-color: var(--pico-primary-focus); + } + :root:not([data-theme]) details summary[role="button"].contrast:not(.outline)::after { + filter: brightness(0); + } + :root:not([data-theme]) + [aria-busy="true"]:not(input, select, textarea).contrast:is( + button, + [type="submit"], + [type="button"], + [type="reset"], + [role="button"] + ):not(.outline)::before { + filter: brightness(0); + } +} +[data-theme="dark"] { + --pico-background-color: #13171f; + --pico-color: #c2c7d0; + --pico-text-selection-color: rgba(1, 170, 255, 0.1875); + --pico-muted-color: #7b8495; + --pico-muted-border-color: #202632; + --pico-primary: #01aaff; + --pico-primary-background: #0172ad; + --pico-primary-border: var(--pico-primary-background); + --pico-primary-underline: rgba(1, 170, 255, 0.5); + --pico-primary-hover: #79c0ff; + --pico-primary-hover-background: #017fc0; + --pico-primary-hover-border: var(--pico-primary-hover-background); + --pico-primary-hover-underline: var(--pico-primary-hover); + --pico-primary-focus: rgba(1, 170, 255, 0.375); + --pico-primary-inverse: #fff; + --pico-secondary: #969eaf; + --pico-secondary-background: #525f7a; + --pico-secondary-border: var(--pico-secondary-background); + --pico-secondary-underline: rgba(150, 158, 175, 0.5); + --pico-secondary-hover: #b3b9c5; + --pico-secondary-hover-background: #5d6b89; + --pico-secondary-hover-border: var(--pico-secondary-hover-background); + --pico-secondary-hover-underline: var(--pico-secondary-hover); + --pico-secondary-focus: rgba(144, 158, 190, 0.25); + --pico-secondary-inverse: #fff; + --pico-contrast: #dfe3eb; + --pico-contrast-background: #eff1f4; + --pico-contrast-border: var(--pico-contrast-background); + --pico-contrast-underline: rgba(223, 227, 235, 0.5); + --pico-contrast-hover: #fff; + --pico-contrast-hover-background: #fff; + --pico-contrast-hover-border: var(--pico-contrast-hover-background); + --pico-contrast-hover-underline: var(--pico-contrast-hover); + --pico-contrast-focus: rgba(207, 213, 226, 0.25); + --pico-contrast-inverse: #000; + --pico-box-shadow: 0.0145rem 0.029rem 0.174rem rgba(7, 9, 12, 0.01698), + 0.0335rem 0.067rem 0.402rem rgba(7, 9, 12, 0.024), 0.0625rem 0.125rem 0.75rem rgba(7, 9, 12, 0.03), + 0.1125rem 0.225rem 1.35rem rgba(7, 9, 12, 0.036), 0.2085rem 0.417rem 2.502rem rgba(7, 9, 12, 0.04302), + 0.5rem 1rem 6rem rgba(7, 9, 12, 0.06), 0 0 0 0.0625rem rgba(7, 9, 12, 0.015); + --pico-h1-color: #f0f1f3; + --pico-h2-color: #e0e3e7; + --pico-h3-color: #c2c7d0; + --pico-h4-color: #b3b9c5; + --pico-h5-color: #a4acba; + --pico-h6-color: #8891a4; + --pico-mark-background-color: #014063; + --pico-mark-color: #fff; + --pico-ins-color: #62af9a; + --pico-del-color: #ce7e7b; + --pico-blockquote-border-color: var(--pico-muted-border-color); + --pico-blockquote-footer-color: var(--pico-muted-color); + --pico-button-box-shadow: 0 0 0 rgba(0, 0, 0, 0); + --pico-button-hover-box-shadow: 0 0 0 rgba(0, 0, 0, 0); + --pico-table-border-color: var(--pico-muted-border-color); + --pico-table-row-stripped-background-color: rgba(111, 120, 135, 0.0375); + --pico-code-background-color: #1a1f28; + --pico-code-color: #8891a4; + --pico-code-kbd-background-color: var(--pico-color); + --pico-code-kbd-color: var(--pico-background-color); + --pico-form-element-background-color: #1c212c; + --pico-form-element-selected-background-color: #2a3140; + --pico-form-element-border-color: #2a3140; + --pico-form-element-color: #e0e3e7; + --pico-form-element-placeholder-color: #8891a4; + --pico-form-element-active-background-color: #1a1f28; + --pico-form-element-active-border-color: var(--pico-primary-border); + --pico-form-element-focus-color: var(--pico-primary-border); + --pico-form-element-disabled-opacity: 0.5; + --pico-form-element-invalid-border-color: #964a50; + --pico-form-element-invalid-active-border-color: #b7403b; + --pico-form-element-invalid-focus-color: var(--pico-form-element-invalid-active-border-color); + --pico-form-element-valid-border-color: #2a7b6f; + --pico-form-element-valid-active-border-color: #16896a; + --pico-form-element-valid-focus-color: var(--pico-form-element-valid-active-border-color); + --pico-switch-background-color: #333c4e; + --pico-switch-checked-background-color: var(--pico-primary-background); + --pico-switch-color: #fff; + --pico-switch-thumb-box-shadow: 0 0 0 rgba(0, 0, 0, 0); + --pico-range-border-color: #202632; + --pico-range-active-border-color: #2a3140; + --pico-range-thumb-border-color: var(--pico-background-color); + --pico-range-thumb-color: var(--pico-secondary-background); + --pico-range-thumb-active-color: var(--pico-primary-background); + --pico-accordion-border-color: var(--pico-muted-border-color); + --pico-accordion-active-summary-color: var(--pico-primary-hover); + --pico-accordion-close-summary-color: var(--pico-color); + --pico-accordion-open-summary-color: var(--pico-muted-color); + --pico-card-background-color: #181c25; + --pico-card-border-color: var(--pico-card-background-color); + --pico-card-box-shadow: var(--pico-box-shadow); + --pico-card-sectioning-background-color: #1a1f28; + --pico-dropdown-background-color: #181c25; + --pico-dropdown-border-color: #202632; + --pico-dropdown-box-shadow: var(--pico-box-shadow); + --pico-dropdown-color: var(--pico-color); + --pico-dropdown-hover-background-color: #202632; + --pico-loading-spinner-opacity: 0.5; + --pico-modal-overlay-background-color: rgba(8, 9, 10, 0.75); + --pico-progress-background-color: #202632; + --pico-progress-color: var(--pico-primary-background); + --pico-tooltip-background-color: var(--pico-contrast-background); + --pico-tooltip-color: var(--pico-contrast-inverse); + --pico-icon-valid: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(42, 123, 111)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='20 6 9 17 4 12'%3E%3C/polyline%3E%3C/svg%3E"); + --pico-icon-invalid: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(150, 74, 80)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'%3E%3C/circle%3E%3Cline x1='12' y1='8' x2='12' y2='12'%3E%3C/line%3E%3Cline x1='12' y1='16' x2='12.01' y2='16'%3E%3C/line%3E%3C/svg%3E"); + color-scheme: dark; +} +[data-theme="dark"] + input:is([type="submit"], [type="button"], [type="reset"], [type="checkbox"], [type="radio"], [type="file"]) { + --pico-form-element-focus-color: var(--pico-primary-focus); +} +[data-theme="dark"] details summary[role="button"].contrast:not(.outline)::after { + filter: brightness(0); +} +[data-theme="dark"] + [aria-busy="true"]:not(input, select, textarea).contrast:is( + button, + [type="submit"], + [type="button"], + [type="reset"], + [role="button"] + ):not(.outline)::before { + filter: brightness(0); +} +[type="checkbox"], +[type="radio"], +[type="range"], +progress { + accent-color: var(--pico-primary); +} +*, +::after, +::before { + box-sizing: border-box; + background-repeat: no-repeat; +} +::after, +::before { + text-decoration: inherit; + vertical-align: inherit; +} +:where(:root) { + -webkit-tap-highlight-color: transparent; + -webkit-text-size-adjust: 100%; + -moz-text-size-adjust: 100%; + text-size-adjust: 100%; + background-color: var(--pico-background-color); + color: var(--pico-color); + font-weight: var(--pico-font-weight); + font-size: var(--pico-font-size); + line-height: var(--pico-line-height); + font-family: var(--pico-font-family); + text-underline-offset: var(--pico-text-underline-offset); + text-rendering: optimizeLegibility; + overflow-wrap: break-word; + -moz-tab-size: 4; + -o-tab-size: 4; + tab-size: 4; +} +body { + width: 100%; + margin: 0; +} +main { + display: block; +} +body > footer, +body > header, +body > main { + padding-block: var(--pico-block-spacing-vertical); +} +section { + margin-bottom: var(--pico-block-spacing-vertical); +} +.container, +.container-fluid { + width: 100%; + margin-block: var(--pico-block-spacing-vertical); + margin-right: auto; + margin-left: auto; + padding-right: var(--pico-spacing); + padding-left: var(--pico-spacing); +} +@media (min-width: 576px) { + .container { + max-width: 510px; + padding-right: 0; + padding-left: 0; + } +} +@media (min-width: 768px) { + .container { + max-width: 700px; + } +} +@media (min-width: 1024px) { + .container { + max-width: 950px; + } +} +@media (min-width: 1280px) { + .container { + max-width: 1200px; + } +} +@media (min-width: 1536px) { + .container { + max-width: 1450px; + } +} +.grid { + grid-column-gap: var(--pico-grid-column-gap); + grid-row-gap: var(--pico-grid-row-gap); + display: grid; + grid-template-columns: 1fr; +} +@media (min-width: 768px) { + .grid { + grid-template-columns: repeat(auto-fit, minmax(0%, 1fr)); + } +} +.grid > * { + min-width: 0; +} +.overflow-auto { + overflow: auto; +} +b, +strong { + font-weight: bolder; +} +sub, +sup { + position: relative; + font-size: 0.75em; + line-height: 0; + vertical-align: baseline; +} +sub { + bottom: -0.25em; +} +sup { + top: -0.5em; +} +address, +blockquote, +dl, +ol, +p, +pre, +table, +ul { + margin-top: 0; + margin-bottom: var(--pico-typography-spacing-vertical); + color: var(--pico-color); + font-style: normal; + font-weight: var(--pico-font-weight); +} +h1, +h2, +h3, +h4, +h5, +h6 { + margin-top: 0; + margin-bottom: var(--pico-typography-spacing-vertical); + color: var(--pico-color); + font-weight: var(--pico-font-weight); + font-size: var(--pico-font-size); + line-height: var(--pico-line-height); + font-family: var(--pico-font-family); +} +h1 { + --pico-color: var(--pico-h1-color); +} +h2 { + --pico-color: var(--pico-h2-color); +} +h3 { + --pico-color: var(--pico-h3-color); +} +h4 { + --pico-color: var(--pico-h4-color); +} +h5 { + --pico-color: var(--pico-h5-color); +} +h6 { + --pico-color: var(--pico-h6-color); +} +:where(article, address, blockquote, dl, figure, form, ol, p, pre, table, ul) ~ :is(h1, h2, h3, h4, h5, h6) { + margin-top: var(--pico-typography-spacing-top); +} +p { + margin-bottom: var(--pico-typography-spacing-vertical); +} +hgroup { + margin-bottom: var(--pico-typography-spacing-vertical); +} +hgroup > * { + margin-top: 0; + margin-bottom: 0; +} +hgroup > :not(:first-child):last-child { + --pico-color: var(--pico-muted-color); + --pico-font-weight: unset; + font-size: 1rem; +} +:where(ol, ul) li { + margin-bottom: calc(var(--pico-typography-spacing-vertical) * 0.25); +} +:where(dl, ol, ul) :where(dl, ol, ul) { + margin: 0; + margin-top: calc(var(--pico-typography-spacing-vertical) * 0.25); +} +ul li { + list-style: square; +} +mark { + padding: 0.125rem 0.25rem; + background-color: var(--pico-mark-background-color); + color: var(--pico-mark-color); + vertical-align: baseline; +} +blockquote { + display: block; + margin: var(--pico-typography-spacing-vertical) 0; + padding: var(--pico-spacing); + border-right: none; + border-left: 0.25rem solid var(--pico-blockquote-border-color); + border-inline-start: 0.25rem solid var(--pico-blockquote-border-color); + border-inline-end: none; +} +blockquote footer { + margin-top: calc(var(--pico-typography-spacing-vertical) * 0.5); + color: var(--pico-blockquote-footer-color); +} +abbr[title] { + border-bottom: 1px dotted; + text-decoration: none; + cursor: help; +} +ins { + color: var(--pico-ins-color); + text-decoration: none; +} +del { + color: var(--pico-del-color); +} +::-moz-selection { + background-color: var(--pico-text-selection-color); +} +::selection { + background-color: var(--pico-text-selection-color); +} +:where(a:not([role="button"])), +[role="link"] { + --pico-color: var(--pico-primary); + --pico-background-color: transparent; + --pico-underline: var(--pico-primary-underline); + outline: 0; + background-color: var(--pico-background-color); + color: var(--pico-color); + -webkit-text-decoration: var(--pico-text-decoration); + text-decoration: var(--pico-text-decoration); + text-decoration-color: var(--pico-underline); + text-underline-offset: 0.125em; + transition: background-color var(--pico-transition), color var(--pico-transition), box-shadow var(--pico-transition), + -webkit-text-decoration var(--pico-transition); + transition: background-color var(--pico-transition), color var(--pico-transition), + text-decoration var(--pico-transition), box-shadow var(--pico-transition); + transition: background-color var(--pico-transition), color var(--pico-transition), + text-decoration var(--pico-transition), box-shadow var(--pico-transition), + -webkit-text-decoration var(--pico-transition); +} +:where(a:not([role="button"])):is([aria-current]:not([aria-current="false"]), :hover, :active, :focus), +[role="link"]:is([aria-current]:not([aria-current="false"]), :hover, :active, :focus) { + --pico-color: var(--pico-primary-hover); + --pico-underline: var(--pico-primary-hover-underline); + --pico-text-decoration: underline; +} +:where(a:not([role="button"])):focus-visible, +[role="link"]:focus-visible { + box-shadow: 0 0 0 var(--pico-outline-width) var(--pico-primary-focus); +} +:where(a:not([role="button"])).secondary, +[role="link"].secondary { + --pico-color: var(--pico-secondary); + --pico-underline: var(--pico-secondary-underline); +} +:where(a:not([role="button"])).secondary:is([aria-current]:not([aria-current="false"]), :hover, :active, :focus), +[role="link"].secondary:is([aria-current]:not([aria-current="false"]), :hover, :active, :focus) { + --pico-color: var(--pico-secondary-hover); + --pico-underline: var(--pico-secondary-hover-underline); +} +:where(a:not([role="button"])).contrast, +[role="link"].contrast { + --pico-color: var(--pico-contrast); + --pico-underline: var(--pico-contrast-underline); +} +:where(a:not([role="button"])).contrast:is([aria-current]:not([aria-current="false"]), :hover, :active, :focus), +[role="link"].contrast:is([aria-current]:not([aria-current="false"]), :hover, :active, :focus) { + --pico-color: var(--pico-contrast-hover); + --pico-underline: var(--pico-contrast-hover-underline); +} +a[role="button"] { + display: inline-block; +} +button { + margin: 0; + overflow: visible; + font-family: inherit; + text-transform: none; +} +[type="button"], +[type="reset"], +[type="submit"], +button { + -webkit-appearance: button; +} +[role="button"], +[type="button"], +[type="file"]::file-selector-button, +[type="reset"], +[type="submit"], +button { + --pico-background-color: var(--pico-primary-background); + --pico-border-color: var(--pico-primary-border); + --pico-color: var(--pico-primary-inverse); + --pico-box-shadow: var(--pico-button-box-shadow, 0 0 0 rgba(0, 0, 0, 0)); + padding: var(--pico-form-element-spacing-vertical) var(--pico-form-element-spacing-horizontal); + border: var(--pico-border-width) solid var(--pico-border-color); + border-radius: var(--pico-border-radius); + outline: 0; + background-color: var(--pico-background-color); + box-shadow: var(--pico-box-shadow); + color: var(--pico-color); + font-weight: var(--pico-font-weight); + font-size: 1rem; + line-height: var(--pico-line-height); + text-align: center; + text-decoration: none; + cursor: pointer; + -webkit-user-select: none; + -moz-user-select: none; + user-select: none; + transition: background-color var(--pico-transition), border-color var(--pico-transition), color var(--pico-transition), + box-shadow var(--pico-transition); +} +[role="button"]:is(:hover, :active, :focus), +[role="button"]:is([aria-current]:not([aria-current="false"])), +[type="button"]:is(:hover, :active, :focus), +[type="button"]:is([aria-current]:not([aria-current="false"])), +[type="file"]::file-selector-button:is(:hover, :active, :focus), +[type="file"]::file-selector-button:is([aria-current]:not([aria-current="false"])), +[type="reset"]:is(:hover, :active, :focus), +[type="reset"]:is([aria-current]:not([aria-current="false"])), +[type="submit"]:is(:hover, :active, :focus), +[type="submit"]:is([aria-current]:not([aria-current="false"])), +button:is(:hover, :active, :focus), +button:is([aria-current]:not([aria-current="false"])) { + --pico-background-color: var(--pico-primary-hover-background); + --pico-border-color: var(--pico-primary-hover-border); + --pico-box-shadow: var(--pico-button-hover-box-shadow, 0 0 0 rgba(0, 0, 0, 0)); + --pico-color: var(--pico-primary-inverse); +} +[role="button"]:focus, +[role="button"]:is([aria-current]:not([aria-current="false"])):focus, +[type="button"]:focus, +[type="button"]:is([aria-current]:not([aria-current="false"])):focus, +[type="file"]::file-selector-button:focus, +[type="file"]::file-selector-button:is([aria-current]:not([aria-current="false"])):focus, +[type="reset"]:focus, +[type="reset"]:is([aria-current]:not([aria-current="false"])):focus, +[type="submit"]:focus, +[type="submit"]:is([aria-current]:not([aria-current="false"])):focus, +button:focus, +button:is([aria-current]:not([aria-current="false"])):focus { + --pico-box-shadow: var(--pico-button-hover-box-shadow, 0 0 0 rgba(0, 0, 0, 0)), + 0 0 0 var(--pico-outline-width) var(--pico-primary-focus); +} +[type="button"], +[type="reset"], +[type="submit"] { + margin-bottom: var(--pico-spacing); +} +:is(button, [type="submit"], [type="button"], [role="button"]).secondary, +[type="file"]::file-selector-button, +[type="reset"] { + --pico-background-color: var(--pico-secondary-background); + --pico-border-color: var(--pico-secondary-border); + --pico-color: var(--pico-secondary-inverse); + cursor: pointer; +} +:is(button, [type="submit"], [type="button"], [role="button"]).secondary:is( + [aria-current]:not([aria-current="false"]), + :hover, + :active, + :focus + ), +[type="file"]::file-selector-button:is([aria-current]:not([aria-current="false"]), :hover, :active, :focus), +[type="reset"]:is([aria-current]:not([aria-current="false"]), :hover, :active, :focus) { + --pico-background-color: var(--pico-secondary-hover-background); + --pico-border-color: var(--pico-secondary-hover-border); + --pico-color: var(--pico-secondary-inverse); +} +:is(button, [type="submit"], [type="button"], [role="button"]).secondary:focus, +:is(button, [type="submit"], [type="button"], [role="button"]).secondary:is( + [aria-current]:not([aria-current="false"]) + ):focus, +[type="file"]::file-selector-button:focus, +[type="file"]::file-selector-button:is([aria-current]:not([aria-current="false"])):focus, +[type="reset"]:focus, +[type="reset"]:is([aria-current]:not([aria-current="false"])):focus { + --pico-box-shadow: var(--pico-button-hover-box-shadow, 0 0 0 rgba(0, 0, 0, 0)), + 0 0 0 var(--pico-outline-width) var(--pico-secondary-focus); +} +:is(button, [type="submit"], [type="button"], [role="button"]).contrast { + --pico-background-color: var(--pico-contrast-background); + --pico-border-color: var(--pico-contrast-border); + --pico-color: var(--pico-contrast-inverse); +} +:is(button, [type="submit"], [type="button"], [role="button"]).contrast:is( + [aria-current]:not([aria-current="false"]), + :hover, + :active, + :focus + ) { + --pico-background-color: var(--pico-contrast-hover-background); + --pico-border-color: var(--pico-contrast-hover-border); + --pico-color: var(--pico-contrast-inverse); +} +:is(button, [type="submit"], [type="button"], [role="button"]).contrast:focus, +:is(button, [type="submit"], [type="button"], [role="button"]).contrast:is( + [aria-current]:not([aria-current="false"]) + ):focus { + --pico-box-shadow: var(--pico-button-hover-box-shadow, 0 0 0 rgba(0, 0, 0, 0)), + 0 0 0 var(--pico-outline-width) var(--pico-contrast-focus); +} +:is(button, [type="submit"], [type="button"], [role="button"]).outline, +[type="reset"].outline { + --pico-background-color: transparent; + --pico-color: var(--pico-primary); + --pico-border-color: var(--pico-primary); +} +:is(button, [type="submit"], [type="button"], [role="button"]).outline:is( + [aria-current]:not([aria-current="false"]), + :hover, + :active, + :focus + ), +[type="reset"].outline:is([aria-current]:not([aria-current="false"]), :hover, :active, :focus) { + --pico-background-color: transparent; + --pico-color: var(--pico-primary-hover); + --pico-border-color: var(--pico-primary-hover); +} +:is(button, [type="submit"], [type="button"], [role="button"]).outline.secondary, +[type="reset"].outline { + --pico-color: var(--pico-secondary); + --pico-border-color: var(--pico-secondary); +} +:is(button, [type="submit"], [type="button"], [role="button"]).outline.secondary:is( + [aria-current]:not([aria-current="false"]), + :hover, + :active, + :focus + ), +[type="reset"].outline:is([aria-current]:not([aria-current="false"]), :hover, :active, :focus) { + --pico-color: var(--pico-secondary-hover); + --pico-border-color: var(--pico-secondary-hover); +} +:is(button, [type="submit"], [type="button"], [role="button"]).outline.contrast { + --pico-color: var(--pico-contrast); + --pico-border-color: var(--pico-contrast); +} +:is(button, [type="submit"], [type="button"], [role="button"]).outline.contrast:is( + [aria-current]:not([aria-current="false"]), + :hover, + :active, + :focus + ) { + --pico-color: var(--pico-contrast-hover); + --pico-border-color: var(--pico-contrast-hover); +} +:where(button, [type="submit"], [type="reset"], [type="button"], [role="button"])[disabled], +:where(fieldset[disabled]) :is(button, [type="submit"], [type="button"], [type="reset"], [role="button"]) { + opacity: 0.5; + pointer-events: none; +} +:where(table) { + width: 100%; + border-collapse: collapse; + border-spacing: 0; + text-indent: 0; +} +td, +th { + padding: calc(var(--pico-spacing) / 2) var(--pico-spacing); + border-bottom: var(--pico-border-width) solid var(--pico-table-border-color); + background-color: var(--pico-background-color); + color: var(--pico-color); + font-weight: var(--pico-font-weight); + text-align: left; + text-align: start; +} +tfoot td, +tfoot th { + border-top: var(--pico-border-width) solid var(--pico-table-border-color); + border-bottom: 0; +} +table.striped tbody tr:nth-child(odd) td, +table.striped tbody tr:nth-child(odd) th { + background-color: var(--pico-table-row-stripped-background-color); +} +:where(audio, canvas, iframe, img, svg, video) { + vertical-align: middle; +} +audio, +video { + display: inline-block; +} +audio:not([controls]) { + display: none; + height: 0; +} +:where(iframe) { + border-style: none; +} +img { + max-width: 100%; + height: auto; + border-style: none; +} +:where(svg:not([fill])) { + fill: currentColor; +} +svg:not(:root) { + overflow: hidden; +} +code, +kbd, +pre, +samp { + font-size: 0.875em; + font-family: var(--pico-font-family); +} +pre code { + font-size: inherit; + font-family: inherit; +} +pre { + -ms-overflow-style: scrollbar; + overflow: auto; +} +code, +kbd, +pre { + border-radius: var(--pico-border-radius); + background: var(--pico-code-background-color); + color: var(--pico-code-color); + font-weight: var(--pico-font-weight); + line-height: initial; +} +code, +kbd { + display: inline-block; + padding: 0.375rem; +} +pre { + display: block; + margin-bottom: var(--pico-spacing); + overflow-x: auto; +} +pre > code { + display: block; + padding: var(--pico-spacing); + background: 0 0; + line-height: var(--pico-line-height); +} +kbd { + background-color: var(--pico-code-kbd-background-color); + color: var(--pico-code-kbd-color); + vertical-align: baseline; +} +figure { + display: block; + margin: 0; + padding: 0; +} +figure figcaption { + padding: calc(var(--pico-spacing) * 0.5) 0; + color: var(--pico-muted-color); +} +hr { + height: 0; + margin: var(--pico-typography-spacing-vertical) 0; + border: 0; + border-top: 1px solid var(--pico-muted-border-color); + color: inherit; +} +[hidden], +template { + display: none !important; +} +canvas { + display: inline-block; +} +input, +optgroup, +select, +textarea { + margin: 0; + font-size: 1rem; + line-height: var(--pico-line-height); + font-family: inherit; + letter-spacing: inherit; +} +input { + overflow: visible; +} +select { + text-transform: none; +} +legend { + max-width: 100%; + padding: 0; + color: inherit; + white-space: normal; +} +textarea { + overflow: auto; +} +[type="checkbox"], +[type="radio"] { + padding: 0; +} +::-webkit-inner-spin-button, +::-webkit-outer-spin-button { + height: auto; +} +[type="search"] { + -webkit-appearance: textfield; + outline-offset: -2px; +} +[type="search"]::-webkit-search-decoration { + -webkit-appearance: none; +} +::-webkit-file-upload-button { + -webkit-appearance: button; + font: inherit; +} +::-moz-focus-inner { + padding: 0; + border-style: none; +} +:-moz-focusring { + outline: 0; +} +:-moz-ui-invalid { + box-shadow: none; +} +::-ms-expand { + display: none; +} +[type="file"], +[type="range"] { + padding: 0; + border-width: 0; +} +input:not([type="checkbox"], [type="radio"], [type="range"]) { + height: calc( + 1rem * var(--pico-line-height) + var(--pico-form-element-spacing-vertical) * 2 + var(--pico-border-width) * 2 + ); +} +fieldset { + width: 100%; + margin: 0; + margin-bottom: var(--pico-spacing); + padding: 0; + border: 0; +} +fieldset legend, +label { + display: block; + margin-bottom: calc(var(--pico-spacing) * 0.375); + color: var(--pico-color); + font-weight: var(--pico-form-label-font-weight, var(--pico-font-weight)); +} +fieldset legend { + margin-bottom: calc(var(--pico-spacing) * 0.5); +} +button[type="submit"], +input:not([type="checkbox"], [type="radio"]), +select, +textarea { + width: 100%; +} +input:not([type="checkbox"], [type="radio"], [type="range"], [type="file"]), +select, +textarea { + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + padding: var(--pico-form-element-spacing-vertical) var(--pico-form-element-spacing-horizontal); +} +input, +select, +textarea { + --pico-background-color: var(--pico-form-element-background-color); + --pico-border-color: var(--pico-form-element-border-color); + --pico-color: var(--pico-form-element-color); + --pico-box-shadow: none; + border: var(--pico-border-width) solid var(--pico-border-color); + border-radius: var(--pico-border-radius); + outline: 0; + background-color: var(--pico-background-color); + box-shadow: var(--pico-box-shadow); + color: var(--pico-color); + font-weight: var(--pico-font-weight); + transition: background-color var(--pico-transition), border-color var(--pico-transition), color var(--pico-transition), + box-shadow var(--pico-transition); +} +:where(select, textarea):not([readonly]):is(:active, :focus), +input:not([type="submit"], [type="button"], [type="reset"], [type="checkbox"], [type="radio"], [readonly]):is( + :active, + :focus + ) { + --pico-background-color: var(--pico-form-element-active-background-color); +} +:where(select, textarea):not([readonly]):is(:active, :focus), +input:not([type="submit"], [type="button"], [type="reset"], [role="switch"], [readonly]):is(:active, :focus) { + --pico-border-color: var(--pico-form-element-active-border-color); +} +:where(select, textarea):not([readonly]):focus, +input:not([type="submit"], [type="button"], [type="reset"], [type="range"], [type="file"], [readonly]):focus { + --pico-box-shadow: 0 0 0 var(--pico-outline-width) var(--pico-form-element-focus-color); +} +:where(fieldset[disabled]) :is(input:not([type="submit"], [type="button"], [type="reset"]), select, textarea), +input:not([type="submit"], [type="button"], [type="reset"])[disabled], +label[aria-disabled="true"], +select[disabled], +textarea[disabled] { + opacity: var(--pico-form-element-disabled-opacity); + pointer-events: none; +} +label[aria-disabled="true"] input[disabled] { + opacity: 1; +} +:where(input, select, textarea):not( + [type="checkbox"], + [type="radio"], + [type="date"], + [type="datetime-local"], + [type="month"], + [type="time"], + [type="week"], + [type="range"] + )[aria-invalid] { + padding-right: calc(var(--pico-form-element-spacing-horizontal) + 1.5rem) !important; + padding-left: var(--pico-form-element-spacing-horizontal); + padding-inline-start: var(--pico-form-element-spacing-horizontal) !important; + padding-inline-end: calc(var(--pico-form-element-spacing-horizontal) + 1.5rem) !important; + background-position: center right 0.75rem; + background-size: 1rem auto; + background-repeat: no-repeat; +} +:where(input, select, textarea):not( + [type="checkbox"], + [type="radio"], + [type="date"], + [type="datetime-local"], + [type="month"], + [type="time"], + [type="week"], + [type="range"] + )[aria-invalid="false"]:not(select) { + background-image: var(--pico-icon-valid); +} +:where(input, select, textarea):not( + [type="checkbox"], + [type="radio"], + [type="date"], + [type="datetime-local"], + [type="month"], + [type="time"], + [type="week"], + [type="range"] + )[aria-invalid="true"]:not(select) { + background-image: var(--pico-icon-invalid); +} +:where(input, select, textarea)[aria-invalid="false"] { + --pico-border-color: var(--pico-form-element-valid-border-color); +} +:where(input, select, textarea)[aria-invalid="false"]:is(:active, :focus) { + --pico-border-color: var(--pico-form-element-valid-active-border-color) !important; +} +:where(input, select, textarea)[aria-invalid="false"]:is(:active, :focus):not([type="checkbox"], [type="radio"]) { + --pico-box-shadow: 0 0 0 var(--pico-outline-width) var(--pico-form-element-valid-focus-color) !important; +} +:where(input, select, textarea)[aria-invalid="true"] { + --pico-border-color: var(--pico-form-element-invalid-border-color); +} +:where(input, select, textarea)[aria-invalid="true"]:is(:active, :focus) { + --pico-border-color: var(--pico-form-element-invalid-active-border-color) !important; +} +:where(input, select, textarea)[aria-invalid="true"]:is(:active, :focus):not([type="checkbox"], [type="radio"]) { + --pico-box-shadow: 0 0 0 var(--pico-outline-width) var(--pico-form-element-invalid-focus-color) !important; +} +[dir="rtl"] + :where(input, select, textarea):not([type="checkbox"], [type="radio"]):is( + [aria-invalid], + [aria-invalid="true"], + [aria-invalid="false"] + ) { + background-position: center left 0.75rem; +} +input::-webkit-input-placeholder, +input::placeholder, +select:invalid, +textarea::-webkit-input-placeholder, +textarea::placeholder { + color: var(--pico-form-element-placeholder-color); + opacity: 1; +} +input:not([type="checkbox"], [type="radio"]), +select, +textarea { + margin-bottom: var(--pico-spacing); +} +select::-ms-expand { + border: 0; + background-color: transparent; +} +select:not([multiple], [size]) { + padding-right: calc(var(--pico-form-element-spacing-horizontal) + 1.5rem); + padding-left: var(--pico-form-element-spacing-horizontal); + padding-inline-start: var(--pico-form-element-spacing-horizontal); + padding-inline-end: calc(var(--pico-form-element-spacing-horizontal) + 1.5rem); + background-image: var(--pico-icon-chevron); + background-position: center right 0.75rem; + background-size: 1rem auto; + background-repeat: no-repeat; +} +select[multiple] option:checked { + background: var(--pico-form-element-selected-background-color); + color: var(--pico-form-element-color); +} +[dir="rtl"] select:not([multiple], [size]) { + background-position: center left 0.75rem; +} +textarea { + display: block; + resize: vertical; +} +textarea[aria-invalid] { + --pico-icon-height: calc( + 1rem * var(--pico-line-height) + var(--pico-form-element-spacing-vertical) * 2 + var(--pico-border-width) * 2 + ); + background-position: top right 0.75rem !important; + background-size: 1rem var(--pico-icon-height) !important; +} +:where(input, select, textarea, fieldset, .grid) + small { + display: block; + width: 100%; + margin-top: calc(var(--pico-spacing) * -0.75); + margin-bottom: var(--pico-spacing); + color: var(--pico-muted-color); +} +:where(input, select, textarea, fieldset, .grid)[aria-invalid="false"] + small { + color: var(--pico-ins-color); +} +:where(input, select, textarea, fieldset, .grid)[aria-invalid="true"] + small { + color: var(--pico-del-color); +} +label > :where(input, select, textarea) { + margin-top: calc(var(--pico-spacing) * 0.25); +} +label:has([type="checkbox"], [type="radio"]) { + width: -moz-fit-content; + width: fit-content; + cursor: pointer; +} +[type="checkbox"], +[type="radio"] { + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + width: 1.25em; + height: 1.25em; + margin-top: -0.125em; + margin-inline-end: 0.5em; + border-width: var(--pico-border-width); + vertical-align: middle; + cursor: pointer; +} +[type="checkbox"]::-ms-check, +[type="radio"]::-ms-check { + display: none; +} +[type="checkbox"]:checked, +[type="checkbox"]:checked:active, +[type="checkbox"]:checked:focus, +[type="radio"]:checked, +[type="radio"]:checked:active, +[type="radio"]:checked:focus { + --pico-background-color: var(--pico-primary-background); + --pico-border-color: var(--pico-primary-border); + background-image: var(--pico-icon-checkbox); + background-position: center; + background-size: 0.75em auto; + background-repeat: no-repeat; +} +[type="checkbox"] ~ label, +[type="radio"] ~ label { + display: inline-block; + margin-bottom: 0; + cursor: pointer; +} +[type="checkbox"] ~ label:not(:last-of-type), +[type="radio"] ~ label:not(:last-of-type) { + margin-inline-end: 1em; +} +[type="checkbox"]:indeterminate { + --pico-background-color: var(--pico-primary-background); + --pico-border-color: var(--pico-primary-border); + background-image: var(--pico-icon-minus); + background-position: center; + background-size: 0.75em auto; + background-repeat: no-repeat; +} +[type="radio"] { + border-radius: 50%; +} +[type="radio"]:checked, +[type="radio"]:checked:active, +[type="radio"]:checked:focus { + --pico-background-color: var(--pico-primary-inverse); + border-width: 0.35em; + background-image: none; +} +[type="checkbox"][role="switch"] { + --pico-background-color: var(--pico-switch-background-color); + --pico-color: var(--pico-switch-color); + width: 2.25em; + height: 1.25em; + border: var(--pico-border-width) solid var(--pico-border-color); + border-radius: 1.25em; + background-color: var(--pico-background-color); + line-height: 1.25em; +} +[type="checkbox"][role="switch"]:not([aria-invalid]) { + --pico-border-color: var(--pico-switch-background-color); +} +[type="checkbox"][role="switch"]:before { + display: block; + aspect-ratio: 1; + height: 100%; + border-radius: 50%; + background-color: var(--pico-color); + box-shadow: var(--pico-switch-thumb-box-shadow); + content: ""; + transition: margin 0.1s ease-in-out; +} +[type="checkbox"][role="switch"]:focus { + --pico-background-color: var(--pico-switch-background-color); + --pico-border-color: var(--pico-switch-background-color); +} +[type="checkbox"][role="switch"]:checked { + --pico-background-color: var(--pico-switch-checked-background-color); + --pico-border-color: var(--pico-switch-checked-background-color); + background-image: none; +} +[type="checkbox"][role="switch"]:checked::before { + margin-inline-start: calc(2.25em - 1.25em); +} +[type="checkbox"][role="switch"][disabled] { + --pico-background-color: var(--pico-border-color); +} +[type="checkbox"][aria-invalid="false"]:checked, +[type="checkbox"][aria-invalid="false"]:checked:active, +[type="checkbox"][aria-invalid="false"]:checked:focus, +[type="checkbox"][role="switch"][aria-invalid="false"]:checked, +[type="checkbox"][role="switch"][aria-invalid="false"]:checked:active, +[type="checkbox"][role="switch"][aria-invalid="false"]:checked:focus { + --pico-background-color: var(--pico-form-element-valid-border-color); +} +[type="checkbox"]:checked:active[aria-invalid="true"], +[type="checkbox"]:checked:focus[aria-invalid="true"], +[type="checkbox"]:checked[aria-invalid="true"], +[type="checkbox"][role="switch"]:checked:active[aria-invalid="true"], +[type="checkbox"][role="switch"]:checked:focus[aria-invalid="true"], +[type="checkbox"][role="switch"]:checked[aria-invalid="true"] { + --pico-background-color: var(--pico-form-element-invalid-border-color); +} +[type="checkbox"][aria-invalid="false"]:checked, +[type="checkbox"][aria-invalid="false"]:checked:active, +[type="checkbox"][aria-invalid="false"]:checked:focus, +[type="checkbox"][role="switch"][aria-invalid="false"]:checked, +[type="checkbox"][role="switch"][aria-invalid="false"]:checked:active, +[type="checkbox"][role="switch"][aria-invalid="false"]:checked:focus, +[type="radio"][aria-invalid="false"]:checked, +[type="radio"][aria-invalid="false"]:checked:active, +[type="radio"][aria-invalid="false"]:checked:focus { + --pico-border-color: var(--pico-form-element-valid-border-color); +} +[type="checkbox"]:checked:active[aria-invalid="true"], +[type="checkbox"]:checked:focus[aria-invalid="true"], +[type="checkbox"]:checked[aria-invalid="true"], +[type="checkbox"][role="switch"]:checked:active[aria-invalid="true"], +[type="checkbox"][role="switch"]:checked:focus[aria-invalid="true"], +[type="checkbox"][role="switch"]:checked[aria-invalid="true"], +[type="radio"]:checked:active[aria-invalid="true"], +[type="radio"]:checked:focus[aria-invalid="true"], +[type="radio"]:checked[aria-invalid="true"] { + --pico-border-color: var(--pico-form-element-invalid-border-color); +} +[type="color"]::-webkit-color-swatch-wrapper { + padding: 0; +} +[type="color"]::-moz-focus-inner { + padding: 0; +} +[type="color"]::-webkit-color-swatch { + border: 0; + border-radius: calc(var(--pico-border-radius) * 0.5); +} +[type="color"]::-moz-color-swatch { + border: 0; + border-radius: calc(var(--pico-border-radius) * 0.5); +} +input:not([type="checkbox"], [type="radio"], [type="range"], [type="file"]):is( + [type="date"], + [type="datetime-local"], + [type="month"], + [type="time"], + [type="week"] + ) { + --pico-icon-position: 0.75rem; + --pico-icon-width: 1rem; + padding-right: calc(var(--pico-icon-width) + var(--pico-icon-position)); + background-image: var(--pico-icon-date); + background-position: center right var(--pico-icon-position); + background-size: var(--pico-icon-width) auto; + background-repeat: no-repeat; +} +input:not([type="checkbox"], [type="radio"], [type="range"], [type="file"])[type="time"] { + background-image: var(--pico-icon-time); +} +[type="date"]::-webkit-calendar-picker-indicator, +[type="datetime-local"]::-webkit-calendar-picker-indicator, +[type="month"]::-webkit-calendar-picker-indicator, +[type="time"]::-webkit-calendar-picker-indicator, +[type="week"]::-webkit-calendar-picker-indicator { + width: var(--pico-icon-width); + margin-right: calc(var(--pico-icon-width) * -1); + margin-left: var(--pico-icon-position); + opacity: 0; +} +@-moz-document url-prefix() { + [type="date"], + [type="datetime-local"], + [type="month"], + [type="time"], + [type="week"] { + padding-right: var(--pico-form-element-spacing-horizontal) !important; + background-image: none !important; + } +} +[dir="rtl"] :is([type="date"], [type="datetime-local"], [type="month"], [type="time"], [type="week"]) { + text-align: right; +} +[type="file"] { + --pico-color: var(--pico-muted-color); + margin-left: calc(var(--pico-outline-width) * -1); + padding: calc(var(--pico-form-element-spacing-vertical) * 0.5) 0; + padding-left: var(--pico-outline-width); + border: 0; + border-radius: 0; + background: 0 0; +} +[type="file"]::file-selector-button { + margin-right: calc(var(--pico-spacing) / 2); + padding: calc(var(--pico-form-element-spacing-vertical) * 0.5) var(--pico-form-element-spacing-horizontal); +} +[type="file"]:is(:hover, :active, :focus)::file-selector-button { + --pico-background-color: var(--pico-secondary-hover-background); + --pico-border-color: var(--pico-secondary-hover-border); +} +[type="file"]:focus::file-selector-button { + --pico-box-shadow: var(--pico-button-hover-box-shadow, 0 0 0 rgba(0, 0, 0, 0)), + 0 0 0 var(--pico-outline-width) var(--pico-secondary-focus); +} +[type="range"] { + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + width: 100%; + height: 1.25rem; + background: 0 0; +} +[type="range"]::-webkit-slider-runnable-track { + width: 100%; + height: 0.375rem; + border-radius: var(--pico-border-radius); + background-color: var(--pico-range-border-color); + -webkit-transition: background-color var(--pico-transition), box-shadow var(--pico-transition); + transition: background-color var(--pico-transition), box-shadow var(--pico-transition); +} +[type="range"]::-moz-range-track { + width: 100%; + height: 0.375rem; + border-radius: var(--pico-border-radius); + background-color: var(--pico-range-border-color); + -moz-transition: background-color var(--pico-transition), box-shadow var(--pico-transition); + transition: background-color var(--pico-transition), box-shadow var(--pico-transition); +} +[type="range"]::-ms-track { + width: 100%; + height: 0.375rem; + border-radius: var(--pico-border-radius); + background-color: var(--pico-range-border-color); + -ms-transition: background-color var(--pico-transition), box-shadow var(--pico-transition); + transition: background-color var(--pico-transition), box-shadow var(--pico-transition); +} +[type="range"]::-webkit-slider-thumb { + -webkit-appearance: none; + width: 1.25rem; + height: 1.25rem; + margin-top: -0.4375rem; + border: 2px solid var(--pico-range-thumb-border-color); + border-radius: 50%; + background-color: var(--pico-range-thumb-color); + cursor: pointer; + -webkit-transition: background-color var(--pico-transition), transform var(--pico-transition); + transition: background-color var(--pico-transition), transform var(--pico-transition); +} +[type="range"]::-moz-range-thumb { + -webkit-appearance: none; + width: 1.25rem; + height: 1.25rem; + margin-top: -0.4375rem; + border: 2px solid var(--pico-range-thumb-border-color); + border-radius: 50%; + background-color: var(--pico-range-thumb-color); + cursor: pointer; + -moz-transition: background-color var(--pico-transition), transform var(--pico-transition); + transition: background-color var(--pico-transition), transform var(--pico-transition); +} +[type="range"]::-ms-thumb { + -webkit-appearance: none; + width: 1.25rem; + height: 1.25rem; + margin-top: -0.4375rem; + border: 2px solid var(--pico-range-thumb-border-color); + border-radius: 50%; + background-color: var(--pico-range-thumb-color); + cursor: pointer; + -ms-transition: background-color var(--pico-transition), transform var(--pico-transition); + transition: background-color var(--pico-transition), transform var(--pico-transition); +} +[type="range"]:active, +[type="range"]:focus-within { + --pico-range-border-color: var(--pico-range-active-border-color); + --pico-range-thumb-color: var(--pico-range-thumb-active-color); +} +[type="range"]:active::-webkit-slider-thumb { + transform: scale(1.25); +} +[type="range"]:active::-moz-range-thumb { + transform: scale(1.25); +} +[type="range"]:active::-ms-thumb { + transform: scale(1.25); +} +input:not([type="checkbox"], [type="radio"], [type="range"], [type="file"])[type="search"] { + padding-inline-start: calc(var(--pico-form-element-spacing-horizontal) + 1.75rem); + background-image: var(--pico-icon-search); + background-position: center left calc(var(--pico-form-element-spacing-horizontal) + 0.125rem); + background-size: 1rem auto; + background-repeat: no-repeat; +} +input:not([type="checkbox"], [type="radio"], [type="range"], [type="file"])[type="search"][aria-invalid] { + padding-inline-start: calc(var(--pico-form-element-spacing-horizontal) + 1.75rem) !important; + background-position: center left 1.125rem, center right 0.75rem; +} +input:not([type="checkbox"], [type="radio"], [type="range"], [type="file"])[type="search"][aria-invalid="false"] { + background-image: var(--pico-icon-search), var(--pico-icon-valid); +} +input:not([type="checkbox"], [type="radio"], [type="range"], [type="file"])[type="search"][aria-invalid="true"] { + background-image: var(--pico-icon-search), var(--pico-icon-invalid); +} +[dir="rtl"] :where(input):not([type="checkbox"], [type="radio"], [type="range"], [type="file"])[type="search"] { + background-position: center right 1.125rem; +} +[dir="rtl"] + :where(input):not([type="checkbox"], [type="radio"], [type="range"], [type="file"])[type="search"][aria-invalid] { + background-position: center right 1.125rem, center left 0.75rem; +} +details { + display: block; + margin-bottom: var(--pico-spacing); +} +details summary { + line-height: 1rem; + list-style-type: none; + cursor: pointer; + transition: color var(--pico-transition); +} +details summary:not([role]) { + color: var(--pico-accordion-close-summary-color); +} +details summary::-webkit-details-marker { + display: none; +} +details summary::marker { + display: none; +} +details summary::-moz-list-bullet { + list-style-type: none; +} +details summary::after { + display: block; + width: 1rem; + height: 1rem; + margin-inline-start: calc(var(--pico-spacing, 1rem) * 0.5); + float: right; + transform: rotate(-90deg); + background-image: var(--pico-icon-chevron); + background-position: right center; + background-size: 1rem auto; + background-repeat: no-repeat; + content: ""; + transition: transform var(--pico-transition); +} +details summary:focus { + outline: 0; +} +details summary:focus:not([role]) { + color: var(--pico-accordion-active-summary-color); +} +details summary:focus-visible:not([role]) { + outline: var(--pico-outline-width) solid var(--pico-primary-focus); + outline-offset: calc(var(--pico-spacing, 1rem) * 0.5); + color: var(--pico-primary); +} +details summary[role="button"] { + width: 100%; + text-align: left; +} +details summary[role="button"]::after { + height: calc(1rem * var(--pico-line-height, 1.5)); +} +details[open] > summary { + margin-bottom: var(--pico-spacing); +} +details[open] > summary:not([role]):not(:focus) { + color: var(--pico-accordion-open-summary-color); +} +details[open] > summary::after { + transform: rotate(0); +} +[dir="rtl"] details summary { + text-align: right; +} +[dir="rtl"] details summary::after { + float: left; + background-position: left center; +} +article { + margin-bottom: var(--pico-block-spacing-vertical); + padding: var(--pico-block-spacing-vertical) var(--pico-block-spacing-horizontal); + border-radius: var(--pico-border-radius); + background: var(--pico-card-background-color); + box-shadow: var(--pico-card-box-shadow); +} +article > footer, +article > header { + margin-right: calc(var(--pico-block-spacing-horizontal) * -1); + margin-left: calc(var(--pico-block-spacing-horizontal) * -1); + padding: calc(var(--pico-block-spacing-vertical) * 0.66) var(--pico-block-spacing-horizontal); + background-color: var(--pico-card-sectioning-background-color); +} +article > header { + margin-top: calc(var(--pico-block-spacing-vertical) * -1); + margin-bottom: var(--pico-block-spacing-vertical); + border-bottom: var(--pico-border-width) solid var(--pico-card-border-color); + border-top-right-radius: var(--pico-border-radius); + border-top-left-radius: var(--pico-border-radius); +} +article > footer { + margin-top: var(--pico-block-spacing-vertical); + margin-bottom: calc(var(--pico-block-spacing-vertical) * -1); + border-top: var(--pico-border-width) solid var(--pico-card-border-color); + border-bottom-right-radius: var(--pico-border-radius); + border-bottom-left-radius: var(--pico-border-radius); +} +details.dropdown { + position: relative; + border-bottom: none; +} +details.dropdown summary::after, +details.dropdown > a::after, +details.dropdown > button::after { + display: block; + width: 1rem; + height: calc(1rem * var(--pico-line-height, 1.5)); + margin-inline-start: 0.25rem; + float: right; + transform: rotate(0) translateX(0.2rem); + background-image: var(--pico-icon-chevron); + background-position: right center; + background-size: 1rem auto; + background-repeat: no-repeat; + content: ""; +} +nav details.dropdown { + margin-bottom: 0; +} +details.dropdown summary:not([role]) { + height: calc( + 1rem * var(--pico-line-height) + var(--pico-form-element-spacing-vertical) * 2 + var(--pico-border-width) * 2 + ); + padding: var(--pico-form-element-spacing-vertical) var(--pico-form-element-spacing-horizontal); + border: var(--pico-border-width) solid var(--pico-form-element-border-color); + border-radius: var(--pico-border-radius); + background-color: var(--pico-form-element-background-color); + color: var(--pico-form-element-placeholder-color); + line-height: inherit; + cursor: pointer; + -webkit-user-select: none; + -moz-user-select: none; + user-select: none; + transition: background-color var(--pico-transition), border-color var(--pico-transition), color var(--pico-transition), + box-shadow var(--pico-transition); +} +details.dropdown summary:not([role]):active, +details.dropdown summary:not([role]):focus { + border-color: var(--pico-form-element-active-border-color); + background-color: var(--pico-form-element-active-background-color); +} +details.dropdown summary:not([role]):focus { + box-shadow: 0 0 0 var(--pico-outline-width) var(--pico-form-element-focus-color); +} +details.dropdown summary:not([role]):focus-visible { + outline: 0; +} +details.dropdown summary:not([role])[aria-invalid="false"] { + --pico-form-element-border-color: var(--pico-form-element-valid-border-color); + --pico-form-element-active-border-color: var(--pico-form-element-valid-focus-color); + --pico-form-element-focus-color: var(--pico-form-element-valid-focus-color); +} +details.dropdown summary:not([role])[aria-invalid="true"] { + --pico-form-element-border-color: var(--pico-form-element-invalid-border-color); + --pico-form-element-active-border-color: var(--pico-form-element-invalid-focus-color); + --pico-form-element-focus-color: var(--pico-form-element-invalid-focus-color); +} +nav details.dropdown { + display: inline; + margin: calc(var(--pico-nav-element-spacing-vertical) * -1) 0; +} +nav details.dropdown summary::after { + transform: rotate(0) translateX(0); +} +nav details.dropdown summary:not([role]) { + height: calc(1rem * var(--pico-line-height) + var(--pico-nav-link-spacing-vertical) * 2); + padding: calc(var(--pico-nav-link-spacing-vertical) - var(--pico-border-width) * 2) + var(--pico-nav-link-spacing-horizontal); +} +nav details.dropdown summary:not([role]):focus-visible { + box-shadow: 0 0 0 var(--pico-outline-width) var(--pico-primary-focus); +} +details.dropdown summary + ul { + display: flex; + z-index: 99; + position: absolute; + left: 0; + flex-direction: column; + width: 100%; + min-width: -moz-fit-content; + min-width: fit-content; + margin: 0; + margin-top: var(--pico-outline-width); + padding: 0; + border: var(--pico-border-width) solid var(--pico-dropdown-border-color); + border-radius: var(--pico-border-radius); + background-color: var(--pico-dropdown-background-color); + box-shadow: var(--pico-dropdown-box-shadow); + color: var(--pico-dropdown-color); + white-space: nowrap; + opacity: 0; + transition: opacity var(--pico-transition), transform 0s ease-in-out 1s; +} +details.dropdown summary + ul[dir="rtl"] { + right: 0; + left: auto; +} +details.dropdown summary + ul li { + width: 100%; + margin-bottom: 0; + padding: calc(var(--pico-form-element-spacing-vertical) * 0.5) var(--pico-form-element-spacing-horizontal); + list-style: none; +} +details.dropdown summary + ul li:first-of-type { + margin-top: calc(var(--pico-form-element-spacing-vertical) * 0.5); +} +details.dropdown summary + ul li:last-of-type { + margin-bottom: calc(var(--pico-form-element-spacing-vertical) * 0.5); +} +details.dropdown summary + ul li a { + display: block; + margin: calc(var(--pico-form-element-spacing-vertical) * -0.5) calc(var(--pico-form-element-spacing-horizontal) * -1); + padding: calc(var(--pico-form-element-spacing-vertical) * 0.5) var(--pico-form-element-spacing-horizontal); + overflow: hidden; + border-radius: 0; + color: var(--pico-dropdown-color); + text-decoration: none; + text-overflow: ellipsis; +} +details.dropdown summary + ul li a:active, +details.dropdown summary + ul li a:focus, +details.dropdown summary + ul li a:focus-visible, +details.dropdown summary + ul li a:hover, +details.dropdown summary + ul li a[aria-current]:not([aria-current="false"]) { + background-color: var(--pico-dropdown-hover-background-color); +} +details.dropdown summary + ul li label { + width: 100%; +} +details.dropdown summary + ul li:has(label):hover { + background-color: var(--pico-dropdown-hover-background-color); +} +details.dropdown[open] summary { + margin-bottom: 0; +} +details.dropdown[open] summary + ul { + transform: scaleY(1); + opacity: 1; + transition: opacity var(--pico-transition), transform 0s ease-in-out 0s; +} +details.dropdown[open] summary::before { + display: block; + z-index: 1; + position: fixed; + width: 100vw; + height: 100vh; + inset: 0; + background: 0 0; + content: ""; + cursor: default; +} +label > details.dropdown { + margin-top: calc(var(--pico-spacing) * 0.25); +} +[role="group"], +[role="search"] { + display: inline-flex; + position: relative; + width: 100%; + margin-bottom: var(--pico-spacing); + border-radius: var(--pico-border-radius); + box-shadow: var(--pico-group-box-shadow, 0 0 0 transparent); + vertical-align: middle; + transition: box-shadow var(--pico-transition); +} +[role="group"] input:not([type="checkbox"], [type="radio"]), +[role="group"] select, +[role="group"] > *, +[role="search"] input:not([type="checkbox"], [type="radio"]), +[role="search"] select, +[role="search"] > * { + position: relative; + flex: 1 1 auto; + margin-bottom: 0; +} +[role="group"] input:not([type="checkbox"], [type="radio"]):not(:first-child), +[role="group"] select:not(:first-child), +[role="group"] > :not(:first-child), +[role="search"] input:not([type="checkbox"], [type="radio"]):not(:first-child), +[role="search"] select:not(:first-child), +[role="search"] > :not(:first-child) { + margin-left: 0; + border-top-left-radius: 0; + border-bottom-left-radius: 0; +} +[role="group"] input:not([type="checkbox"], [type="radio"]):not(:last-child), +[role="group"] select:not(:last-child), +[role="group"] > :not(:last-child), +[role="search"] input:not([type="checkbox"], [type="radio"]):not(:last-child), +[role="search"] select:not(:last-child), +[role="search"] > :not(:last-child) { + border-top-right-radius: 0; + border-bottom-right-radius: 0; +} +[role="group"] input:not([type="checkbox"], [type="radio"]):focus, +[role="group"] select:focus, +[role="group"] > :focus, +[role="search"] input:not([type="checkbox"], [type="radio"]):focus, +[role="search"] select:focus, +[role="search"] > :focus { + z-index: 2; +} +[role="group"] [role="button"]:not(:first-child), +[role="group"] [type="button"]:not(:first-child), +[role="group"] [type="reset"]:not(:first-child), +[role="group"] [type="submit"]:not(:first-child), +[role="group"] button:not(:first-child), +[role="group"] input:not([type="checkbox"], [type="radio"]):not(:first-child), +[role="group"] select:not(:first-child), +[role="search"] [role="button"]:not(:first-child), +[role="search"] [type="button"]:not(:first-child), +[role="search"] [type="reset"]:not(:first-child), +[role="search"] [type="submit"]:not(:first-child), +[role="search"] button:not(:first-child), +[role="search"] input:not([type="checkbox"], [type="radio"]):not(:first-child), +[role="search"] select:not(:first-child) { + margin-left: calc(var(--pico-border-width) * -1); +} +[role="group"] [role="button"], +[role="group"] [type="button"], +[role="group"] [type="reset"], +[role="group"] [type="submit"], +[role="group"] button, +[role="search"] [role="button"], +[role="search"] [type="button"], +[role="search"] [type="reset"], +[role="search"] [type="submit"], +[role="search"] button { + width: auto; +} +@supports selector(:has(*)) { + [role="group"]:has(button:focus, [type="submit"]:focus, [type="button"]:focus, [role="button"]:focus), + [role="search"]:has(button:focus, [type="submit"]:focus, [type="button"]:focus, [role="button"]:focus) { + --pico-group-box-shadow: var(--pico-group-box-shadow-focus-with-button); + } + [role="group"]:has(button:focus, [type="submit"]:focus, [type="button"]:focus, [role="button"]:focus) + input:not([type="checkbox"], [type="radio"]), + [role="group"]:has(button:focus, [type="submit"]:focus, [type="button"]:focus, [role="button"]:focus) select, + [role="search"]:has(button:focus, [type="submit"]:focus, [type="button"]:focus, [role="button"]:focus) + input:not([type="checkbox"], [type="radio"]), + [role="search"]:has(button:focus, [type="submit"]:focus, [type="button"]:focus, [role="button"]:focus) select { + border-color: transparent; + } + [role="group"]:has(input:not([type="submit"], [type="button"]):focus, select:focus), + [role="search"]:has(input:not([type="submit"], [type="button"]):focus, select:focus) { + --pico-group-box-shadow: var(--pico-group-box-shadow-focus-with-input); + } + [role="group"]:has(input:not([type="submit"], [type="button"]):focus, select:focus) [role="button"], + [role="group"]:has(input:not([type="submit"], [type="button"]):focus, select:focus) [type="button"], + [role="group"]:has(input:not([type="submit"], [type="button"]):focus, select:focus) [type="submit"], + [role="group"]:has(input:not([type="submit"], [type="button"]):focus, select:focus) button, + [role="search"]:has(input:not([type="submit"], [type="button"]):focus, select:focus) [role="button"], + [role="search"]:has(input:not([type="submit"], [type="button"]):focus, select:focus) [type="button"], + [role="search"]:has(input:not([type="submit"], [type="button"]):focus, select:focus) [type="submit"], + [role="search"]:has(input:not([type="submit"], [type="button"]):focus, select:focus) button { + --pico-button-box-shadow: 0 0 0 var(--pico-border-width) var(--pico-primary-border); + --pico-button-hover-box-shadow: 0 0 0 var(--pico-border-width) var(--pico-primary-hover-border); + } + [role="group"] [role="button"]:focus, + [role="group"] [type="button"]:focus, + [role="group"] [type="reset"]:focus, + [role="group"] [type="submit"]:focus, + [role="group"] button:focus, + [role="search"] [role="button"]:focus, + [role="search"] [type="button"]:focus, + [role="search"] [type="reset"]:focus, + [role="search"] [type="submit"]:focus, + [role="search"] button:focus { + box-shadow: none; + } +} +[role="search"] > :first-child { + border-top-left-radius: 5rem; + border-bottom-left-radius: 5rem; +} +[role="search"] > :last-child { + border-top-right-radius: 5rem; + border-bottom-right-radius: 5rem; +} +[aria-busy="true"]:not(input, select, textarea, html) { + white-space: nowrap; +} +[aria-busy="true"]:not(input, select, textarea, html)::before { + display: inline-block; + width: 1em; + height: 1em; + background-image: var(--pico-icon-loading); + background-size: 1em auto; + background-repeat: no-repeat; + content: ""; + vertical-align: -0.125em; +} +[aria-busy="true"]:not(input, select, textarea, html):not(:empty)::before { + margin-inline-end: calc(var(--pico-spacing) * 0.5); +} +[aria-busy="true"]:not(input, select, textarea, html):empty { + text-align: center; +} +[role="button"][aria-busy="true"], +[type="button"][aria-busy="true"], +[type="reset"][aria-busy="true"], +[type="submit"][aria-busy="true"], +a[aria-busy="true"], +button[aria-busy="true"] { + pointer-events: none; +} +:root { + --pico-scrollbar-width: 0px; +} +dialog { + display: flex; + z-index: 999; + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: 0; + align-items: center; + justify-content: center; + width: inherit; + min-width: 100%; + height: inherit; + min-height: 100%; + padding: 0; + border: 0; + -webkit-backdrop-filter: var(--pico-modal-overlay-backdrop-filter); + backdrop-filter: var(--pico-modal-overlay-backdrop-filter); + background-color: var(--pico-modal-overlay-background-color); + color: var(--pico-color); +} +dialog article { + width: 100%; + max-height: calc(100vh - var(--pico-spacing) * 2); + margin: var(--pico-spacing); + overflow: auto; +} +@media (min-width: 576px) { + dialog article { + max-width: 510px; + } +} +@media (min-width: 768px) { + dialog article { + max-width: 700px; + } +} +dialog article > header > * { + margin-bottom: 0; +} +dialog article > header .close, +dialog article > header :is(a, button)[rel="prev"] { + margin: 0; + margin-left: var(--pico-spacing); + padding: 0; + float: right; +} +dialog article > footer { + text-align: right; +} +dialog article > footer [role="button"], +dialog article > footer button { + margin-bottom: 0; +} +dialog article > footer [role="button"]:not(:first-of-type), +dialog article > footer button:not(:first-of-type) { + margin-left: calc(var(--pico-spacing) * 0.5); +} +dialog article .close, +dialog article :is(a, button)[rel="prev"] { + display: block; + width: 1rem; + height: 1rem; + margin-top: calc(var(--pico-spacing) * -1); + margin-bottom: var(--pico-spacing); + margin-left: auto; + border: none; + background-image: var(--pico-icon-close); + background-position: center; + background-size: auto 1rem; + background-repeat: no-repeat; + background-color: transparent; + opacity: 0.5; + transition: opacity var(--pico-transition); +} +dialog article .close:is([aria-current]:not([aria-current="false"]), :hover, :active, :focus), +dialog article :is(a, button)[rel="prev"]:is([aria-current]:not([aria-current="false"]), :hover, :active, :focus) { + opacity: 1; +} +dialog:not([open]), +dialog[open="false"] { + display: none; +} +.modal-is-open { + padding-right: var(--pico-scrollbar-width, 0); + overflow: hidden; + pointer-events: none; + touch-action: none; +} +.modal-is-open dialog { + pointer-events: auto; + touch-action: auto; +} +:where(.modal-is-opening, .modal-is-closing) dialog, +:where(.modal-is-opening, .modal-is-closing) dialog > article { + animation-duration: 0.2s; + animation-timing-function: ease-in-out; + animation-fill-mode: both; +} +:where(.modal-is-opening, .modal-is-closing) dialog { + animation-duration: 0.8s; + animation-name: modal-overlay; +} +:where(.modal-is-opening, .modal-is-closing) dialog > article { + animation-delay: 0.2s; + animation-name: modal; +} +.modal-is-closing dialog, +.modal-is-closing dialog > article { + animation-delay: 0s; + animation-direction: reverse; +} +@keyframes modal-overlay { + from { + -webkit-backdrop-filter: none; + backdrop-filter: none; + background-color: transparent; + } +} +@keyframes modal { + from { + transform: translateY(-100%); + opacity: 0; + } +} +:where(nav li)::before { + float: left; + content: "​"; +} +nav, +nav ul { + display: flex; +} +nav { + justify-content: space-between; + overflow: visible; +} +nav ol, +nav ul { + align-items: center; + margin-bottom: 0; + padding: 0; + list-style: none; +} +nav ol:first-of-type, +nav ul:first-of-type { + margin-left: calc(var(--pico-nav-element-spacing-horizontal) * -1); +} +nav ol:last-of-type, +nav ul:last-of-type { + margin-right: calc(var(--pico-nav-element-spacing-horizontal) * -1); +} +nav li { + display: inline-block; + margin: 0; + padding: var(--pico-nav-element-spacing-vertical) var(--pico-nav-element-spacing-horizontal); +} +nav li :where(a, [role="link"]) { + display: inline-block; + margin: calc(var(--pico-nav-link-spacing-vertical) * -1) calc(var(--pico-nav-link-spacing-horizontal) * -1); + padding: var(--pico-nav-link-spacing-vertical) var(--pico-nav-link-spacing-horizontal); + border-radius: var(--pico-border-radius); +} +nav li :where(a, [role="link"]):not(:hover) { + text-decoration: none; +} +nav li [role="button"], +nav li [type="button"], +nav li button, +nav li input:not([type="checkbox"], [type="radio"], [type="range"], [type="file"]), +nav li select { + height: auto; + margin-right: inherit; + margin-bottom: 0; + margin-left: inherit; + padding: calc(var(--pico-nav-link-spacing-vertical) - var(--pico-border-width) * 2) + var(--pico-nav-link-spacing-horizontal); +} +nav[aria-label="breadcrumb"] { + align-items: center; + justify-content: start; +} +nav[aria-label="breadcrumb"] ul li:not(:first-child) { + margin-inline-start: var(--pico-nav-link-spacing-horizontal); +} +nav[aria-label="breadcrumb"] ul li a { + margin: calc(var(--pico-nav-link-spacing-vertical) * -1) 0; + margin-inline-start: calc(var(--pico-nav-link-spacing-horizontal) * -1); +} +nav[aria-label="breadcrumb"] ul li:not(:last-child)::after { + display: inline-block; + position: absolute; + width: calc(var(--pico-nav-link-spacing-horizontal) * 4); + margin: 0 calc(var(--pico-nav-link-spacing-horizontal) * -1); + content: var(--pico-nav-breadcrumb-divider); + color: var(--pico-muted-color); + text-align: center; + text-decoration: none; + white-space: nowrap; +} +nav[aria-label="breadcrumb"] a[aria-current]:not([aria-current="false"]) { + background-color: transparent; + color: inherit; + text-decoration: none; + pointer-events: none; +} +aside li, +aside nav, +aside ol, +aside ul { + display: block; +} +aside li { + padding: calc(var(--pico-nav-element-spacing-vertical) * 0.5) var(--pico-nav-element-spacing-horizontal); +} +aside li a { + display: block; +} +aside li [role="button"] { + margin: inherit; +} +[dir="rtl"] nav[aria-label="breadcrumb"] ul li:not(:last-child) ::after { + content: "\\"; +} +progress { + display: inline-block; + vertical-align: baseline; +} +progress { + -webkit-appearance: none; + -moz-appearance: none; + display: inline-block; + appearance: none; + width: 100%; + height: 0.5rem; + margin-bottom: calc(var(--pico-spacing) * 0.5); + overflow: hidden; + border: 0; + border-radius: var(--pico-border-radius); + background-color: var(--pico-progress-background-color); + color: var(--pico-progress-color); +} +progress::-webkit-progress-bar { + border-radius: var(--pico-border-radius); + background: 0 0; +} +progress[value]::-webkit-progress-value { + background-color: var(--pico-progress-color); + -webkit-transition: inline-size var(--pico-transition); + transition: inline-size var(--pico-transition); +} +progress::-moz-progress-bar { + background-color: var(--pico-progress-color); +} +@media (prefers-reduced-motion: no-preference) { + progress:indeterminate { + background: var(--pico-progress-background-color) + linear-gradient(to right, var(--pico-progress-color) 30%, var(--pico-progress-background-color) 30%) top left/150% + 150% no-repeat; + animation: progress-indeterminate 1s linear infinite; + } + progress:indeterminate[value]::-webkit-progress-value { + background-color: transparent; + } + progress:indeterminate::-moz-progress-bar { + background-color: transparent; + } +} +@media (prefers-reduced-motion: no-preference) { + [dir="rtl"] progress:indeterminate { + animation-direction: reverse; + } +} +@keyframes progress-indeterminate { + 0% { + background-position: 200% 0; + } + 100% { + background-position: -200% 0; + } +} +[data-tooltip] { + position: relative; +} +[data-tooltip]:not(a, button, input) { + border-bottom: 1px dotted; + text-decoration: none; + cursor: help; +} +[data-tooltip]::after, +[data-tooltip]::before, +[data-tooltip][data-placement="top"]::after, +[data-tooltip][data-placement="top"]::before { + display: block; + z-index: 99; + position: absolute; + bottom: 100%; + left: 50%; + padding: 0.25rem 0.5rem; + overflow: hidden; + transform: translate(-50%, -0.25rem); + border-radius: var(--pico-border-radius); + background: var(--pico-tooltip-background-color); + content: attr(data-tooltip); + color: var(--pico-tooltip-color); + font-style: normal; + font-weight: var(--pico-font-weight); + font-size: 0.875rem; + text-decoration: none; + text-overflow: ellipsis; + white-space: nowrap; + opacity: 0; + pointer-events: none; +} +[data-tooltip]::after, +[data-tooltip][data-placement="top"]::after { + padding: 0; + transform: translate(-50%, 0); + border-top: 0.3rem solid; + border-right: 0.3rem solid transparent; + border-left: 0.3rem solid transparent; + border-radius: 0; + background-color: transparent; + content: ""; + color: var(--pico-tooltip-background-color); +} +[data-tooltip][data-placement="bottom"]::after, +[data-tooltip][data-placement="bottom"]::before { + top: 100%; + bottom: auto; + transform: translate(-50%, 0.25rem); +} +[data-tooltip][data-placement="bottom"]:after { + transform: translate(-50%, -0.3rem); + border: 0.3rem solid transparent; + border-bottom: 0.3rem solid; +} +[data-tooltip][data-placement="left"]::after, +[data-tooltip][data-placement="left"]::before { + top: 50%; + right: 100%; + bottom: auto; + left: auto; + transform: translate(-0.25rem, -50%); +} +[data-tooltip][data-placement="left"]:after { + transform: translate(0.3rem, -50%); + border: 0.3rem solid transparent; + border-left: 0.3rem solid; +} +[data-tooltip][data-placement="right"]::after, +[data-tooltip][data-placement="right"]::before { + top: 50%; + right: auto; + bottom: auto; + left: 100%; + transform: translate(0.25rem, -50%); +} +[data-tooltip][data-placement="right"]:after { + transform: translate(-0.3rem, -50%); + border: 0.3rem solid transparent; + border-right: 0.3rem solid; +} +[data-tooltip]:focus::after, +[data-tooltip]:focus::before, +[data-tooltip]:hover::after, +[data-tooltip]:hover::before { + opacity: 1; +} +@media (hover: hover) and (pointer: fine) { + [data-tooltip]:focus::after, + [data-tooltip]:focus::before, + [data-tooltip]:hover::after, + [data-tooltip]:hover::before { + --pico-tooltip-slide-to: translate(-50%, -0.25rem); + transform: translate(-50%, 0.75rem); + animation-duration: 0.2s; + animation-fill-mode: forwards; + animation-name: tooltip-slide; + opacity: 0; + } + [data-tooltip]:focus::after, + [data-tooltip]:hover::after { + --pico-tooltip-caret-slide-to: translate(-50%, 0rem); + transform: translate(-50%, -0.25rem); + animation-name: tooltip-caret-slide; + } + [data-tooltip][data-placement="bottom"]:focus::after, + [data-tooltip][data-placement="bottom"]:focus::before, + [data-tooltip][data-placement="bottom"]:hover::after, + [data-tooltip][data-placement="bottom"]:hover::before { + --pico-tooltip-slide-to: translate(-50%, 0.25rem); + transform: translate(-50%, -0.75rem); + animation-name: tooltip-slide; + } + [data-tooltip][data-placement="bottom"]:focus::after, + [data-tooltip][data-placement="bottom"]:hover::after { + --pico-tooltip-caret-slide-to: translate(-50%, -0.3rem); + transform: translate(-50%, -0.5rem); + animation-name: tooltip-caret-slide; + } + [data-tooltip][data-placement="left"]:focus::after, + [data-tooltip][data-placement="left"]:focus::before, + [data-tooltip][data-placement="left"]:hover::after, + [data-tooltip][data-placement="left"]:hover::before { + --pico-tooltip-slide-to: translate(-0.25rem, -50%); + transform: translate(0.75rem, -50%); + animation-name: tooltip-slide; + } + [data-tooltip][data-placement="left"]:focus::after, + [data-tooltip][data-placement="left"]:hover::after { + --pico-tooltip-caret-slide-to: translate(0.3rem, -50%); + transform: translate(0.05rem, -50%); + animation-name: tooltip-caret-slide; + } + [data-tooltip][data-placement="right"]:focus::after, + [data-tooltip][data-placement="right"]:focus::before, + [data-tooltip][data-placement="right"]:hover::after, + [data-tooltip][data-placement="right"]:hover::before { + --pico-tooltip-slide-to: translate(0.25rem, -50%); + transform: translate(-0.75rem, -50%); + animation-name: tooltip-slide; + } + [data-tooltip][data-placement="right"]:focus::after, + [data-tooltip][data-placement="right"]:hover::after { + --pico-tooltip-caret-slide-to: translate(-0.3rem, -50%); + transform: translate(-0.05rem, -50%); + animation-name: tooltip-caret-slide; + } +} +@keyframes tooltip-slide { + to { + transform: var(--pico-tooltip-slide-to); + opacity: 1; + } +} +@keyframes tooltip-caret-slide { + 50% { + opacity: 0; + } + to { + transform: var(--pico-tooltip-caret-slide-to); + opacity: 1; + } +} +[aria-controls] { + cursor: pointer; +} +[aria-disabled="true"], +[disabled] { + cursor: not-allowed; +} +[aria-hidden="false"][hidden] { + display: initial; +} +[aria-hidden="false"][hidden]:not(:focus) { + clip: rect(0, 0, 0, 0); + position: absolute; +} +[tabindex], +a, +area, +button, +input, +label, +select, +summary, +textarea { + -ms-touch-action: manipulation; +} +[dir="rtl"] { + direction: rtl; +} +@media (prefers-reduced-motion: reduce) { + :not([aria-busy="true"]), + :not([aria-busy="true"])::after, + :not([aria-busy="true"])::before { + background-attachment: initial !important; + animation-duration: 1ms !important; + animation-delay: -1ms !important; + animation-iteration-count: 1 !important; + scroll-behavior: auto !important; + transition-delay: 0s !important; + transition-duration: 0s !important; + } +} + +/* -- Custom -- */ + +.muted { + color: var(--pico-muted-color); +} + +nav > form > input[type="submit"], +.grid > input { + margin-bottom: 0; +} + +button.link, +input.link[type="submit"] { + background-color: transparent; + border: none; + color: var(--pico-link-color); + cursor: pointer; + text-decoration: underline; + padding: 0; + margin: 0; + height: auto; +} + +td { + max-width: 20rem; +} + +td p, +td a { + display: block; + margin: 0; + width: 100%; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; +} + +h1 a { + text-decoration: none; + color: inherit; +} diff --git a/packages/blink/example.config.json b/packages/blink/example.config.json new file mode 100644 index 0000000..7c8b37d --- /dev/null +++ b/packages/blink/example.config.json @@ -0,0 +1,10 @@ +{ + "port": 3000, + "database": "store.db", + "users": [ + { + "username": "admin", + "password": "hunter2_ifYouDontChangeThisPasswordYouDeserveToBeHacked" + } + ] +} diff --git a/packages/blink/package.json b/packages/blink/package.json new file mode 100644 index 0000000..939fe24 --- /dev/null +++ b/packages/blink/package.json @@ -0,0 +1,19 @@ +{ + "name": "@feathers-studio/blink", + "module": "src/index.ts", + "type": "module", + "dependencies": { + "@hyperactive/hyper": "workspace:*", + "cookie": "^1.0.2", + "jsdom": "^25.0.1", + "mime-types": "^2.1.35", + "nanoid": "^5.0.9" + }, + "devDependencies": { + "@types/bun": "latest", + "@types/mime-types": "^2.1.4" + }, + "peerDependencies": { + "typescript": "^5.0.0" + } +} diff --git a/packages/blink/src/config.ts b/packages/blink/src/config.ts new file mode 100644 index 0000000..dcc9424 --- /dev/null +++ b/packages/blink/src/config.ts @@ -0,0 +1,24 @@ +export interface ConfigUser { + username: string; + password: string; +} + +export interface Config { + port: number; + database: string; + users: ConfigUser[]; +} + +let config: Config = { + port: 3000, + database: "blink.db", + users: [], +}; + +try { + config = Object.assign(config, (await Bun.file("config.json").json()) as Partial); +} catch { + console.log("Config not found, using defaults. Create a config to be able to login."); +} + +export { config }; diff --git a/packages/blink/src/index.ts b/packages/blink/src/index.ts new file mode 100644 index 0000000..60f8346 --- /dev/null +++ b/packages/blink/src/index.ts @@ -0,0 +1,71 @@ +import { config } from "./config"; + +import { extname, join } from "node:path"; +import { authenticate } from "./middleware/auth"; +import { login, logout, loginPage } from "./pages/auth"; +import { dashboard } from "./pages/dashboard"; +import * as links from "./pages/links"; +import { lookup } from "mime-types"; +import { generateETagFromFile, seconds } from "./utils"; + +const ASSETS_ROOT = join(__dirname, "../assets/"); + +Bun.serve({ + port: config.port, + async fetch(request, server) { + const method = request.method; + const url = new URL(request.url); + { + const proto = request.headers.get("x-forwarded-proto"); + const host = request.headers.get("x-forwarded-host"); + if (proto) url.protocol = proto; + if (host) url.host = host; + } + const ip = request.headers.get("x-forwarded-for") ?? server.requestIP(request)?.address ?? null; + + console.log(`[${new Date().toISOString()}] ${method} ${url.pathname}${url.search} (${ip})`); + + const accept = request.headers.get("Accept"); + const withJSON = accept?.includes("application/json") ?? false; + + if (method === "GET") { + if (url.pathname.startsWith("/assets/")) { + const assetPath = join(ASSETS_ROOT, url.pathname.slice("/assets/".length)); + if (!assetPath.startsWith(ASSETS_ROOT)) { + return new Response("Access Denied", { status: 403 }); + } + return new Response(Bun.file(assetPath), { + headers: { + "Content-Type": lookup(extname(assetPath)) || "application/octet-stream", + "Cache-Control": `public, max-age=${seconds(10)}`, + "ETag": await generateETagFromFile(assetPath), + }, + }); + } + } + + if (method === "POST") { + if (url.pathname === "/login") return login(request, { ip, withJSON }); + if (url.pathname === "/logout") return logout(request, { withJSON }); + } + + const user = await authenticate(request); + if (user instanceof Response) { + if (url.pathname === "/") { + return loginPage(request, { url }); + } + } else { + if (url.pathname === "/") { + return dashboard(request, { user, url }); + } + + if (url.pathname === "/links") { + if (method === "POST") return links.POST(request, { user, withJSON }); + } + } + + return links.GET(request, { url, ip, withJSON }); + }, +}); + +console.log(`Blink is running on http://localhost:${config.port}`); diff --git a/packages/blink/src/middleware/auth.ts b/packages/blink/src/middleware/auth.ts new file mode 100644 index 0000000..c867c1e --- /dev/null +++ b/packages/blink/src/middleware/auth.ts @@ -0,0 +1,17 @@ +import { parse as cookie } from "cookie"; +import { queries } from "../store"; +import type { User } from "../types"; +import { redirectClear } from "../utils"; + +export async function authenticate(request: Request): Promise { + const token = cookie(request.headers.get("cookie") ?? "").token; + if (!token) return redirectClear("/?error=Your session has expired, please login again"); + + const session = queries.sessions.access(token); + if (!session) return redirectClear("/?error=Your session has expired, please login again"); + + const user = queries.users.getById(session.user_id); + if (!user) return redirectClear("/?error=Your session has expired, please login again"); + + return user; +} diff --git a/packages/blink/src/pages/auth.ts b/packages/blink/src/pages/auth.ts new file mode 100644 index 0000000..bc7e92f --- /dev/null +++ b/packages/blink/src/pages/auth.ts @@ -0,0 +1,100 @@ +import { body, form, h1, head, hgroup, input, link, p, title } from "@hyperactive/hyper/elements"; +import { parse as cookie } from "cookie"; +import { queries } from "../store"; +import { days, generateMeta, html, json, redirectClear } from "../utils"; + +interface LoginParams { + ip: string | null; + withJSON: boolean; +} + +export async function login(request: Request, { ip, withJSON }: LoginParams) { + const body = await request.formData(); + const username = body.get("username")?.toString(); + const password = body.get("password")?.toString(); + + if (!username || !password) { + if (withJSON) return json({ error: "Invalid username or password" }, 401); + return new Response("Invalid username or password", { status: 401 }); + } + + const result = queries.users.getByUsername(username); + if (!result) { + if (withJSON) return json({ error: "Invalid username or password" }, 401); + return redirectClear("/?error=Invalid username or password"); + } + + if (!(await Bun.password.verify(password, result.password))) { + if (withJSON) return json({ error: "Invalid username or password" }, 401); + return redirectClear("/?error=Invalid username or password"); + } + + const session = queries.sessions.create({ + token: crypto.randomUUID(), + user_id: result.id, + ip_address: ip, + user_agent: request.headers.get("user-agent"), + expires_at: new Date(Date.now() + days(30) * 1000).toISOString(), + }); + + if (!session) { + if (withJSON) return json({ error: "Failed to create session" }, 500); + return redirectClear("/?error=Failed to create session"); + } + + if (withJSON) return json({ token: session.token, expires_at: session.expires_at }, 200); + return redirectClear("/", { + "Set-Cookie": `token=${session.token}; Max-Age=${days(30)}; HttpOnly; Secure; SameSite=Strict`, + }); +} + +interface LogoutParams { + withJSON: boolean; +} + +export async function logout(request: Request, { withJSON }: LogoutParams) { + const token = cookie(request.headers.get("cookie") ?? "").token; + if (!token) { + if (withJSON) return json({ error: "No token" }, 401); + return redirectClear("/?error=No token"); + } + + const changes = await queries.sessions.logout(token); + + if (!changes.changes) { + if (withJSON) return json({ error: "Could not find active session" }, 401); + return redirectClear("/?error=Could not find active session"); + } + + if (withJSON) return json({}, 200); + return redirectClear("/", { + "Set-Cookie": `token=; Max-Age=0; HttpOnly; Secure; SameSite=Strict`, + }); +} + +export async function loginPage(request: Request, { url }: { url: URL }) { + const error = url.searchParams.get("error"); + + return html( + // @ts-expect-error "prefix" is not a valid prop, but og protocol uses it + { prefix: "http://ogp.me/ns#" }, + head( + title("Blink"), + link({ rel: "icon", type: "image/png", href: "/assets/img/favicon-96x96.png", sizes: "96x96" }), + link({ rel: "shortcut icon", href: "/assets/img/favicon.ico" }), + link({ rel: "stylesheet", href: "/assets/style.css" }), + ...generateMeta({ title: "Blink", description: "Shorten links using Blink" }, url.protocol + "//" + url.host), + ), + body( + { class: "container" }, + h1("Blink"), + form( + { action: "/login", method: "POST", enctype: "application/x-www-form-urlencoded" }, + input({ name: "username", type: "text", placeholder: "Username" }), + input({ name: "password", type: "password", placeholder: "Password" }), + input({ type: "submit", value: "Login" }), + ), + hgroup(error && p({ class: "muted" }, "Error: ", error)), + ), + )(); +} diff --git a/packages/blink/src/pages/dashboard.ts b/packages/blink/src/pages/dashboard.ts new file mode 100644 index 0000000..967c537 --- /dev/null +++ b/packages/blink/src/pages/dashboard.ts @@ -0,0 +1,98 @@ +import { parse as cookie } from "cookie"; +import { + a, + body, + button, + form, + h1, + head, + hgroup, + i, + input, + link, + meta, + nav, + p, + section, + table, + tbody, + td, + th, + thead, + title, + tr, +} from "@hyperactive/hyper/elements"; +import { queries } from "../store"; +import type { User } from "../types"; +import { html, days } from "../utils"; + +export async function dashboard(request: Request, { user, url }: { user: User; url: URL }) { + const error = url.searchParams.get("error"); + const _title = url.searchParams.get("title") ?? ""; + const _target = url.searchParams.get("target") ?? ""; + + const created = url.searchParams.get("created"); + const created_link = created ? queries.links.get(created) : null; + + const page = Number(url.searchParams.get("page")) || 1; + const limit = Number(url.searchParams.get("limit")) || 10; + + const listLinks = queries.links.list({ user_id: user.id, page, limit }); + + const session = cookie(request.headers.get("cookie") ?? "").token; + + return html( + {}, + head( + title("Blink"), + meta({ name: "viewport", content: "width=device-width, initial-scale=1" }), + link({ rel: "icon", type: "image/png", href: "/assets/img/favicon-96x96.png", sizes: "96x96" }), + link({ rel: "shortcut icon", href: "/assets/img/favicon.ico" }), + link({ rel: "stylesheet", href: "/assets/style.css" }), + ), + body( + { class: "container" }, + nav( + h1(a({ href: "/" }, "Blink")), + form(input({ class: "link", type: "submit", value: "Logout", formaction: "/logout", formmethod: "POST" })), + ), + section( + form( + { role: "group", action: "/links", method: "POST", enctype: "application/x-www-form-urlencoded" }, + input({ name: "target", type: "text", placeholder: "https://example.com", value: _target }), + input({ name: "title", type: "text", placeholder: "Title (optional)", value: _title }), + button("Shorten!"), + ), + hgroup( + error + ? p({ class: "muted" }, "Error: ", error) + : created_link && + p( + { class: "muted" }, + "Link created: ", + a({ href: "/" + created_link.slug }, url.host, "/", created_link.slug), + ), + ), + ), + listLinks.length && + table( + thead(tr(th("Title"), th("Short URL"), th("Target URL"), th("Visits"))), + tbody( + ...listLinks.map(link => + tr( + td(p(link.title || i("-"))), + td( + a({ href: "/" + link.slug, target: "_blank", rel: "noopener noreferrer" }, url.host, "/", link.slug), + ), + td(a({ href: link.target, target: "_blank", rel: "noopener noreferrer" }, link.target)), + td(String(link.visits)), + ), + ), + ), + ), + ), + )({ + // Refresh Cookie expires every time the page is loaded + "Set-Cookie": `token=${session}; Max-Age=${days(30)}; HttpOnly; Secure; SameSite=Strict`, + }); +} diff --git a/packages/blink/src/pages/links.ts b/packages/blink/src/pages/links.ts new file mode 100644 index 0000000..f3edc66 --- /dev/null +++ b/packages/blink/src/pages/links.ts @@ -0,0 +1,103 @@ +import { a, body, head, link as link_tag, meta, noscript, p } from "@hyperactive/hyper/elements"; +import { customAlphabet } from "nanoid"; +import { queries } from "../store"; +import type { User } from "../types"; +import { html, fetchMeta, generateMeta, json, redirect, type Meta } from "../utils"; + +const alphabet = "23456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnpqrstuvwxyz_-"; +const nanoid = customAlphabet(alphabet, 8); + +interface PostLinkParams { + user: User; + withJSON: boolean; +} + +export async function POST(request: Request, { user, withJSON }: PostLinkParams) { + const formData = await request.formData(); + const title = formData.get("title")?.toString(); + let target = formData.get("target")?.toString(); + + if (!target) { + if (withJSON) return json({ error: "Target is required", title }, 400); + return redirect("/", {}, { error: "Target is required", title }); + } + + let meta: Meta | null = null; + + try { + const url = new URL(target); + target = url.toString(); + meta = await fetchMeta(target); + } catch (error) { + if (withJSON) return json({ error: "Invalid target URL", title, target }, 400); + return redirect("/", {}, { error: "Invalid target URL", title, target }); + } + + let slug: string; + let attempts = 0; + while (queries.links.get((slug = nanoid()))) { + attempts++; + if (attempts > 100) { + if (withJSON) return json({ error: "Failed to generate unique slug", title, target }, 500); + return redirect("/", {}, { error: "Failed to generate unique slug", title, target }); + } + } + + if (title) (meta.title = title), (meta.meta_title = title); + + const link = queries.links.create({ ...meta, target, slug, user_id: user.id }); + + if (!link) { + if (withJSON) return json({ error: "Failed to create link", title, target }, 500); + return redirect("/", {}, { error: "Failed to create link", title, target }); + } + + if (withJSON) { + const { slug, target, title, description, meta_title, meta_description, meta_image } = link; + return json({ slug, target, title, description, meta_title, meta_description, meta_image }); + } + + return redirect("/", {}, { created: slug }); +} + +interface LinkParams { + url: URL; + ip: string | null; + withJSON: boolean; +} + +export async function GET(request: Request, { url, ip, withJSON }: LinkParams) { + const slug = url.pathname.slice(1); + + const link = queries.links.get(slug); + if (!link) { + if (withJSON) return json({ error: "Link not found" }, 404); + return new Response("Link not found", { status: 404 }); + } + + const user_agent = request.headers.get("user-agent"); + queries.visits.create({ link_id: link.id, ip_address: ip, user_agent }); + + const tags = generateMeta(link, url.protocol + "//" + url.host); + + if (withJSON) { + const { slug, target, title, description, meta_title, meta_description, meta_image } = link; + return json({ slug, target, title, description, meta_title, meta_description, meta_image }); + } + + return html( + // @ts-expect-error "prefix" is not a valid prop, but og protocol uses it + { prefix: "http://ogp.me/ns#" }, + head( + link_tag({ rel: "icon", type: "image/png", href: "/assets/img/favicon-96x96.png", sizes: "96x96" }), + link_tag({ rel: "shortcut icon", href: "/assets/img/favicon.ico" }), + ...tags, + meta({ "http-equiv": "refresh", "content": `0; url=${link.target}` }), + ), + body( + p("Redirecting you to ", a({ href: link.target }, link.target)), + // script(`window.location.href = ${JSON.stringify(l.target)};`), + noscript("If you are not redirected automatically, please ", a({ href: link.target }, "click here")), + ), + )(); +} diff --git a/packages/blink/src/setup.ts b/packages/blink/src/setup.ts new file mode 100644 index 0000000..3cdd1cf --- /dev/null +++ b/packages/blink/src/setup.ts @@ -0,0 +1,201 @@ +import { Database } from "bun:sqlite"; +import { config } from "./config"; + +export const db = new Database(config.database, { strict: true }); + +db.exec("PRAGMA journal_mode = WAL;"); + +const migrations = [ + { + up() { + // Create tables + + db.exec( + `CREATE TABLE IF NOT EXISTS users ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + username TEXT UNIQUE NOT NULL, + password TEXT NOT NULL, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP + )`, + ); + + db.exec( + `CREATE TABLE IF NOT EXISTS sessions ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + token TEXT UNIQUE, + username TEXT NOT NULL, + ip_address TEXT, + user_agent TEXT NOT NULL, + expires_at DATETIME NOT NULL, + last_active_at DATETIME, + logged_out_at DATETIME, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP + )`, + ); + + db.exec( + `CREATE TABLE IF NOT EXISTS links ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id INTEGER NOT NULL, + slug TEXT UNIQUE NOT NULL, + target TEXT, + title TEXT, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE ON UPDATE CASCADE + )`, + ); + + db.exec( + `CREATE TABLE IF NOT EXISTS visits ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + link_id INTEGER NOT NULL, + ip_address TEXT, + user_agent TEXT, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (link_id) REFERENCES links(id) ON DELETE CASCADE ON UPDATE CASCADE + )`, + ); + + // Create triggers + + for (const table of ["users", "sessions", "links", "db_meta"]) { + db.exec(`DROP TRIGGER IF EXISTS update_${table}_updated_at`); + db.exec( + `CREATE TRIGGER IF NOT EXISTS update_${table}_updated_at + AFTER UPDATE ON ${table} + BEGIN + UPDATE ${table} SET updated_at = CURRENT_TIMESTAMP WHERE id = NEW.id; + END;`, + ); + } + }, + down() { + db.exec("DROP TABLE IF EXISTS users"); + db.exec("DROP TABLE IF EXISTS sessions"); + db.exec("DROP TABLE IF EXISTS links"); + db.exec("DROP TABLE IF EXISTS visits"); + db.exec("DROP TABLE IF EXISTS db_meta"); + }, + }, + { + up() { + db.exec("UPDATE db_meta SET value = '2' WHERE key = 'version'"); + }, + down() { + db.exec("UPDATE db_meta SET value = '1' WHERE key = 'version'"); + }, + }, + { + up() { + // this migration was written at a time when the app didn't have many users + // so we can drop the sessions table and create a new one with the foreign key constraint + db.exec("DROP TABLE IF EXISTS sessions"); + + db.exec( + `CREATE TABLE IF NOT EXISTS sessions ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + token TEXT UNIQUE, + ip_address TEXT, + user_agent TEXT NOT NULL, + expires_at DATETIME NOT NULL, + last_active_at DATETIME, + logged_out_at DATETIME, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, + user_id INTEGER NOT NULL, + FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE ON UPDATE CASCADE + )`, + ); + }, + down() { + db.exec("DROP TABLE IF EXISTS sessions"); + + db.exec( + `CREATE TABLE IF NOT EXISTS sessions ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + token TEXT UNIQUE, + username TEXT NOT NULL, + ip_address TEXT, + user_agent TEXT NOT NULL, + expires_at DATETIME NOT NULL, + last_active_at DATETIME, + logged_out_at DATETIME, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP + )`, + ); + }, + }, + { + up() { + db.exec("ALTER TABLE links ADD COLUMN meta_title TEXT"); + db.exec("ALTER TABLE links ADD COLUMN meta_description TEXT"); + db.exec("ALTER TABLE links ADD COLUMN meta_image TEXT"); + }, + down() { + db.exec("ALTER TABLE links DROP COLUMN meta_title"); + db.exec("ALTER TABLE links DROP COLUMN meta_description"); + db.exec("ALTER TABLE links DROP COLUMN meta_image"); + }, + }, + { + up() { + db.exec("ALTER TABLE links ADD COLUMN description TEXT"); + }, + down() { + db.exec("ALTER TABLE links DROP COLUMN description"); + }, + }, +]; + +db.exec( + `CREATE TABLE IF NOT EXISTS db_meta ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + key TEXT UNIQUE NOT NULL, + value TEXT NOT NULL, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP + )`, +); + +const currentVersion = Number( + db // + .query<{ value: string }, { key: string }>("SELECT value FROM db_meta WHERE key = :key") + .get({ key: "version" })?.value ?? 0, +); + +if (currentVersion === 0) { + console.log("Database is empty, applying migrations..."); +} else { + console.log( + `Current database version: ${currentVersion}. ` + + (currentVersion < migrations.length ? "Applying migrations to reach version." : "No migrations to apply."), + ); +} + +for (let index = 1; index <= migrations.length; index++) { + const migration = migrations[index - 1]; + if (currentVersion < index) { + try { + db.transaction(() => { + migration.up(); + db.exec( + `INSERT INTO db_meta (key, value) + VALUES ('version', ?) + ON CONFLICT(key) DO UPDATE SET + value = excluded.value, + updated_at = CURRENT_TIMESTAMP + `, + [index.toString()], + ); + })(); + console.log(`Migration to version ${index} applied successfully.`); + } catch (error) { + console.error(`Failed to apply migration to version ${index}:`, error); + break; + } + } +} diff --git a/packages/blink/src/store.ts b/packages/blink/src/store.ts new file mode 100644 index 0000000..4dd7234 --- /dev/null +++ b/packages/blink/src/store.ts @@ -0,0 +1,94 @@ +import { config } from "./config"; +import { db } from "./setup"; +import type { User, Session, Link, Visit } from "./types"; + +export const queries = { + users: { + getByUsername: (username: string) => + db.query(`SELECT * FROM users WHERE username = :username`).get({ username }), + getById: (id: number) => db.query(`SELECT * FROM users WHERE id = :id`).get({ id }), + create: (user: Omit) => + db + .query>( + `INSERT INTO users (username, password) VALUES (:username, :password) RETURNING *`, + ) + .get(user), + list: () => db.query(`SELECT * FROM users`).all(), + update: (user: Pick) => + db + .query>( + `UPDATE users SET username = :username, password = :password WHERE username = :username`, + ) + .run(user), + }, + sessions: { + create: (session: Omit) => + db + .query>( + `INSERT INTO sessions (token, user_id, ip_address, user_agent, expires_at) + VALUES (:token, :user_id, :ip_address, :user_agent, :expires_at) RETURNING *`, + ) + .get(session), + access: (token: string) => + db + .query( + `UPDATE sessions + SET last_active_at = CURRENT_TIMESTAMP, expires_at = datetime('now', '+30 days') + WHERE token = :token AND logged_out_at IS NULL AND expires_at > CURRENT_TIMESTAMP + RETURNING *`, + ) + .get({ token }), + logout: (token: string) => + db + .query(`UPDATE sessions SET logged_out_at = CURRENT_TIMESTAMP WHERE token = :token`) + .run({ token }), + }, + links: { + get: (slug: string) => db.query(`SELECT * FROM links WHERE slug = :slug`).get({ slug }), + create: (link: Omit) => + db + .query>( + `INSERT INTO links (title, description, meta_title, meta_description, meta_image, target, slug, user_id) + VALUES (:title, :description, :meta_title, :meta_description, :meta_image, :target, :slug, :user_id) + RETURNING *`, + ) + .get(link), + list: ({ user_id, page = 1, limit = 10 }: { user_id: number; page: number; limit: number }) => + db + .query( + `SELECT links.*, + (SELECT COUNT(*) FROM visits WHERE visits.link_id = links.id) AS visits + FROM links + WHERE links.user_id = :user_id + ORDER BY links.created_at DESC + LIMIT :limit + OFFSET :offset`, + ) + .all({ user_id, offset: (page - 1) * limit, limit }), + }, + visits: { + create: (visit: Omit) => + db + .query>( + `INSERT INTO visits (link_id, ip_address, user_agent) + VALUES (:link_id, :ip_address, :user_agent) RETURNING *`, + ) + .get(visit), + }, +}; + +{ + for (const user of config.users) { + const existing = queries.users.getByUsername(user.username); + if (!existing) { + console.log(`Creating user ${user.username}`); + queries.users.create({ username: user.username, password: await Bun.password.hash(user.password) }); + } else if (!(await Bun.password.verify(user.password, existing.password))) { + console.log(`Updating password for user ${user.username}`); + queries.users.update({ username: user.username, password: await Bun.password.hash(user.password) }); + } + } +} + +const users = queries.users.list(); +console.log(users.length, "users in the database:", users.map(user => user.username).join(", ")); diff --git a/packages/blink/src/types.ts b/packages/blink/src/types.ts new file mode 100644 index 0000000..f4d43c6 --- /dev/null +++ b/packages/blink/src/types.ts @@ -0,0 +1,41 @@ +export type User = { + id: number; + username: string; + password: string; + created_at: string; + updated_at: string; +}; + +export type Session = { + id: number; + token: string; + user_id: number; + ip_address: string | null; + user_agent: string | null; + expires_at: string; + logged_out_at: string | null; + created_at: string; + updated_at: string; +}; + +export type Link = { + id: number; + user_id: number; + slug: string; + target: string; + title?: string | null; + description?: string | null; + meta_title?: string | null; + meta_description?: string | null; + meta_image?: string | null; + created_at: string; + updated_at: string; +}; + +export type Visit = { + id: number; + link_id: number; + ip_address?: string | null; + user_agent?: string | null; + created_at: string; +}; diff --git a/packages/blink/src/utils.ts b/packages/blink/src/utils.ts new file mode 100644 index 0000000..c639864 --- /dev/null +++ b/packages/blink/src/utils.ts @@ -0,0 +1,151 @@ +import { meta as meta_tag } from "@hyperactive/hyper/elements"; +import { JSDOM } from "jsdom"; +import { stat } from "node:fs/promises"; +import { html as html_tag } from "@hyperactive/hyper/elements"; +import { renderHTML, type HyperNode } from "@hyperactive/hyper"; + +export const ellipses = (text: string, maxLength: number) => + text.length > maxLength ? text.slice(0, maxLength - 3) + "..." : text; + +export const days = (n: number) => n * 60 * 60 * 24; +export const hours = (n: number) => n * 60 * 60; +export const minutes = (n: number) => n * 60; +export const seconds = (n: number) => n; + +export async function generateETagFromFile(filePath: string): Promise { + const stats = await stat(filePath); + const mtime = stats.mtime.getTime().toString(); + const size = stats.size.toString(); + const rawETag = `${mtime}-${size}`; + + const hashBuffer = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(rawETag)); + const hashArray = new Uint8Array(hashBuffer); + let hashHex = ""; + for (let i = 0; i < hashArray.length; i++) { + hashHex += hashArray[i].toString(16).padStart(2, "0"); + } + return `"${hashHex}"`; +} + +type AllDefined = { [K in keyof T]: NonNullable }; + +const filterUndefined = >(obj: T) => + Object.fromEntries(Object.entries(obj).filter(([_, v]) => v !== undefined)) as AllDefined; + +export const redirectClear = (url: string, headers: Record = {}) => + redirect(url, { "Set-Cookie": `token=; Max-Age=0; HttpOnly; Secure; SameSite=Strict`, ...headers }); + +export const redirect = ( + url: string, + headers: Record = {}, + searchParams: Record = {}, +) => + new Response(null, { + status: 302, + headers: { + Location: url + "?" + new URLSearchParams(filterUndefined(searchParams)), + ...headers, + }, + }); + +export const json = (body: any, status = 200, headers: Record = {}) => + new Response(JSON.stringify(body), { status, headers: { "Content-Type": "application/json", ...headers } }); + +export const html = + (...elements: Parameters) => + (headers: Record = {}) => + new Response("" + renderHTML(html_tag(...elements)), { + headers: { "Content-Type": "text/html", ...headers }, + }); + +export interface Meta { + title?: string | null; + description?: string | null; + meta_title?: string | null; + meta_description?: string | null; + meta_image?: string | null; +} + +export async function fetchMeta(url: string): Promise { + const u = new URL(url); + + if (u.protocol === "magnet:") { + const title = u.searchParams.get("dn") ?? "Magnet URI"; + const description = u.searchParams.get("xt"); + return { + title, + description, + meta_title: title, + meta_description: description, + meta_image: null, + }; + } + + if (["http:", "https:"].includes(u.protocol)) { + try { + const response = await fetch(u); + const html = await response.text(); + const dom = new JSDOM(html); + const doc = dom.window.document; + + const title = doc.querySelector("title")?.textContent; + const description = doc.querySelector("meta[name='description']")?.getAttribute("content"); + + const meta_title = ( + doc.querySelector("meta[name='title']") ?? + doc.querySelector("meta[property='og:title']") ?? + doc.querySelector("meta[name='twitter:title']") + )?.getAttribute("content"); + + const meta_description = ( + doc.querySelector("meta[name='description']") ?? + doc.querySelector("meta[property='og:description']") ?? + doc.querySelector("meta[name='twitter:description']") + )?.getAttribute("content"); + + const meta_image = ( + doc.querySelector("meta[property='og:image']") ?? doc.querySelector("meta[property='twitter:image']") + )?.getAttribute("content"); + + return { title, description, meta_title, meta_description, meta_image }; + } catch (error) { + console.error("Error fetching meta for", url, error instanceof Error ? error.message : "Unknown error"); + } + } + + return { title: null, description: null, meta_title: null, meta_description: null, meta_image: null }; +} + +export function generateMeta(meta: Meta, fallback_root: string) { + const m = (name: string, content?: string | null, fallback?: string | null) => { + content = content ?? fallback; + // standard tags and twitter use "name" + return content ? meta_tag({ name, content }) : null; + }; + + const mp = (property: string, content?: string | null, fallback?: string | null) => { + content = content ?? fallback; + // @ts-expect-error "property" is not a valid prop, but og uses it + return content ? meta_tag({ property, content }) : null; + }; + + const card = "summary_large_image"; + + const title = meta.title ?? meta.meta_title ?? "Link Preview"; + const description = meta.description ?? meta.meta_description ?? "This link was shortened using Blink"; + + const img = meta.meta_image ?? `${fallback_root}/assets/img/1200x630.jpg`; + + return [ + m("title", title), + m("description", description), + mp("og:type", "website"), + mp("og:title", meta.meta_title, title), + mp("og:description", meta.meta_description, description), + mp("og:image", img), + m("twitter:card", card), + m("twitter:title", meta.meta_title, title), + m("twitter:description", meta.meta_description, description), + m("twitter:image", img), + ].filter(Boolean); +} diff --git a/packages/blink/tsconfig.json b/packages/blink/tsconfig.json new file mode 100644 index 0000000..238655f --- /dev/null +++ b/packages/blink/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + // Enable latest features + "lib": ["ESNext", "DOM"], + "target": "ESNext", + "module": "ESNext", + "moduleDetection": "force", + "jsx": "react-jsx", + "allowJs": true, + + // Bundler mode + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "noEmit": true, + + // Best practices + "strict": true, + "skipLibCheck": true, + "noFallthroughCasesInSwitch": true, + + // Some stricter flags (disabled by default) + "noUnusedLocals": false, + "noUnusedParameters": false, + "noPropertyAccessFromIndexSignature": false + } +} diff --git a/packages/h2h/LICENSE b/packages/h2h/LICENSE new file mode 100644 index 0000000..249ab64 --- /dev/null +++ b/packages/h2h/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) Feathers Studio + +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/packages/h2h/h2h.js b/packages/h2h/h2h.js new file mode 100644 index 0000000..2410252 --- /dev/null +++ b/packages/h2h/h2h.js @@ -0,0 +1,95 @@ +import { Parser } from "https://esm.sh/htmlparser2@9.0.0"; + +export const h2h = (input, stream, opts) => { + const elements = new Set(); + let indentLevel = 0; + let attrOpen = false; + let textWritten = false; + let justClosed = false; + let size = 0; + const streamWrite = stream.write; + stream.write = (...chunk) => ((size += chunk.length), console.log(size), streamWrite(...chunk)); + + const parser = new Parser( + { + onopentag(name, attr) { + elements.add(name); + if (justClosed) { + justClosed = false; + } + if (textWritten) { + stream.write(", "); + textWritten = false; + } + if (attrOpen) attrOpen = false; + stream.write("\n" + "\t".repeat(indentLevel++) + `${name}`); + let attrKeys = Object.keys(attr); + if (attrKeys.length) { + if (opts?.useFancySelectors) { + let selector = ""; + if (attr.id) selector += "#" + attr.id.split(" ").join("#"); + if (attr.class) selector += "." + attr.class.split(" ").join("."); + if (selector) stream.write(`["${selector}"]`); + + delete attr.class; + delete attr.id; + } + + stream.write("("); + + attrKeys = Object.keys(attr); + if (attrKeys.length) { + stream.write(JSON.stringify(attr), ", "); + attrOpen = true; + } + } else { + stream.write("("); + } + }, + + oncomment(comments) { + justClosed = false; + (comments = comments.trim()) && + comments.split("\n").forEach(comment => stream.write("\n// " + "\t".repeat(indentLevel) + comment.trim())); + }, + + ontext(text) { + if (!text.trim()) return; + if (textWritten) { + stream.write(", "); + textWritten = false; + } + if (attrOpen) attrOpen = false; + justClosed = false; + stream.write("`" + text.trim().replace(/`/g, "\\`") + "`"); + textWritten = true; + }, + + onclosetag() { + if (justClosed) stream.pop(); + if (textWritten) { + textWritten = false; + } + if (attrOpen) { + stream.pop(); + stream.pop(); + attrOpen = false; + } + justClosed = true; + stream.write("),"); + indentLevel--; + }, + + onend() { + if (justClosed) stream.pop(); + if (size) stream.write(";\n"); + stream.end(); + }, + }, + { decodeEntities: true }, + ); + + parser.write(input); + parser.end(); + return { elements, stream }; +}; diff --git a/packages/h2h/index.html b/packages/h2h/index.html new file mode 100644 index 0000000..e7d6801 --- /dev/null +++ b/packages/h2h/index.html @@ -0,0 +1,153 @@ + + + +

HTML to Hyperactive

+ +
+
+ + + + + + + + + +
+ + \ No newline at end of file diff --git a/packages/hyper/.gitignore b/packages/hyper/.gitignore new file mode 100644 index 0000000..3a8a924 --- /dev/null +++ b/packages/hyper/.gitignore @@ -0,0 +1,178 @@ +# Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore + +lib/ +!src/lib/ + +# Logs + +logs +_.log +npm-debug.log_ +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Caches + +.cache + +# Diagnostic reports (https://nodejs.org/api/report.html) + +report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json + +# Runtime data + +pids +_.pid +_.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover + +lib-cov + +# Coverage directory used by tools like istanbul + +coverage +*.lcov + +# nyc test coverage + +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) + +.grunt + +# Bower dependency directory (https://bower.io/) + +bower_components + +# node-waf configuration + +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) + +build/Release + +# Dependency directories + +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) + +web_modules/ + +# TypeScript cache + +*.tsbuildinfo + +# Optional npm cache directory + +.npm + +# Optional eslint cache + +.eslintcache + +# Optional stylelint cache + +.stylelintcache + +# Microbundle cache + +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history + +.node_repl_history + +# Output of 'npm pack' + +*.tgz + +# Yarn Integrity file + +.yarn-integrity + +# dotenv environment variable files + +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# parcel-bundler cache (https://parceljs.org/) + +.parcel-cache + +# Next.js build output + +.next +out + +# Nuxt.js build / generate output + +.nuxt +dist + +# Gatsby files + +# Comment in the public line in if your project uses Gatsby and not Next.js + +# https://nextjs.org/blog/next-9-1#public-directory-support + +# public + +# vuepress build output + +.vuepress/dist + +# vuepress v2.x temp and cache directory + +.temp + +# Docusaurus cache and generated files + +.docusaurus + +# Serverless directories + +.serverless/ + +# FuseBox cache + +.fusebox/ + +# DynamoDB Local files + +.dynamodb/ + +# TernJS port file + +.tern-port + +# Stores VSCode versions used for testing VSCode extensions + +.vscode-test + +# yarn v2 + +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* + +# IntelliJ based IDEs +.idea + +# Finder (MacOS) folder config +.DS_Store diff --git a/packages/hyper/LICENSE b/packages/hyper/LICENSE new file mode 100644 index 0000000..249ab64 --- /dev/null +++ b/packages/hyper/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) Feathers Studio + +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/packages/hyper/README.md b/packages/hyper/README.md new file mode 100644 index 0000000..786fc65 --- /dev/null +++ b/packages/hyper/README.md @@ -0,0 +1,90 @@ +
+ Hyperactive +
+ +
+

hyperactive

+
+ +Hyperactive is a powerful set of tools to build reactive web applications. + +We're currently working on a 2.0 release, which will include fully reactive client-side rendering. To try the latest version, you can get `hyper`: + +```bash +npm install https://gethyper.dev + +yarn add https://gethyper.dev + +pnpm add https://gethyper.dev + +bun install https://gethyper.dev +``` + +Hyperactive is also available on [NPM](https://www.npmjs.com/package/@hyperactive/hyper). + +This is not a release version, so expect some bugs. + +[![Hyperactive Version 2.0.0-beta.8](https://img.shields.io/static/v1?label=Version&message=2.0.0-beta.8&style=for-the-badge&labelColor=FF6A00&color=fff)](https://npmjs.com/package/@hyperactive/hyper) + +
+

Usage

+
+ +### On the server + +```TypeScript +import { renderHTML } from "@hyperactive/hyper"; +import { div, p, h1, br } from "@hyperactive/hyper/elements"; + +assertEquals( + renderHTML( + section( + { class: "container" }, + div( + img({ src: "/hero.jpg" }), + h1("Hello World"), + ), + ), + ), + `

Hello World

`, +); +``` + +### In the browser + +[![@types/web 0.0.234](https://img.shields.io/static/v1?label=@types/web&message=0.0.234&style=for-the-badge&labelColor=ff0000&color=fff)](https://npmjs.com/package/@types/web) + +Please install `@types/web` to use Hyperactive in the browser. Your package manager will automatically install the correct version of `@types/web` for you by default. See the [versions](./docs/versions.md) table for the correct version of `@types/web` for each version of Hyperactive. + +```bash +bun install @types/web +``` + +```TypeScript +import { State, renderDOM } from "@hyperactive/hyper"; +import { div, p, button } from "@hyperactive/hyper/elements"; + +const s = new State(0); + +const root = document.getElementById("root"); + +renderDOM( + root, + div( + p("You clicked ", s, " times"), + button( + { on: { click: () => s.update(s.value + 1) } }, + "Increment" + ), + ), +); + +``` + +
+

Testimonials

+
+ +
+ Thomas's testimonial +
diff --git a/packages/hyper/package.json b/packages/hyper/package.json new file mode 100644 index 0000000..326edb3 --- /dev/null +++ b/packages/hyper/package.json @@ -0,0 +1,36 @@ +{ + "name": "@hyperactive/hyper", + "version": "2.0.0-beta.8", + "module": "lib/index.js", + "sideEffects": false, + "devDependencies": { + "@types/bun": "latest", + "@types/semver": "^7.5.8", + "semver": "^7.6.3" + }, + "peerDependencies": { + "@types/web": "0.0.234" + }, + "peerDependenciesMeta": { + "@types/web": { + "optional": true + } + }, + "scripts": { + "check": "tsc --noEmit", + "build": "tsc", + "test": "bun test --coverage", + "prepack": "bun run scripts/prepack.ts", + "prepare": "bun run build" + }, + "exports": { + ".": "./lib/index.js", + "./elements": "./lib/elements.js", + "./dom": "./lib/lib/dom.js" + }, + "type": "module", + "files": [ + "lib", + "src" + ] +} diff --git a/packages/hyper/scripts/prepack.ts b/packages/hyper/scripts/prepack.ts new file mode 100644 index 0000000..675cc46 --- /dev/null +++ b/packages/hyper/scripts/prepack.ts @@ -0,0 +1,83 @@ +/// +import { join } from "node:path"; +import rcompare from "semver/functions/rcompare"; + +const pkg = Bun.file(join(import.meta.dir, "../package.json")); +const pkgJson = await pkg.json(); +const hyperVersion = pkgJson.version; +const typesWebVersion = pkgJson.peerDependencies["@types/web"]; + +if (typeof hyperVersion !== "string") { + throw new Error("Failed to find Hyperactive version in package.json. Check scripts/prepack.ts"); +} + +if (typeof typesWebVersion !== "string") { + throw new Error("Failed to find @types/web version in package.json. Check scripts/prepack.ts"); +} + +const promptfree = Bun.argv.includes("--yes") || Bun.argv.includes("-y"); + +if (!promptfree) { + console.log(`Hyperactive ${hyperVersion} requires @types/web ${typesWebVersion}.`); + + const answer = await prompt("Update README.md and docs/versions.md? (Y/n)"); + + if (answer === "n") { + console.log("Skipping"); + process.exit(0); + } +} + +{ + const readme = Bun.file(join(import.meta.dir, "../../../README.md")); + const readmeText = await readme.text(); + + // match ![Hyperactive Version 2.0.0-beta.1] + const oldVersion = readmeText.match(/!\[Hyperactive Version (?.+?)\]/)?.groups?.version; + if (!oldVersion) { + throw new Error("Failed to find old Hyperactive version in README.md. Check scripts/postinstall.ts"); + } + + const oldTypesWebVersion = readmeText.match(/!\[@types\/web (?.+?)\]/)?.groups?.version; + if (!oldTypesWebVersion) { + throw new Error("Failed to find old @types/web version in README.md. Check scripts/postinstall.ts"); + } + + console.log(); + console.log("Updating README.md"); + console.log(`Hyperactive from ${oldVersion} to ${hyperVersion}`); + console.log(`@types/web from ${oldTypesWebVersion} to ${typesWebVersion}`); + console.log(); + + const newReadme = readmeText.replaceAll(oldVersion, hyperVersion).replaceAll(oldTypesWebVersion, typesWebVersion); + await readme.writer().write(newReadme); + + await Bun.write(join(import.meta.dir, "../README.md"), newReadme); +} + +{ + const versions = Bun.file(join(import.meta.dir, "../../../docs/versions.md")); + const versionsText = await versions.text(); + + // parse all table rows + let versionsRows = (versionsText.match(/^\| (.*) \| (.*) \|$/gm)?.slice(2) ?? []).map(row => { + const [hyper, types] = row + .split("|") + .slice(1, 3) + .map(cell => cell.trim()); + return { hyper, types }; + }); + + versionsRows = versionsRows.filter(row => row.hyper !== hyperVersion); + versionsRows.push({ hyper: hyperVersion, types: typesWebVersion }); + versionsRows.sort((a, b) => rcompare(a.hyper, b.hyper)); + + const newVersions = versionsRows.map(row => `| ${row.hyper} | ${row.types} |`).join("\n"); + + await Bun.write(versions, ""); + // replace everything after | ------------------- | ------------------ | + const MARKER = "| ------------------- | ------------------ |"; + const indexOfMarker = versionsText.indexOf(MARKER); + const newVersionsText = versionsText.slice(0, indexOfMarker) + MARKER + "\n" + newVersions; + await versions.writer().write(newVersionsText); +} diff --git a/packages/hyper/scripts/publish.sh b/packages/hyper/scripts/publish.sh new file mode 100755 index 0000000..7d87c26 --- /dev/null +++ b/packages/hyper/scripts/publish.sh @@ -0,0 +1,20 @@ +#!/bin/bash + +# Check if filename is provided +if [ -z "$1" ]; then + echo "Usage: $0 " + exit 1 +fi + +# Configure AWS CLI with Scaleway credentials +aws configure set aws_access_key_id $SCW_ACCESS_KEY +aws configure set aws_secret_access_key $SCW_SECRET_KEY +aws configure set region fr-par # or your region +aws configure set endpoint_url https://$SCW_ENDPOINT + +# Upload file and make it public +aws s3 cp "$1" s3://pkg/$(basename "$1") --acl public-read + +# Print the public URL +echo "File uploaded successfully!" +echo "Public URL: https://$SCW_ENDPOINT/pkg/$(basename "$1")" diff --git a/packages/hyper/src/attributes.ts b/packages/hyper/src/attributes.ts new file mode 100644 index 0000000..c29fd3a --- /dev/null +++ b/packages/hyper/src/attributes.ts @@ -0,0 +1,20 @@ +import type { Tag } from "./lib/tags.ts"; +import type { Attributes as Attr } from "./lib/attributes.ts"; +import { ReadonlyState } from "./state.ts"; +import { AriaAttributes } from "./lib/aria.ts"; +import { MaybeArray, MaybeState, MaybeString } from "./util.ts"; + +type Aria = { [K in keyof AriaAttributes]?: AriaAttributes[K] | ReadonlyState }; + +/** Re-export of Attributes with State support */ +export type Attributes = + { + + // prettier-ignore + [K in keyof Attr]?: + K extends "ref" | "on" ? Attr[K] + : K extends "aria" ? Aria + : K extends "class" ? MaybeState> + : Attr[K] | ReadonlyState[K]>; + + }; diff --git a/packages/hyper/src/browser.test.ts b/packages/hyper/src/browser.test.ts new file mode 100644 index 0000000..06ca744 --- /dev/null +++ b/packages/hyper/src/browser.test.ts @@ -0,0 +1,100 @@ +import { renderDOM, State } from "./index.ts"; +import { div, input, p, ul, button, li, form } from "./elements.ts"; + +import type { Document } from "./lib/dom.ts"; +import { List, Member } from "./list.ts"; +declare const document: Document; +const root = document.getElementById("root")!; + +// { +// const state = new State(""); + +// renderDOM( +// root, +// div( +// { class: "container" }, +// input({ +// type: "text", +// // value: state.value, +// on: { +// input(e) { +// const value = (e.target as any).value; +// state.set(value); +// }, +// }, +// }), +// p(state.to(s => (s === "Bye" ? h1("Good", b("bye!")) : span("Hello ", s, " !")))), +// ), +// ); +// } + +{ + // Hyperactive version + + const Todo = (todo: Member<{ id: number; content: string }>) => { + const state = new State(todo.value.content); + + return li( + // span(todo.index.to(i => String(i + 1))), + form( + p(todo.to(t => t.content || "Untitled")), + input({ + type: "text", + value: state, + on: { input: e => state.set((e.target as any).value) }, + }), + button( + { + on: { + async click() { + // await fetch("...", { + // method: "PUT", + // body: JSON.stringify({ value: state.value }), + // }); + todo.set({ ...todo.value, content: state.value }); + }, + }, + }, + "Update", + ), + button( + { + on: { + async click() { + // await fetch("...", { + // method: "DELETE", + // body: JSON.stringify({ value: state.value }), + // }); + todo.remove(); + }, + }, + }, + "Delete", + ), + ), + ); + }; + + const App = () => { + const todos = new List([ + { id: 1, content: "Hyperactive" }, + { id: 2, content: "Jigza" }, + { id: 3, content: "Telegraf" }, + ]); + + // const interval = setInterval(() => { + // todos.append({ id: todos.size.value + 1, content: "Untitled" }); + // }, 1000); + + return div.container( + // button({ on: { click: () => clearInterval(interval) } }, "STOP"), + ul(todos.each(Todo)), + button( + { on: { click: () => todos.append({ id: todos.size.value + 1, content: "" }) } }, + "Add", + ), + ); + }; + + renderDOM(root, App()); +} diff --git a/packages/hyper/src/context.internal.ts b/packages/hyper/src/context.internal.ts new file mode 100644 index 0000000..6d54d20 --- /dev/null +++ b/packages/hyper/src/context.internal.ts @@ -0,0 +1,9 @@ +import { HyperNodeish } from "./node"; + +export class Provider { + constructor(public readonly contextId: symbol, public readonly contextValue: T, public readonly contextualChild: HyperNodeish) {} +} + +export class Consumer { + constructor(public readonly contextId: symbol, public readonly renderWithContext: (value: T) => HyperNodeish) {} +} \ No newline at end of file diff --git a/packages/hyper/src/context.test.ts b/packages/hyper/src/context.test.ts new file mode 100644 index 0000000..b1a544f --- /dev/null +++ b/packages/hyper/src/context.test.ts @@ -0,0 +1,131 @@ +import { describe, expect, it } from "bun:test"; +import { div, p, span } from "./elements.ts"; +import { Context, renderHTML } from "./index.ts"; + +describe("Context", () => { + it("should create a simple context", () => { + const context = new Context(() => "test"); + + const nested = context.with(value => p(value)); + const tree = context.provider("value", div(nested)); + + expect(renderHTML(tree)).toBe("

value

"); + }); + + it("should create a nested context", () => { + const context = new Context(() => "test"); + + const nested2 = context.with(value => p(value)); + const nested = context.provider("other value", nested2); + const tree = context.provider("value", div(nested)); + + expect(renderHTML(tree)).toBe("

other value

"); + }); + + it("should throw an error if a context is not found", () => { + const context = new Context(() => "test", "ErrorContext"); + + const tree = context.with(value => div(p(value))); + + expect(() => renderHTML(tree)).toThrow("Requested context for (id: ErrorContext) not found. Was the Context Provider used?"); + }); + + it("should handle multiple distinct contexts correctly", () => { + const stringContext = new Context(() => "default string"); + const numberContext = new Context(() => 0); + + const tree = stringContext.provider( + "hello", + numberContext.provider( + 123, + stringContext.with(sVal => numberContext.with(nVal => p(`${sVal} ${nVal}`))), + ), + ); + + expect(renderHTML(tree)).toBe("

hello 123

"); + }); + + it("should handle objects as context values", () => { + type MyObject = { message: string; count: number }; + const objectContext = new Context(() => ({ message: "default", count: 0 })); + + const tree = objectContext.provider( + { message: "custom", count: 42 }, + objectContext.with(obj => p(`${obj.message} ${obj.count}`)), + ); + + expect(renderHTML(tree)).toBe("

custom 42

"); + }); + + it("should allow null as a provided context value", () => { + // Default value is non-null to distinguish it from an explicit null + const nullableContext = new Context(() => "default string"); + + const tree = nullableContext.provider( + null, // Explicitly providing null + nullableContext.with(value => p(value === null ? "was null" : value!)), + ); + + expect(renderHTML(tree)).toBe("

was null

"); + }); + + it("should allow undefined as a provided context value", () => { + const undefinedContext = new Context(() => "default"); + + const tree = undefinedContext.provider( + undefined, + undefinedContext.with(value => p(value === undefined ? "was undefined" : value!)), + ); + expect(renderHTML(tree)).toBe("

was undefined

"); + }); + + it("should handle functions as context values", () => { + const funcContext = new Context<() => string>(() => () => "default func"); + + const tree = funcContext.provider( + () => "dynamic value from func", + funcContext.with(fn => p(fn())), + ); + expect(renderHTML(tree)).toBe("

dynamic value from func

"); + }); + + it("multiple sibling consumers should receive the same context value", () => { + const sharedContext = new Context(() => "default"); + + const sibling1 = sharedContext.with(value => p(value)); + const sibling2 = sharedContext.with(value => span(value)); + const tree = sharedContext.provider("shared", div(sibling1, sibling2)); + expect(renderHTML(tree)).toBe("

shared

shared
"); + }); + + it("inner provider of the same context overrides outer provider for its descendants only", () => { + const themeContext = new Context(() => "light"); + + const themedButton = (label: string) => themeContext.with(theme => p(`${label}: ${theme}`)); + + const tree = themeContext.provider( + "dark", + div( + themedButton("Button A"), + themeContext.provider("contrast", themedButton("Button B")), + themedButton("Button C"), + ), + ); + expect(renderHTML(tree)).toBe("

Button A: dark

Button B: contrast

Button C: dark

"); + }); + + it("consumer of one context is unaffected by providers of a different context", () => { + const contextA = new Context(() => "A_default"); + const contextB = new Context(() => "B_default"); + + const tree = contextA.provider( + "Value A", + contextB.provider( + "Value B", + // This consumer should only care about ContextA + contextA.with(valA => p(`A: ${valA}`)), + ), + ); + expect(renderHTML(tree)).toBe("

A: Value A

"); + }); +}); diff --git a/packages/hyper/src/context.ts b/packages/hyper/src/context.ts new file mode 100644 index 0000000..ed2f34d --- /dev/null +++ b/packages/hyper/src/context.ts @@ -0,0 +1,23 @@ +import { HyperNodeish } from "./node.ts"; +import { randId } from "./util.ts"; +import * as ContextInternal from "./context.internal.ts"; + +export class Context { + id: symbol; + + constructor(public readonly defaultCtx: () => T, public readonly name: string = randId()) { + this.id = Symbol(name); + } + + provider(value: T, child: HyperNodeish) { + return new ContextInternal.Provider(this.id, value, child); + } + + with(fn: (value: T) => HyperNodeish) { + return new ContextInternal.Consumer(this.id, fn); + } + + static isContext(x: any): x is ContextInternal.Provider | ContextInternal.Consumer { + return x instanceof ContextInternal.Provider || x instanceof ContextInternal.Consumer; + } +} diff --git a/packages/hyper/src/domutils.ts b/packages/hyper/src/domutils.ts new file mode 100644 index 0000000..0129e12 --- /dev/null +++ b/packages/hyper/src/domutils.ts @@ -0,0 +1,18 @@ +// @ts-nocheck + +import { State } from "./state.ts"; +import type { HyperNode } from "./node.ts"; +import type { HTMLInputElement } from "./lib/dom.ts"; + +export const bind = , S extends State>(node: N, state: S): N => { + const oldRef = node.attrs.ref || (() => {}); + + node.attrs.ref = (el: HTMLInputElement) => { + el.value = state.value; + state.listen(value => (el.value = value)); + el.addEventListener("input", e => state.publish((e?.target as unknown as { value: string }).value)); + oldRef(el as any); + }; + + return node; +}; diff --git a/packages/hyper/src/element.ts b/packages/hyper/src/element.ts new file mode 100644 index 0000000..11e3035 --- /dev/null +++ b/packages/hyper/src/element.ts @@ -0,0 +1,76 @@ +import { h, HyperNode, normaliseParams } from "./node.ts"; +import { parseSelector } from "./parse.ts"; +import { isNonNullable, MaybeString } from "./util.ts"; +import type { HyperNodeish, NonEmptyElement } from "./node.ts"; +import type { Tag } from "./lib/tags.ts"; +import type { EmptyElements } from "./lib/emptyElements.ts"; +import type { Attributes } from "./attributes.ts"; +import { ReadonlyState, State } from "./state.ts"; + +export namespace Hyper { + export interface Empty { + // no children for empty tags + (props?: Attributes): HyperNode; + [selector: string]: Hyper.Empty; + } + + export interface Base { + (props: Attributes): HyperNode; + (...childNodes: HyperNodeish[]): HyperNode; + (props: Attributes, ...childNodes: HyperNodeish[]): HyperNode; + [selector: string]: Hyper.Base; + } + + export type Element = T extends EmptyElements ? Hyper.Empty : Hyper.Base; +} + +export type Elements = { [k in Tag]: Hyper.Element }; + +function createSelectorProxy( + element: T, + hyperElement: Hyper.Element, + loaded?: string, +): Hyper.Element { + type hE = Hyper.Element; + + return new Proxy(hyperElement, { + // Do NOT cache the target _ like we do below in elements, + // because if users use this feature a lot, it'll leak memory + // Just let elements.a.hello be a new function every time + // and elements.a.hello !== elements.a.hello + // It's okay -- don't fret about it + get(_: hE, selector: string) { + const parsed = parseSelector([loaded, selector].filter(Boolean).join(" ")); + + const hyperElement = function hyperElement( + props?: Attributes | HyperNodeish, + ...childNodes: HyperNodeish[] + ) { + const { attrs, children } = normaliseParams(props, childNodes); + + const className = new State(""); + + if (ReadonlyState.isState(attrs.class)) { + className.set(attrs.class.value); + attrs.class.pipe(className); + } else className.set(attrs.class); + + if (parsed.class) className.setWith(c => [parsed.class, c].flatMap(x => (x ? x : [])).filter(Boolean)); + + const merged = { ...attrs, id: parsed.id || attrs.id, class: className }; + + return new HyperNode(element, merged, children.filter(isNonNullable)); + } as hE; + + return createSelectorProxy(element, hyperElement, selector); + }, + }); +} + +export function create(element: T) { + const hyperElement = function hyperElement(...params: any[]) { + return h(element as NonEmptyElement, ...params); + } as Elements[T]; + + return /* @__PURE__ */ createSelectorProxy(element, hyperElement); +} diff --git a/packages/hyper/src/elements.ts b/packages/hyper/src/elements.ts new file mode 100644 index 0000000..4d45724 --- /dev/null +++ b/packages/hyper/src/elements.ts @@ -0,0 +1,809 @@ +// This file was generated by a script. +// Do not manually edit this file. + +import { create } from "./element.ts"; + +/** + * Together with its href attribute, creates a hyperlink to web pages, files, email addresses, locations within the current page, or anything else a URL can address. + * + * @see {@link https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/a MDN | a} + */ +export const a = create("a"); + +/** + * Represents an abbreviation or acronym. + * + * @see {@link https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/abbr MDN | abbr} + */ +export const abbr = create("abbr"); + +/** + * Indicates that the enclosed HTML provides contact information for a person or people, or for an organization. + * + * @see {@link https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/address MDN | address} + */ +export const address = create("address"); + +/** + * Defines an area inside an image map that has predefined clickable areas. An ``image map`` allows geometric areas on an image to be associated with [hyperlink](https://developer.mozilla.org/en-US/docs/Glossary/Hyperlink). + * + * @see {@link https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/area MDN | area} + */ +export const area = create("area"); + +/** + * Represents a self-contained composition in a document, page, application, or site, which is intended to be independently distributable or reusable (e.g., in syndication). Examples include a forum post, a magazine or newspaper article, a blog entry, a product card, a user-submitted comment, an interactive widget or gadget, or any other independent item of content. + * + * @see {@link https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/article MDN | article} + */ +export const article = create("article"); + +/** + * Represents a portion of a document whose content is only indirectly related to the document's main content. Asides are frequently presented as sidebars or call-out boxes. + * + * @see {@link https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/aside MDN | aside} + */ +export const aside = create("aside"); + +/** + * Used to embed sound content in documents. It may contain one or more audio sources, represented using the src attribute or the source element: the browser will choose the most suitable one. It can also be the destination for streamed media, using a [MediaStream](https://developer.mozilla.org/en-US/docs/Web/API/MediaStream). + * + * @see {@link https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/audio MDN | audio} + */ +export const audio = create("audio"); + +/** + * Used to draw the reader's attention to the element's contents, which are not otherwise granted special importance. This was formerly known as the Boldface element, and most browsers still draw the text in boldface. However, you should not use `` for styling text or granting importance. If you wish to create boldface text, you should use the CSS [font-weight](https://developer.mozilla.org/en-US/docs/Web/CSS/font-weight) property. If you wish to indicate an element is of special importance, you should use the [``](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/strong) element. + * + * @see {@link https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/b MDN | b} + */ +export const b = create("b"); + +/** + * Specifies the base URL to use for all relative URLs in a document. There can be only one such element in a document. + * + * @see {@link https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/base MDN | base} + */ +export const base = create("base"); + +/** + * Tells the browser's bidirectional algorithm to treat the text it contains in isolation from its surrounding text. It's particularly useful when a website dynamically inserts some text and doesn't know the directionality of the text being inserted. + * + * @see {@link https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/bdi MDN | bdi} + */ +export const bdi = create("bdi"); + +/** + * Overrides the current directionality of text, so that the text within is rendered in a different direction. + * + * @see {@link https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/bdo MDN | bdo} + */ +export const bdo = create("bdo"); + +/** + * Indicates that the enclosed text is an extended quotation. Usually, this is rendered visually by indentation. A URL for the source of the quotation may be given using the cite attribute, while a text representation of the source can be given using the [``](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/cite) element. + * + * @see {@link https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/blockquote MDN | blockquote} + */ +export const blockquote = create("blockquote"); + +/** + * Represents the content of an HTML document. There can be only one such element in a document. + * + * @see {@link https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/body MDN | body} + */ +export const body = create("body"); + +/** + * Produces a line break in text (carriage-return). It is useful for writing a poem or an address, where the division of lines is significant. + * + * @see {@link https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/br MDN | br} + */ +export const br = create("br"); + +/** + * An interactive element activated by a user with a mouse, keyboard, finger, voice command, or other assistive technology. Once activated, it performs an action, such as submitting a [form](https://developer.mozilla.org/en-US/docs/Learn_web_development/Extensions/Forms) or opening a dialog. + * + * @see {@link https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/button MDN | button} + */ +export const button = create("button"); + +/** + * Container element to use with either the [canvas scripting API](https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API) or the [WebGL API](https://developer.mozilla.org/en-US/docs/Web/API/WebGL_API) to draw graphics and animations. + * + * @see {@link https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/canvas MDN | canvas} + */ +export const canvas = create("canvas"); + +/** + * Specifies the caption (or title) of a table. + * + * @see {@link https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/caption MDN | caption} + */ +export const caption = create("caption"); + +/** + * Used to mark up the title of a creative work. The reference may be in an abbreviated form according to context-appropriate conventions related to citation metadata. + * + * @see {@link https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/cite MDN | cite} + */ +export const cite = create("cite"); + +/** + * Displays its contents styled in a fashion intended to indicate that the text is a short fragment of computer code. By default, the content text is displayed using the user agent's default monospace font. + * + * @see {@link https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/code MDN | code} + */ +export const code = create("code"); + +/** + * Defines one or more columns in a column group represented by its implicit or explicit parent [``](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/colgroup) element. The `` element is only valid as a child of a [``](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/colgroup) element that has no [span](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/colgroup#span) attribute defined. + * + * @see {@link https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/col MDN | col} + */ +export const col = create("col"); + +/** + * Defines a group of columns within a table. + * + * @see {@link https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/colgroup MDN | colgroup} + */ +export const colgroup = create("colgroup"); + +/** + * Links a given piece of content with a machine-readable translation. If the content is time- or date-related, the `