From 8ccf20e5cf506436ccb788fbcd35541af5e6c507 Mon Sep 17 00:00:00 2001 From: Mykyta Date: Sat, 22 Aug 2026 13:04:39 +0200 Subject: [PATCH 1/9] feat: init docs --- docs/.gitignore | 24 +++ docs/bun.lock | 273 +++++++++++++++++++++++++ docs/config/.vitepress/config.mts | 30 +++ docs/config/.vitepress/theme/index.ts | 17 ++ docs/config/.vitepress/theme/style.css | 131 ++++++++++++ docs/content/api-examples.md | 49 +++++ docs/content/index.md | 25 +++ docs/content/markdown-examples.md | 85 ++++++++ docs/package.json | 12 ++ 9 files changed, 646 insertions(+) create mode 100644 docs/.gitignore create mode 100644 docs/bun.lock create mode 100644 docs/config/.vitepress/config.mts create mode 100644 docs/config/.vitepress/theme/index.ts create mode 100644 docs/config/.vitepress/theme/style.css create mode 100644 docs/content/api-examples.md create mode 100644 docs/content/index.md create mode 100644 docs/content/markdown-examples.md create mode 100644 docs/package.json diff --git a/docs/.gitignore b/docs/.gitignore new file mode 100644 index 00000000..a547bf36 --- /dev/null +++ b/docs/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/docs/bun.lock b/docs/bun.lock new file mode 100644 index 00000000..1645fb47 --- /dev/null +++ b/docs/bun.lock @@ -0,0 +1,273 @@ +{ + "lockfileVersion": 1, + "configVersion": 1, + "workspaces": { + "": { + "devDependencies": { + "vitepress": "^2.0.0-alpha.19", + "vue": "^3.5.41", + }, + }, + }, + "packages": { + "@babel/helper-string-parser": ["@babel/helper-string-parser@7.29.7", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="], + + "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="], + + "@babel/parser": ["@babel/parser@7.29.8", "", { "dependencies": { "@babel/types": "^7.29.8" }, "bin": "./bin/babel-parser.js" }, "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA=="], + + "@babel/types": ["@babel/types@7.29.8", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg=="], + + "@docsearch/css": ["@docsearch/css@4.7.0", "", {}, "sha512-Sk5xkdRFeE7PeWjG9l4AfTwdvMfr9wHiwNNCpHXT4v4SNyNMKdHGvEILc31BgaVFGDDNbv5u/a73tofRiwbEZw=="], + + "@docsearch/js": ["@docsearch/js@4.7.0", "", {}, "sha512-x5lCqu1tetgsJFkjQ6VSocbHldsRkGEgwg5N98Vx21sq/V5wcmj4u226PY9k+TEpIgQ772zlYbPLTPicWyGnpA=="], + + "@docsearch/sidepanel-js": ["@docsearch/sidepanel-js@4.7.0", "", {}, "sha512-A8r34jCU8kcIk2viECEn2msA28ojUF1BLi/3v5OWWc5G2N3jOuuumBXoeYjfr8dA0UxgFSy5R2bt12dnFJQSyA=="], + + "@iconify-json/simple-icons": ["@iconify-json/simple-icons@1.2.93", "", { "dependencies": { "@iconify/types": "*" } }, "sha512-/XhANjfGYOuqvSR3TmUnkQkINvQ4GVjVuukvymRbxtVFBvIq/yiXJqCDycKcQPT401OYT9H2vIY6ihAlz1QIAw=="], + + "@iconify/types": ["@iconify/types@2.0.0", "", {}, "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg=="], + + "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], + + "@oxc-project/types": ["@oxc-project/types@0.146.0", "", {}, "sha512-XC0QsnnhVe7sLIWmYmdPw7x5P0h4W8vUU3Nv1ySgWXtvCz8NizoAEpGXA0sOYoJQV2Rl13LgURAHQ5cI5ILCSA=="], + + "@rolldown/binding-android-arm-eabi": ["@rolldown/binding-android-arm-eabi@1.2.5", "", { "os": "android", "cpu": "arm" }, "sha512-DLe/i+l8ynIBY7XEQ191TeZvCoowIGa18R+dIV30GW7DiOtp74i/xX8hs8GUjW5ARV7VZuie3d6AumSmCwbeRA=="], + + "@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.2.5", "", { "os": "android", "cpu": "arm64" }, "sha512-zXcwKlQApYAOELHd8PwKDFkagYF9Wy4e0RJ+0qnzl9Pjnpj75TEG8ufv40p2J7kCEfwZAsNiuzRIyNNMWT38ig=="], + + "@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.2.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-dK4QakI42nzWgJT5sm4y4y/O//D4OxM75/cH28RLV+nzIN9AY+YsbuUVrUTjlLjXR6vpyxFbSsbmNuJ6BP9sww=="], + + "@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.2.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-fqSALaUu1Wjd1nK2uW2kJDWdLCc8lx1IcY+MTY26Aurfdx19anlzhqXOgCFbBFQnlFDTn4TC1/7Nz4Bl2mLP3A=="], + + "@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.2.5", "", { "os": "freebsd", "cpu": "x64" }, "sha512-/vCnNxlkxs9tKxNDcyWUePpJ/PgTzxIaVhoM5SmG8UV+GR/IcPam4VYxi7GIMo7PSDuNqlJqvprqii9NqqVCMw=="], + + "@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.2.5", "", { "os": "linux", "cpu": "arm" }, "sha512-abk0NLA519LxRCszmbE0jYKuQ9YPocOXTiOXOo6Yr+YAT95VH+PtqYAjOJvGKt3viEd/x4qzabAlwd5bHOOARg=="], + + "@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.2.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-Y7eALiJ8lr0M2HH103Js+g7V34wf6snlpZLAsHI90uLhr3PVlNsbFVAXJC9d/V6BnPyKtpSwI+NcB/RLxsQxuA=="], + + "@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.2.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-xMvZgnbZg4YVnR/AX2b3oOPDTFYJvUVaJg5FedA/LuvexAtXibZQej4cnTkw3rjsJ/ggUROB64TdtETiim+FYA=="], + + "@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.2.5", "", { "os": "linux", "cpu": "ppc64" }, "sha512-GRjeqTUDHTo5GwntsLaAMcBahG3nlpjftXWZLN73HiYQlhwEowvarFgQnRnQZtIp4keXX7quXFbG38uPZBa2EA=="], + + "@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.2.5", "", { "os": "linux", "cpu": "s390x" }, "sha512-vLNTR45F2Uwc8AufkNXPmB4VliaXs+FvcheEogIzOXzO4l+LzieXF5A/TWxLy5HtqpsRCHUfd0lPVrrdgXdLHQ=="], + + "@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.2.5", "", { "os": "linux", "cpu": "x64" }, "sha512-Mgj59/HTuYeK9Gz2MA+mBWKnHsAgkBSec15ZMb1st3oIfFbX7gCjOae7GydHhzcyQi9Z/7M1QuN9bR3oFqF0jQ=="], + + "@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.2.5", "", { "os": "linux", "cpu": "x64" }, "sha512-mY8AP0/ichsbhAxGnLa3d3+MwV0EfgrPND2bplI3Ym8T6R2pJ0N87bvrKVwNXmdy3jnr6eQBecdqx/HMknBmpA=="], + + "@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.2.5", "", { "os": "none", "cpu": "arm64" }, "sha512-8SLssA2oweAxyRgDp789ACfRb/3P+zNRJpzZxSizxF9m8NUDQ4+3xjo8ttjhVGGw6Qxb70oZiEtIjaKikCO7Yw=="], + + "@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.2.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-vGbruD5zquhoc8D9SViXgN2FBJtNdTyQ4DtG+SWiEGlJiAzoKcZ2xp+xuXCffhubVdt0NJlTZqkeRuERy7g8Cw=="], + + "@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.2.5", "", { "os": "win32", "cpu": "x64" }, "sha512-e/SXpgISz+IoqVcSSI0rx/d/he8zqLex+/rCWpnHpmVfmPIUjag9H6P7zotf0gJHwPUhQxZ/mF8tr6acebT9yw=="], + + "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.1", "", {}, "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw=="], + + "@shikijs/core": ["@shikijs/core@4.4.3", "", { "dependencies": { "@shikijs/primitive": "4.4.3", "@shikijs/types": "4.4.3", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.5", "hast-util-to-html": "^9.0.5" } }, "sha512-QCR4q2ZO/ILJEuwiBMel4wdcTDb1JGwfjKTxPDF6x8ixOaluPrVqIn06C99AcRPhmYlBR56d/Fb+GN58GzExpg=="], + + "@shikijs/engine-javascript": ["@shikijs/engine-javascript@4.4.3", "", { "dependencies": { "@shikijs/types": "4.4.3", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.6" } }, "sha512-FbOjFJp9VLdo1Wevs10BBtVxiTWwNLqZh5Gkhjgda/ioL15YOgeSl9n+6XMa3qRlPQzfhFNe641SrynFHYG0nQ=="], + + "@shikijs/engine-oniguruma": ["@shikijs/engine-oniguruma@4.4.3", "", { "dependencies": { "@shikijs/types": "4.4.3", "@shikijs/vscode-textmate": "^10.0.2" } }, "sha512-EcOQkxdxGQrc1Row/cC2c96/v1dbZqGnEVu1qTuT/MJmp6+cXCvQussowVmCv5Tqr3KuY3c7IbM6HTW3LJ1k9w=="], + + "@shikijs/langs": ["@shikijs/langs@4.4.3", "", { "dependencies": { "@shikijs/types": "4.4.3" } }, "sha512-ePic0yfAJGOF83D5wBHK/00EjK65oahBYxFk5epgq33WRv7X9UuxLEV8PtR0szC0z8dl7INIpIodB99JRFlR+A=="], + + "@shikijs/primitive": ["@shikijs/primitive@4.4.3", "", { "dependencies": { "@shikijs/types": "4.4.3", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.5" } }, "sha512-m0wBeLDQDeIxRdUmrCPdQqfuUamDwRL5isCfYbguKD6NiaKpVbsv+3J81DyIKgNW5h4WAIIr8T4EkgQrBBxvaQ=="], + + "@shikijs/themes": ["@shikijs/themes@4.4.3", "", { "dependencies": { "@shikijs/types": "4.4.3" } }, "sha512-w8UHjeUnIR965KMWJHUPXOc2mNJUnK3vpVLYLvw5IYU2mnTTJ89E24OrJDBNiJDQ0qzb0tc4l7mrIXx5cFeIyw=="], + + "@shikijs/transformers": ["@shikijs/transformers@4.4.3", "", { "dependencies": { "@shikijs/core": "4.4.3", "@shikijs/types": "4.4.3" } }, "sha512-oJSARV6NaWd+rnNJbtnpAdj3Zg0ZVyzsnMgb3vi3HA+35y8lBWUCpOnWsmyiXZIikY+x1BDqrQUgmxfzWh7Jvw=="], + + "@shikijs/types": ["@shikijs/types@4.4.3", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.5" } }, "sha512-UEJxmRR++MAGR6hugn0vgVS2W/6lWAts84FFSrnlH9sP0LNol7E5+NQ792pH8liWUhyMyjhTgSUH3k7iD7tc5g=="], + + "@shikijs/vscode-textmate": ["@shikijs/vscode-textmate@10.0.2", "", {}, "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg=="], + + "@types/hast": ["@types/hast@3.0.5", "", { "dependencies": { "@types/unist": "*" } }, "sha512-rp/ezSWaD1m44dPKICGhiskI13nVr7qTloFwDa/IYkhhf5nzwP+zIQcIJh3WIFSBOy/H1PzB40jPjMDksN4F+g=="], + + "@types/linkify-it": ["@types/linkify-it@5.0.0", "", {}, "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q=="], + + "@types/markdown-it": ["@types/markdown-it@14.2.0", "", { "dependencies": { "@types/linkify-it": "^5", "@types/mdurl": "^2" } }, "sha512-NoQ2yGlLWj4wpxMs+TYmRKk3thDrQ97agr7sFqfLsAlvoS8SNQuTrlObhFqG9iugdTtgOE9jpJ6FNM4ZGsa5xQ=="], + + "@types/mdast": ["@types/mdast@4.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA=="], + + "@types/mdurl": ["@types/mdurl@2.0.0", "", {}, "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg=="], + + "@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="], + + "@types/web-bluetooth": ["@types/web-bluetooth@0.0.21", "", {}, "sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA=="], + + "@ungap/structured-clone": ["@ungap/structured-clone@1.3.3", "", {}, "sha512-60YRaenCQcVjYEKOcG824+DRGGIQ3VKErcBoAEDJZz5bKIs2ZG+X/H9Nk+Q6EVkwJk5QNApxbrc5QtBSwtrXAg=="], + + "@vitejs/plugin-vue": ["@vitejs/plugin-vue@6.0.8", "", { "dependencies": { "@rolldown/pluginutils": "^1.0.1" }, "peerDependencies": { "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0", "vue": "^3.2.25" } }, "sha512-0ZjgOg7oO6farnNGup7yvoM/YXZV84OZxHAwtflItNa/6zzQyVb5LNxyea3FEKEX2XlagIKzrlH7wwxkKgtiew=="], + + "@vue/compiler-core": ["@vue/compiler-core@3.5.41", "", { "dependencies": { "@babel/parser": "^7.29.8", "@vue/shared": "3.5.41", "entities": "^7.0.1", "estree-walker": "^2.0.2", "source-map-js": "^1.2.1" } }, "sha512-q0Xtv/F9w2YO/7htQhtiL+Ev2WCJbe5N2hc+XfgyKkEKqWpSxknmT8QOuGdEKNdjPq0c3F7rNpFkTo3Kfrm7pg=="], + + "@vue/compiler-dom": ["@vue/compiler-dom@3.5.41", "", { "dependencies": { "@vue/compiler-core": "3.5.41", "@vue/shared": "3.5.41" } }, "sha512-oKacVfNglLvGjnS6BXOlGL7EyG2h8X03pqXCjzotRZUaXGjbrTJUnVAQjrCqUnS+lyu31nwQjZY/d817GmCnfw=="], + + "@vue/compiler-sfc": ["@vue/compiler-sfc@3.5.41", "", { "dependencies": { "@babel/parser": "^7.29.8", "@vue/compiler-core": "3.5.41", "@vue/compiler-dom": "3.5.41", "@vue/compiler-ssr": "3.5.41", "@vue/shared": "3.5.41", "estree-walker": "^2.0.2", "magic-string": "^0.30.21", "postcss": "^8.5.19", "source-map-js": "^1.2.1" } }, "sha512-XJhip7R2wy6vX3knCxdZN4KracFaZUef58s1KYewqluedHIJaPIVfXoYT7MF1F8nCvv6k8bWWxDC8opMkg1VTQ=="], + + "@vue/compiler-ssr": ["@vue/compiler-ssr@3.5.41", "", { "dependencies": { "@vue/compiler-dom": "3.5.41", "@vue/shared": "3.5.41" } }, "sha512-U3v5OejKEGqOI0Wy0+Sz7hGuIFZHA4LSXzrNM3IMIeDyJEBBfTpX26n3SDgToRpP2bLc9FfI2j/kSgcJ8Emq5A=="], + + "@vue/devtools-api": ["@vue/devtools-api@8.2.1", "", { "dependencies": { "@vue/devtools-kit": "^8.2.1" } }, "sha512-6u4vXBlIBAC1wMplIZgpyPn7uh/s4Bf6F5bMzvLv+EdJ0aHs/+4B7Ygv864EStQSjRbsRzTko/kUG1A1IejQ3A=="], + + "@vue/devtools-kit": ["@vue/devtools-kit@8.2.1", "", { "dependencies": { "@vue/devtools-shared": "^8.2.1", "birpc": "^2.6.1", "hookable": "^5.5.3", "perfect-debounce": "^2.0.0" } }, "sha512-FIGIuq3AWReEpbAHY/cRGeHDfI0qOb8OCQ3YjbEAX04uaxIDbGc9rhkbVcG7rnfHPXE3RsU5KrWOu9V/okd8AQ=="], + + "@vue/devtools-shared": ["@vue/devtools-shared@8.2.1", "", {}, "sha512-Fkac7lUdGReh6pVOi3AYPRGe82LQqRmAfThW7RRligOAP0ZA/Z1z9XLHDM9dv34pV2HRc79DK8uKPeG2fLnA/g=="], + + "@vue/reactivity": ["@vue/reactivity@3.5.41", "", { "dependencies": { "@vue/shared": "3.5.41" } }, "sha512-rznsqKM0np0x18EjzF8x88MpEhdNsffbvFbckLL5+oUKz1BxAImEmO7J1ArRYSyo6aQaVoBDp7jEkT91OOxydA=="], + + "@vue/runtime-core": ["@vue/runtime-core@3.5.41", "", { "dependencies": { "@vue/reactivity": "3.5.41", "@vue/shared": "3.5.41" } }, "sha512-Vcry58hiAKwGen9Z1jUZE0feFsNArPCMOImYI8el48A9Idf6DuQYD0U05zZIF2Iad1hGhPSvcbBbAOhNr55fhg=="], + + "@vue/runtime-dom": ["@vue/runtime-dom@3.5.41", "", { "dependencies": { "@vue/reactivity": "3.5.41", "@vue/runtime-core": "3.5.41", "@vue/shared": "3.5.41", "csstype": "^3.2.3" } }, "sha512-3vVBahVBS9+U6cmXBLyb8nE6/yYo4J/CGI9eVFs3KiMc0YHuudwKyShTD65jtJy/L9PUUxNAFu4cj4LiJ0UFbw=="], + + "@vue/server-renderer": ["@vue/server-renderer@3.5.41", "", { "dependencies": { "@vue/compiler-ssr": "3.5.41", "@vue/runtime-dom": "3.5.41", "@vue/shared": "3.5.41" } }, "sha512-n6hx/pNFfbD6SuyeuMVkvqox8bwf/ET9JlA/kAz/imw8sw++wkqKe2mHX5KutjPpbKE4Z56yTHszoOjGMI9igQ=="], + + "@vue/shared": ["@vue/shared@3.5.41", "", {}, "sha512-IOnwSCma8j+9xJT6b8H0dEYidC80NsYmNMlZxRsukYcSoGaDBohog5hDxzeUXdFeGWFA++vWvxqOmrr96VlqMA=="], + + "@vueuse/core": ["@vueuse/core@14.4.0", "", { "dependencies": { "@types/web-bluetooth": "^0.0.21", "@vueuse/metadata": "14.4.0", "@vueuse/shared": "14.4.0" }, "peerDependencies": { "vue": "^3.5.0" } }, "sha512-X4WHz1HlCzCBoYXesUkifzzWBAcZgXG8Fi5iNPQg/epdzOB3gu8Fawj3hvuwYR1nGcXGnvxwYYcUC/71++svtQ=="], + + "@vueuse/integrations": ["@vueuse/integrations@14.4.0", "", { "dependencies": { "@vueuse/core": "14.4.0", "@vueuse/shared": "14.4.0" }, "peerDependencies": { "async-validator": "^4", "axios": "^1", "change-case": "^5", "drauu": "^0.4", "focus-trap": "^7 || ^8", "fuse.js": "^7", "idb-keyval": "^6", "jwt-decode": "^4", "nprogress": "^0.2", "qrcode": "^1.5", "sortablejs": "^1", "universal-cookie": "^7 || ^8", "vue": "^3.5.0" }, "optionalPeers": ["async-validator", "axios", "change-case", "drauu", "focus-trap", "fuse.js", "idb-keyval", "jwt-decode", "nprogress", "qrcode", "sortablejs", "universal-cookie"] }, "sha512-oJz9qTgczvA7L1nXQFRU7h8tQbOCoiceqvMMhT9XYMyOGTqLJ2rEa09PON+nD2t48sZUfeOmg4eaWJXV4sZb/w=="], + + "@vueuse/metadata": ["@vueuse/metadata@14.4.0", "", {}, "sha512-swx/255R6JyHZFJhx845iz5CRWDZdCfvkZOpACWc5+c5WHcG24mv8gUT1WIdFQaHt6dq79rvILd9QnCWiyVm9g=="], + + "@vueuse/shared": ["@vueuse/shared@14.4.0", "", { "peerDependencies": { "vue": "^3.5.0" } }, "sha512-JRgY90Sz8DDtPMsaDflvPMp9xYk69JZAmbuDvAquUVXKr2gEjqtzGNTTthLfckH0BzBqvnu31gb4a8TGLRe79g=="], + + "birpc": ["birpc@2.9.0", "", {}, "sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw=="], + + "ccount": ["ccount@2.0.1", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="], + + "character-entities-html4": ["character-entities-html4@2.1.0", "", {}, "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA=="], + + "character-entities-legacy": ["character-entities-legacy@3.0.0", "", {}, "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ=="], + + "comma-separated-tokens": ["comma-separated-tokens@2.0.3", "", {}, "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg=="], + + "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], + + "dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="], + + "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], + + "devlop": ["devlop@1.1.0", "", { "dependencies": { "dequal": "^2.0.0" } }, "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA=="], + + "entities": ["entities@7.0.1", "", {}, "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="], + + "estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="], + + "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], + + "focus-trap": ["focus-trap@8.2.2", "", { "dependencies": { "tabbable": "^6.5.0" } }, "sha512-qV0g8hRYBqgACcFOH3f9wXc4zPKhr/0z9RI2a6ZijZ72EeBi4g8oBy8zAWuUR1TsMpOzwpUMFvjdasrC41Joug=="], + + "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], + + "hast-util-to-html": ["hast-util-to-html@9.0.5", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-whitespace": "^3.0.0", "html-void-elements": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "stringify-entities": "^4.0.0", "zwitch": "^2.0.4" } }, "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw=="], + + "hast-util-whitespace": ["hast-util-whitespace@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw=="], + + "hookable": ["hookable@5.5.3", "", {}, "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ=="], + + "html-void-elements": ["html-void-elements@3.0.0", "", {}, "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg=="], + + "lightningcss": ["lightningcss@1.33.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.33.0", "lightningcss-darwin-arm64": "1.33.0", "lightningcss-darwin-x64": "1.33.0", "lightningcss-freebsd-x64": "1.33.0", "lightningcss-linux-arm-gnueabihf": "1.33.0", "lightningcss-linux-arm64-gnu": "1.33.0", "lightningcss-linux-arm64-musl": "1.33.0", "lightningcss-linux-x64-gnu": "1.33.0", "lightningcss-linux-x64-musl": "1.33.0", "lightningcss-win32-arm64-msvc": "1.33.0", "lightningcss-win32-x64-msvc": "1.33.0" } }, "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA=="], + + "lightningcss-android-arm64": ["lightningcss-android-arm64@1.33.0", "", { "os": "android", "cpu": "arm64" }, "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg=="], + + "lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.33.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg=="], + + "lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.33.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ=="], + + "lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.33.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg=="], + + "lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.33.0", "", { "os": "linux", "cpu": "arm" }, "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ=="], + + "lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.33.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg=="], + + "lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.33.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ=="], + + "lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.33.0", "", { "os": "linux", "cpu": "x64" }, "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg=="], + + "lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.33.0", "", { "os": "linux", "cpu": "x64" }, "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw=="], + + "lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.33.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA=="], + + "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.33.0", "", { "os": "win32", "cpu": "x64" }, "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA=="], + + "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], + + "mark.js": ["mark.js@8.11.1", "", {}, "sha512-1I+1qpDt4idfgLQG+BNWmrqku+7/2bi5nLf4YwF8y8zXvmfiTBY3PV3ZibfrjBueCByROpuBjLLFCajqkgYoLQ=="], + + "mdast-util-to-hast": ["mdast-util-to-hast@13.2.1", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "@ungap/structured-clone": "^1.0.0", "devlop": "^1.0.0", "micromark-util-sanitize-uri": "^2.0.0", "trim-lines": "^3.0.0", "unist-util-position": "^5.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA=="], + + "micromark-util-character": ["micromark-util-character@2.1.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q=="], + + "micromark-util-encode": ["micromark-util-encode@2.0.1", "", {}, "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw=="], + + "micromark-util-sanitize-uri": ["micromark-util-sanitize-uri@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-symbol": "^2.0.0" } }, "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ=="], + + "micromark-util-symbol": ["micromark-util-symbol@2.0.1", "", {}, "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q=="], + + "micromark-util-types": ["micromark-util-types@2.0.2", "", {}, "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA=="], + + "minisearch": ["minisearch@7.2.0", "", {}, "sha512-dqT2XBYUOZOiC5t2HRnwADjhNS2cecp9u+TJRiJ1Qp/f5qjkeT5APcGPjHw+bz89Ms8Jp+cG4AlE+QZ/QnDglg=="], + + "nanoid": ["nanoid@3.3.18", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w=="], + + "oniguruma-parser": ["oniguruma-parser@0.12.2", "", {}, "sha512-6HVa5oIrgMC6aA6WF6XyyqbhRPJrKR02L20+2+zpDtO5QAzGHAUGw5TKQvwi5vctNnRHkJYmjAhRVQF2EKdTQw=="], + + "oniguruma-to-es": ["oniguruma-to-es@4.3.6", "", { "dependencies": { "oniguruma-parser": "^0.12.2", "regex": "^6.1.0", "regex-recursion": "^6.0.2" } }, "sha512-csuQ9x3Yr0cEIs/Zgx/OEt9iBw9vqIunAPQkx19R/fiMq2oGVTgcMqO/V3Ybqefr1TBvosI6jU539ksaBULJyA=="], + + "perfect-debounce": ["perfect-debounce@2.1.0", "", {}, "sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g=="], + + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], + + "picomatch": ["picomatch@4.0.5", "", {}, "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A=="], + + "postcss": ["postcss@8.5.26", "", { "dependencies": { "nanoid": "^3.3.17", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ=="], + + "property-information": ["property-information@7.2.0", "", {}, "sha512-IAtzIB6sUiWaJYrX9smp3V46pBGbBeLFRGdh25kg1334VcBlD8HzhPeNIWQH9zhGmo2itIe25EHt9dQP7G5hmg=="], + + "regex": ["regex@6.1.0", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg=="], + + "regex-recursion": ["regex-recursion@6.0.2", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg=="], + + "regex-utilities": ["regex-utilities@2.3.0", "", {}, "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng=="], + + "rolldown": ["rolldown@1.2.5", "", { "dependencies": { "@oxc-project/types": "=0.146.0", "@rolldown/pluginutils": "^1.0.0" }, "optionalDependencies": { "@rolldown/binding-android-arm-eabi": "1.2.5", "@rolldown/binding-android-arm64": "1.2.5", "@rolldown/binding-darwin-arm64": "1.2.5", "@rolldown/binding-darwin-x64": "1.2.5", "@rolldown/binding-freebsd-x64": "1.2.5", "@rolldown/binding-linux-arm-gnueabihf": "1.2.5", "@rolldown/binding-linux-arm64-gnu": "1.2.5", "@rolldown/binding-linux-arm64-musl": "1.2.5", "@rolldown/binding-linux-ppc64-gnu": "1.2.5", "@rolldown/binding-linux-s390x-gnu": "1.2.5", "@rolldown/binding-linux-x64-gnu": "1.2.5", "@rolldown/binding-linux-x64-musl": "1.2.5", "@rolldown/binding-openharmony-arm64": "1.2.5", "@rolldown/binding-win32-arm64-msvc": "1.2.5", "@rolldown/binding-win32-x64-msvc": "1.2.5" }, "bin": { "rolldown": "./bin/cli.mjs" } }, "sha512-VD2IE5PUG4Oj8zz2VGykiYd5wbnjdIiSsNQb8Qu5B+noEp+A78mu2iVvpp27g8es14Tk9rofNs5Tku9iQCS4fA=="], + + "shiki": ["shiki@4.4.3", "", { "dependencies": { "@shikijs/core": "4.4.3", "@shikijs/engine-javascript": "4.4.3", "@shikijs/engine-oniguruma": "4.4.3", "@shikijs/langs": "4.4.3", "@shikijs/themes": "4.4.3", "@shikijs/types": "4.4.3", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.5" } }, "sha512-Mb/GvXPHBAXdgGIcnfU5L3ldpn1XcxrGkPHwqgRx17/I2XRfqlFKk2vGkHWINn1kdXvzJZeuO3is6I9KLPFm0g=="], + + "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], + + "space-separated-tokens": ["space-separated-tokens@2.0.2", "", {}, "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q=="], + + "stringify-entities": ["stringify-entities@4.0.4", "", { "dependencies": { "character-entities-html4": "^2.0.0", "character-entities-legacy": "^3.0.0" } }, "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg=="], + + "tabbable": ["tabbable@6.5.0", "", {}, "sha512-wieBHXygIm7OyQOu5hQlkk62/WyCFYGlWg7L6/ZCUZwx0o398Zkn4pVmMyfYhfMG8kGrj/Krt8eIk6UKC6VzwA=="], + + "tinyglobby": ["tinyglobby@0.2.17", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g=="], + + "trim-lines": ["trim-lines@3.0.1", "", {}, "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg=="], + + "unist-util-is": ["unist-util-is@6.0.1", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g=="], + + "unist-util-position": ["unist-util-position@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA=="], + + "unist-util-stringify-position": ["unist-util-stringify-position@4.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ=="], + + "unist-util-visit": ["unist-util-visit@5.1.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg=="], + + "unist-util-visit-parents": ["unist-util-visit-parents@6.0.2", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ=="], + + "vfile": ["vfile@6.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile-message": "^4.0.0" } }, "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q=="], + + "vfile-message": ["vfile-message@4.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw=="], + + "vite": ["vite@8.2.2", "", { "dependencies": { "lightningcss": "^1.33.0", "picomatch": "^4.0.5", "postcss": "^8.5.26", "rolldown": "~1.2.4", "tinyglobby": "^0.2.17" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.4.0 || ^0.5.0", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-cFKLV/PRgAUlIRm5WjMjJ86jrftzpqcgH+Us+DS8mI3CDNiH30Whrz8uHL3+MOLPAgqbMBAqWdAHAphOAM+z/Q=="], + + "vitepress": ["vitepress@2.0.0-alpha.19", "", { "dependencies": { "@docsearch/css": "^4.7.0", "@docsearch/js": "^4.7.0", "@docsearch/sidepanel-js": "^4.7.0", "@iconify-json/simple-icons": "^1.2.92", "@shikijs/core": "^4.4.1", "@shikijs/transformers": "^4.4.1", "@shikijs/types": "^4.4.1", "@types/markdown-it": "^14.1.2", "@vitejs/plugin-vue": "^6.0.8", "@vue/devtools-api": "^8.2.1", "@vue/shared": "^3.5.40", "@vueuse/core": "^14.4.0", "@vueuse/integrations": "^14.4.0", "focus-trap": "^8.2.2", "mark.js": "8.11.1", "minisearch": "^7.2.0", "shiki": "^4.4.1", "vite": "^8.2.0", "vue": "^3.5.40" }, "peerDependencies": { "markdown-it-mathjax3": "^4", "postcss": "^8" }, "optionalPeers": ["markdown-it-mathjax3", "postcss"], "bin": { "vitepress": "bin/vitepress.js" } }, "sha512-WnBsb0Bwr43kXKyiis+lld/7ri3hnMbthS8N3hpFtjjwsdLO4IRmiAE08D7aud4q6oMDf9uwRowxzNqRFe/amw=="], + + "vue": ["vue@3.5.41", "", { "dependencies": { "@vue/compiler-dom": "3.5.41", "@vue/compiler-sfc": "3.5.41", "@vue/runtime-dom": "3.5.41", "@vue/server-renderer": "3.5.41", "@vue/shared": "3.5.41" }, "peerDependencies": { "typescript": "*" }, "optionalPeers": ["typescript"] }, "sha512-2laE0p+aK+/AOPG/XL/WepOs/GlK755LJ1XECi9kDUrz1FKNw8rb2Xzlw9JS1rqEV55nb0ttsKxVlTCcd+R5cg=="], + + "zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="], + } +} diff --git a/docs/config/.vitepress/config.mts b/docs/config/.vitepress/config.mts new file mode 100644 index 00000000..4c252f22 --- /dev/null +++ b/docs/config/.vitepress/config.mts @@ -0,0 +1,30 @@ +import { defineConfig } from 'vitepress' + +// https://vitepress.dev/reference/site-config +export default defineConfig({ + srcDir: "../content", + + title: "OpenAdmin", + description: "Admin panel for python", + themeConfig: { + // https://vitepress.dev/reference/default-theme-config + nav: [ + { text: 'Home', link: '/' }, + { text: 'Examples', link: '/markdown-examples' } + ], + + sidebar: [ + { + text: 'Examples', + items: [ + { text: 'Markdown Examples', link: '/markdown-examples' }, + { text: 'Runtime API Examples', link: '/api-examples' } + ] + } + ], + + socialLinks: [ + { icon: 'github', link: 'https://github.com/vuejs/vitepress' } + ] + } +}) diff --git a/docs/config/.vitepress/theme/index.ts b/docs/config/.vitepress/theme/index.ts new file mode 100644 index 00000000..def4cfc8 --- /dev/null +++ b/docs/config/.vitepress/theme/index.ts @@ -0,0 +1,17 @@ +// https://vitepress.dev/guide/custom-theme +import { h } from 'vue' +import type { Theme } from 'vitepress' +import DefaultTheme from 'vitepress/theme' +import './style.css' + +export default { + extends: DefaultTheme, + Layout: () => { + return h(DefaultTheme.Layout, null, { + // https://vitepress.dev/guide/extending-default-theme#layout-slots + }) + }, + enhanceApp({ app, router, siteData }) { + // ... + } +} satisfies Theme diff --git a/docs/config/.vitepress/theme/style.css b/docs/config/.vitepress/theme/style.css new file mode 100644 index 00000000..32d3997d --- /dev/null +++ b/docs/config/.vitepress/theme/style.css @@ -0,0 +1,131 @@ +/** + * Customize default theme styling by overriding CSS variables: + * https://github.com/vuejs/vitepress/blob/main/src/client/theme-default/styles/vars.css + */ + +/** + * Colors + * + * Each colors have exact same color scale system with 3 levels of solid + * colors with different brightness, and 1 soft color. + * + * - `XXX-1`: The most solid color used mainly for colored text. It must + * satisfy the contrast ratio against when used on top of `XXX-soft`. + * + * - `XXX-2`: The color used mainly for hover state of the button. + * + * - `XXX-3`: The color for solid background, such as bg color of the button. + * It must satisfy the contrast ratio with pure white (#ffffff) text on + * top of it. + * + * - `XXX-soft`: The color used for subtle background such as custom container + * or badges. It must satisfy the contrast ratio when putting `XXX-1` colors + * on top of it. + * + * The soft color must be semi transparent alpha channel. This is crucial + * because it allows adding multiple "soft" colors on top of each other + * to create an accent, such as when having inline code block inside + * custom containers. + * + * - `default`: The color used purely for subtle indication without any + * special meanings attached to it such as bg color for menu hover state. + * + * - `brand`: Used for primary brand colors, such as link text, button with + * brand theme, etc. + * + * - `tip`: Used to indicate useful information. The default theme uses the + * brand color for this by default. + * + * - `warning`: Used to indicate warning to the users. Used in custom + * container, badges, etc. + * + * - `danger`: Used to show error, or dangerous message to the users. Used + * in custom container, badges, etc. + * -------------------------------------------------------------------------- */ + +:root { + --vp-c-default-1: var(--vp-c-gray-1); + --vp-c-default-2: var(--vp-c-gray-2); + --vp-c-default-3: var(--vp-c-gray-3); + --vp-c-default-soft: var(--vp-c-gray-soft); + + --vp-c-brand-1: var(--vp-c-indigo-1); + --vp-c-brand-2: var(--vp-c-indigo-2); + --vp-c-brand-3: var(--vp-c-indigo-3); + --vp-c-brand-soft: var(--vp-c-indigo-soft); + + --vp-c-tip-1: var(--vp-c-brand-1); + --vp-c-tip-2: var(--vp-c-brand-2); + --vp-c-tip-3: var(--vp-c-brand-3); + --vp-c-tip-soft: var(--vp-c-brand-soft); + + --vp-c-warning-1: var(--vp-c-yellow-1); + --vp-c-warning-2: var(--vp-c-yellow-2); + --vp-c-warning-3: var(--vp-c-yellow-3); + --vp-c-warning-soft: var(--vp-c-yellow-soft); + + --vp-c-danger-1: var(--vp-c-red-1); + --vp-c-danger-2: var(--vp-c-red-2); + --vp-c-danger-3: var(--vp-c-red-3); + --vp-c-danger-soft: var(--vp-c-red-soft); +} + +/** + * Component: Button + * -------------------------------------------------------------------------- */ + +:root { + --vp-button-brand-border: transparent; + --vp-button-brand-text: var(--vp-c-white); + --vp-button-brand-bg: var(--vp-c-brand-3); + --vp-button-brand-hover-border: transparent; + --vp-button-brand-hover-text: var(--vp-c-white); + --vp-button-brand-hover-bg: var(--vp-c-brand-2); + --vp-button-brand-active-border: transparent; + --vp-button-brand-active-text: var(--vp-c-white); + --vp-button-brand-active-bg: var(--vp-c-brand-1); +} + +/** + * Component: Home + * -------------------------------------------------------------------------- */ + +:root { + --vp-home-hero-name-color: transparent; + --vp-home-hero-name-background: -webkit-linear-gradient( + 120deg, + #bd34fe 30%, + #41d1ff + ); + + --vp-home-hero-image-background-image: linear-gradient( + -45deg, + #bd34fe 50%, + #47caff 50% + ); + --vp-home-hero-image-filter: blur(44px); +} + +@media (min-width: 640px) { + :root { + --vp-home-hero-image-filter: blur(56px); + } +} + +@media (min-width: 960px) { + :root { + --vp-home-hero-image-filter: blur(68px); + } +} + +/** + * Component: Custom Block + * -------------------------------------------------------------------------- */ + +:root { + --vp-custom-block-tip-border: transparent; + --vp-custom-block-tip-text: var(--vp-c-text-1); + --vp-custom-block-tip-bg: var(--vp-c-brand-soft); + --vp-custom-block-tip-code-bg: var(--vp-c-brand-soft); +} + diff --git a/docs/content/api-examples.md b/docs/content/api-examples.md new file mode 100644 index 00000000..6bd8bb5c --- /dev/null +++ b/docs/content/api-examples.md @@ -0,0 +1,49 @@ +--- +outline: deep +--- + +# Runtime API Examples + +This page demonstrates usage of some of the runtime APIs provided by VitePress. + +The main `useData()` API can be used to access site, theme, and page data for the current page. It works in both `.md` and `.vue` files: + +```md + + +## Results + +### Theme Data +
{{ theme }}
+ +### Page Data +
{{ page }}
+ +### Page Frontmatter +
{{ frontmatter }}
+``` + + + +## Results + +### Theme Data +
{{ theme }}
+ +### Page Data +
{{ page }}
+ +### Page Frontmatter +
{{ frontmatter }}
+ +## More + +Check out the documentation for the [full list of runtime APIs](https://vitepress.dev/reference/runtime-api#usedata). diff --git a/docs/content/index.md b/docs/content/index.md new file mode 100644 index 00000000..076f79af --- /dev/null +++ b/docs/content/index.md @@ -0,0 +1,25 @@ +--- +# https://vitepress.dev/reference/default-theme-home-page +layout: home + +hero: + name: "OpenAdmin" + text: "Admin panel for python" + tagline: My great project tagline + actions: + - theme: brand + text: Markdown Examples + link: /markdown-examples + - theme: alt + text: API Examples + link: /api-examples + +features: + - title: Feature A + details: Lorem ipsum dolor sit amet, consectetur adipiscing elit + - title: Feature B + details: Lorem ipsum dolor sit amet, consectetur adipiscing elit + - title: Feature C + details: Lorem ipsum dolor sit amet, consectetur adipiscing elit +--- + diff --git a/docs/content/markdown-examples.md b/docs/content/markdown-examples.md new file mode 100644 index 00000000..f9258a55 --- /dev/null +++ b/docs/content/markdown-examples.md @@ -0,0 +1,85 @@ +# Markdown Extension Examples + +This page demonstrates some of the built-in markdown extensions provided by VitePress. + +## Syntax Highlighting + +VitePress provides Syntax Highlighting powered by [Shiki](https://github.com/shikijs/shiki), with additional features like line-highlighting: + +**Input** + +````md +```js{4} +export default { + data () { + return { + msg: 'Highlighted!' + } + } +} +``` +```` + +**Output** + +```js{4} +export default { + data () { + return { + msg: 'Highlighted!' + } + } +} +``` + +## Custom Containers + +**Input** + +```md +::: info +This is an info box. +::: + +::: tip +This is a tip. +::: + +::: warning +This is a warning. +::: + +::: danger +This is a dangerous warning. +::: + +::: details +This is a details block. +::: +``` + +**Output** + +::: info +This is an info box. +::: + +::: tip +This is a tip. +::: + +::: warning +This is a warning. +::: + +::: danger +This is a dangerous warning. +::: + +::: details +This is a details block. +::: + +## More + +Check out the documentation for the [full list of markdown extensions](https://vitepress.dev/guide/markdown). diff --git a/docs/package.json b/docs/package.json new file mode 100644 index 00000000..c3cee989 --- /dev/null +++ b/docs/package.json @@ -0,0 +1,12 @@ +{ + "dependencies": {}, + "devDependencies": { + "vitepress": "^2.0.0-alpha.19", + "vue": "^3.5.41" + }, + "scripts": { + "dev": "vitepress dev config", + "build": "vitepress build config", + "preview": "vitepress preview config" + } +} \ No newline at end of file From 2d9248e050f3502f39629618a18ec2a258465124 Mon Sep 17 00:00:00 2001 From: Mykyta Date: Sat, 22 Aug 2026 13:05:24 +0200 Subject: [PATCH 2/9] feat: add run dev docs --- Makefile | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Makefile b/Makefile index 559e6cf6..0d26eeae 100644 --- a/Makefile +++ b/Makefile @@ -62,6 +62,9 @@ check: check/format check/lint check/typing check/cves check/security check/unus dev/client: @ cd client && bun run dev +dev/docs: + @ cd docs && bun run dev + dev/example: @ cd client && bun run build @ PYTHONPATH=. uv run fastapi dev examples/main.py --host 0.0.0.0 --port $${PORT:-8000} --reload From 4393b6889edd427ae8e6ebe3f5431216b3e9b896 Mon Sep 17 00:00:00 2001 From: Mykyta Date: Sat, 22 Aug 2026 13:06:34 +0200 Subject: [PATCH 3/9] ref --- docs/.gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/.gitignore b/docs/.gitignore index a547bf36..f8a2596c 100644 --- a/docs/.gitignore +++ b/docs/.gitignore @@ -22,3 +22,4 @@ dist-ssr *.njsproj *.sln *.sw? +config/.vitepress/cache \ No newline at end of file From ae37e39a8e875ea24adbf394788a83945929c66e Mon Sep 17 00:00:00 2001 From: Mykyta Date: Sat, 22 Aug 2026 13:08:37 +0200 Subject: [PATCH 4/9] ref --- docs/content/api-examples.md | 49 ------------------ docs/content/markdown-examples.md | 85 ------------------------------- 2 files changed, 134 deletions(-) delete mode 100644 docs/content/api-examples.md delete mode 100644 docs/content/markdown-examples.md diff --git a/docs/content/api-examples.md b/docs/content/api-examples.md deleted file mode 100644 index 6bd8bb5c..00000000 --- a/docs/content/api-examples.md +++ /dev/null @@ -1,49 +0,0 @@ ---- -outline: deep ---- - -# Runtime API Examples - -This page demonstrates usage of some of the runtime APIs provided by VitePress. - -The main `useData()` API can be used to access site, theme, and page data for the current page. It works in both `.md` and `.vue` files: - -```md - - -## Results - -### Theme Data -
{{ theme }}
- -### Page Data -
{{ page }}
- -### Page Frontmatter -
{{ frontmatter }}
-``` - - - -## Results - -### Theme Data -
{{ theme }}
- -### Page Data -
{{ page }}
- -### Page Frontmatter -
{{ frontmatter }}
- -## More - -Check out the documentation for the [full list of runtime APIs](https://vitepress.dev/reference/runtime-api#usedata). diff --git a/docs/content/markdown-examples.md b/docs/content/markdown-examples.md deleted file mode 100644 index f9258a55..00000000 --- a/docs/content/markdown-examples.md +++ /dev/null @@ -1,85 +0,0 @@ -# Markdown Extension Examples - -This page demonstrates some of the built-in markdown extensions provided by VitePress. - -## Syntax Highlighting - -VitePress provides Syntax Highlighting powered by [Shiki](https://github.com/shikijs/shiki), with additional features like line-highlighting: - -**Input** - -````md -```js{4} -export default { - data () { - return { - msg: 'Highlighted!' - } - } -} -``` -```` - -**Output** - -```js{4} -export default { - data () { - return { - msg: 'Highlighted!' - } - } -} -``` - -## Custom Containers - -**Input** - -```md -::: info -This is an info box. -::: - -::: tip -This is a tip. -::: - -::: warning -This is a warning. -::: - -::: danger -This is a dangerous warning. -::: - -::: details -This is a details block. -::: -``` - -**Output** - -::: info -This is an info box. -::: - -::: tip -This is a tip. -::: - -::: warning -This is a warning. -::: - -::: danger -This is a dangerous warning. -::: - -::: details -This is a details block. -::: - -## More - -Check out the documentation for the [full list of markdown extensions](https://vitepress.dev/guide/markdown). From 8b0fcdacb5b941fd8e9a3a8fd168ba38ad410f2a Mon Sep 17 00:00:00 2001 From: Mykyta Date: Sat, 22 Aug 2026 13:09:02 +0200 Subject: [PATCH 5/9] ref --- docs/config/.vitepress/config.mts | 2 -- 1 file changed, 2 deletions(-) diff --git a/docs/config/.vitepress/config.mts b/docs/config/.vitepress/config.mts index 4c252f22..a3af4c84 100644 --- a/docs/config/.vitepress/config.mts +++ b/docs/config/.vitepress/config.mts @@ -17,8 +17,6 @@ export default defineConfig({ { text: 'Examples', items: [ - { text: 'Markdown Examples', link: '/markdown-examples' }, - { text: 'Runtime API Examples', link: '/api-examples' } ] } ], From 5aa248bee79d708048e841bad866c7a786156419 Mon Sep 17 00:00:00 2001 From: Mykyta Date: Sat, 22 Aug 2026 13:19:26 +0200 Subject: [PATCH 6/9] feat: added content --- docs/content/auth/index.md | 96 ++++++++++++ docs/content/components/action.md | 103 +++++++++++++ docs/content/components/area-chart.md | 24 +++ docs/content/components/bar-chart.md | 70 +++++++++ docs/content/components/form.md | 111 ++++++++++++++ docs/content/components/index.md | 38 +++++ docs/content/components/line-chart.md | 24 +++ docs/content/components/markdown.md | 84 ++++++++++ docs/content/components/pie-chart.md | 77 ++++++++++ docs/content/components/stat.md | 80 ++++++++++ docs/content/components/table.md | 144 ++++++++++++++++++ docs/content/cookbook/implementing-auth.md | 93 +++++++++++ .../implementing-form-with-reference.md | 88 +++++++++++ docs/content/cookbook/implementing-table.md | 117 ++++++++++++++ docs/content/introduction/getting-started.md | 96 ++++++++++++ .../content/introduction/what-is-openadmin.md | 58 +++++++ 16 files changed, 1303 insertions(+) create mode 100644 docs/content/auth/index.md create mode 100644 docs/content/components/action.md create mode 100644 docs/content/components/area-chart.md create mode 100644 docs/content/components/bar-chart.md create mode 100644 docs/content/components/form.md create mode 100644 docs/content/components/index.md create mode 100644 docs/content/components/line-chart.md create mode 100644 docs/content/components/markdown.md create mode 100644 docs/content/components/pie-chart.md create mode 100644 docs/content/components/stat.md create mode 100644 docs/content/components/table.md create mode 100644 docs/content/cookbook/implementing-auth.md create mode 100644 docs/content/cookbook/implementing-form-with-reference.md create mode 100644 docs/content/cookbook/implementing-table.md create mode 100644 docs/content/introduction/getting-started.md create mode 100644 docs/content/introduction/what-is-openadmin.md diff --git a/docs/content/auth/index.md b/docs/content/auth/index.md new file mode 100644 index 00000000..878c06ff --- /dev/null +++ b/docs/content/auth/index.md @@ -0,0 +1,96 @@ +# Authentication + +`AdminAuth` holds three hooks — login, authenticate, and logout — and `AdminPanel` wires them into the panel's routes for you. + +```python +from openadmin.fastapi import AdminAuth + +auth = AdminAuth() +``` + +::: warning No protection by default +An `AdminAuth()` instance's three hooks are no-op stubs until you override them: `login` and `logout` do nothing, and `authenticate` never raises — so it lets every request through. Passing `auth=None` to `AdminPanel` (the default) is equivalent: no `/auth/*` routes are even mounted, and `/api/*` is completely open. Either way, nothing is actually gated until you decorate all three hooks yourself. +::: + +## The three hooks + +```python +@auth.login() +def login(req: Request, login_req: LoginReq) -> None: ... + +@auth.authenticate() +def authenticate(req: Request) -> None: ... + +@auth.logout() +def logout(req: Request) -> None: ... +``` + +Each decorator just stores the function you give it — it doesn't wrap or alter it, so the function can still be called or tested directly like any other. Each hook may be sync or async (`None | Awaitable[None]`). + +- **`login_func(req, login_req)`** — receives a `LoginReq` (`{username: str, password: str}`, a pydantic model). Raise an `HTTPException` to reject the credentials; return normally to accept them. This is where you'd typically write something into `req.session`. +- **`authenticate_func(req)`** — runs as a dependency on **every** request under `/api/*`, i.e. every stat, table, form, action, chart, and markdown endpoint on every page. Raise an `HTTPException` (typically 401) to reject the request; return normally to allow it. +- **`logout_func(req)`** — typically clears `req.session`. It runs behind `authenticate_func` itself, so a caller must already be authenticated to log out. + +## How `AdminPanel` wires them up + +Passing `auth=` to `AdminPanel(...)` does three things: + +1. Mounts `POST /auth/login`, calling your `login_func` and returning `204 No Content` on success. +2. Mounts `POST /auth/logout`, calling your `logout_func`, itself gated behind `authenticate_func`. +3. Adds `authenticate_func` as a router-level dependency on the entire `/api` router — so it runs before any component endpoint, panel-wide, with no per-page or per-component opt-in needed. + +The frontend's static assets (served at `/`) and the login endpoint itself are intentionally not gated, since a client needs to load the login screen and call `/auth/login` before it has anything to authenticate with. + +## Example + +```python +# admin/auth.py +from fastapi import HTTPException, Request, status + +from openadmin.fastapi import AdminAuth, LoginReq + +auth = AdminAuth() + + +@auth.login() +def login(req: Request, login_req: LoginReq) -> None: + if login_req.username == "admin" and login_req.password == "admin": + req.session.update({"token": "admin-token"}) + else: + raise HTTPException(status.HTTP_401_UNAUTHORIZED, "Invalid username or password") + + +@auth.authenticate() +def authenticate(req: Request) -> None: + if req.session.get("token") != "admin-token": + raise HTTPException(status.HTTP_401_UNAUTHORIZED, "Unauthorized") + + +@auth.logout() +def logout(req: Request) -> None: + req.session.clear() +``` + +`req.session` comes from Starlette's `SessionMiddleware`, added on the *outer* application — not on `admin.app` — since middleware on the outer app also covers requests routed into the mounted sub-app: + +```python +# main.py +from fastapi import FastAPI +from starlette.middleware.sessions import SessionMiddleware + +from openadmin.fastapi import AdminPanel + +from .admin.auth import auth + +app = FastAPI() +app.add_middleware(SessionMiddleware, secret_key="change-me") + +admin = AdminPanel("My Admin", auth=auth) +app.mount("/admin", admin.app) +``` + +::: danger +Cookie-based sessions are only as secure as `secret_key`. Never hardcode it — load it from an environment variable or secret store — and compare credentials with a real user store and hashed passwords, not the plaintext check shown above. +::: + +See [Implementing Auth](/cookbook/implementing-auth) for a full step-by-step recipe. diff --git a/docs/content/components/action.md b/docs/content/components/action.md new file mode 100644 index 00000000..0e2c8beb --- /dev/null +++ b/docs/content/components/action.md @@ -0,0 +1,103 @@ +# Action + +An action is a one-off button that calls an endpoint — standalone on a page, or attached to a table row. + +```python +@page.action("Delete Book", method="delete", description="Remove a book by ID") +async def delete_book(session: AsyncSessionDep, book_id: int = Query(...)) -> spec.Action: + book = await session.get(models.Book, book_id) + if book: + await session.delete(book) + await session.commit() + return {"message": f"Deleted book #{book_id}"} +``` + +## Decorator + +```python +page.action( + name: str, + *, + method: spec.HttpMethod = "post", + description: str | None = None, + is_hidden: bool = False, + icon: spec.Icon | None = None, + color: spec.Color | None = None, +) +``` + +The decorated function is registered at ` //action/`, using whichever HTTP method you pass — `get`, `post` (the default), `put`, `patch`, `delete`, or `head`. + +## Return value + +```python +class ActionResponse(TypedDict): + icon: NotRequired[Icon] + color: NotRequired[Color] + toast: NotRequired[str] + table: NotRequired[dict | object] + message: NotRequired[str] + +type Action = ActionResponse | None | str +``` + +Same shape as a [form](/components/form)'s response: return `None` for a silent success, a string as shorthand for `message`, or a full `ActionResponse` dict with `toast`/`message`/`table` for richer feedback. + +```python +@page.action("Ping Service", method="get", icon="activity", color="green") +async def ping_service(target: str = Query("api")) -> spec.Action: + return { + "toast": f"{target} responded in 12ms", + "message": f"Pinged '{target}' — reachable", + } +``` + +## Parameters by HTTP method + +Actions take parameters the same way any FastAPI endpoint does — `Query` for `get`/`delete`, a pydantic model or `Body(...)` for `post`/`put`/`patch`, `Form(...)` fields when appropriate: + +```python +@page.action("Rotate API Key", method="patch", icon="key", color="amber") +async def rotate_api_key( + key_name: str = Form(..., description="Key to rotate"), + expires_in_days: int = Form(30, description="Validity period for the new key"), +) -> spec.Action: + ... +``` + +## Attaching an action to a table row + +Give an action `is_hidden=True` when its only purpose is to be triggered from a row, then reference it from that row's `__actions__` with `reference_action`: + +```python +from openadmin.fastapi import reference_action + +@page.action("Delete Author", is_hidden=True) +async def delete_author(id: str) -> spec.Action: + ... + return {"toast": f"User with id {id} deleted"} + + +@page.table("All Authors") +async def get_all_authors(...) -> spec.Table: + return { + "data": [ + { + "id": author.id, + "name": ..., + "__actions__": [ + { + "label": "Delete this user", + "action": reference_action(delete_author), + "query": {"id": author.id}, + "color": "red", + "icon": "trash", + }, + ], + } + for author in authors + ], + } +``` + +`reference_action` reads the ID that the `@page.action(...)` decorator stamped onto `delete_author`, so `delete_author` must already be decorated before it's referenced this way — a plain top-to-bottom ordering requirement, not special behavior. See [Implementing a Table](/cookbook/implementing-table) for the full recipe. diff --git a/docs/content/components/area-chart.md b/docs/content/components/area-chart.md new file mode 100644 index 00000000..f5564d63 --- /dev/null +++ b/docs/content/components/area-chart.md @@ -0,0 +1,24 @@ +# Area Chart + +::: warning Not yet available +`AreaChart` is defined in `openadmin.spec` and included in the `Component` union, but `AdminPage` doesn't expose an `area_chart(...)` decorator yet — the implementation is present in the source but commented out. This page documents the spec shape as a preview of what's coming; there's no way to register one today. +::: + +## Spec shape + +```python +class AreaChartComponent(TypedDict): + type: Literal["area-chart"] + id: str + name: str + description: str | None + refresh: int | None + method: HttpMethod + form: JsonSchema | None + body: JsonSchema | None + query: JsonSchema | None + +type AreaChart = AreaChartComponent +``` + +In the meantime, a time-series trend can be approximated with a [bar chart](/components/bar-chart). diff --git a/docs/content/components/bar-chart.md b/docs/content/components/bar-chart.md new file mode 100644 index 00000000..ed555696 --- /dev/null +++ b/docs/content/components/bar-chart.md @@ -0,0 +1,70 @@ +# Bar Chart + +A bar chart for categorical data — one bar per category, with an optional legend. + +```python +@page.bar_chart("Books per Genre", description="Absolute book count for each genre") +async def get_books_per_genre(session: AsyncSessionDep): + ... + return [{"label": row.name, "value": row.count} for row in result] +``` + +## Decorator + +```python +page.bar_chart( + name: str, + *, + description: str | None = None, + icon: spec.Icon | None = None, + color: spec.Color | None = None, + caption: str | None = None, + caption_description: str | None = None, + caption_icon: spec.Icon | None = None, + config: dict[str, spec.BarChartConfigValue] | None = None, + data_key: str | None = None, + refresh: timedelta | None = None, +) +``` + +The decorated function is registered as `GET //bar-chart/`. + +## Return value + +```python +type BarChartData = ( + list[dict[str, int | float | str]] + | list[dict[Literal["data", "value"], int | float | str]] + | object +) + +class BarChartResponce(TypedDict): + config: NotRequired[dict[str, BarChartConfigValue]] + icon: NotRequired[Icon] + color: NotRequired[Color] + refresh: NotRequired[int | None] + data: BarChartData + +type BarChart = BarChartData | BarChartResponce +``` + +The simplest shape is a bare list of `{"label": ..., "value": ...}` dicts: + +```python +return [{"label": "Fiction", "value": 128}, {"label": "History", "value": 47}] +``` + +Return a `BarChartResponce` dict instead when you need to override `config`, `icon`, `color`, or `refresh` per response. + +## Config and legend + +`config` maps a data key to display metadata used for the legend: + +```python +class BarChartConfigValue(TypedDict): + name: NotRequired[str] + color: NotRequired[Color] + icon: NotRequired[Icon] +``` + +`data_key` selects which field in your row dicts holds the series value, for shapes that don't already use the default `label`/`value` keys. diff --git a/docs/content/components/form.md b/docs/content/components/form.md new file mode 100644 index 00000000..797a7f01 --- /dev/null +++ b/docs/content/components/form.md @@ -0,0 +1,111 @@ +# Form + +A form submits to your own endpoint using whatever request shape you declare — a pydantic body, `Query`/`Form`/`Body` parameters, or a mix. + +```python +class AddBookBody(BaseModel): + title: str + author_id: int + published_year: int | None = None + + +@page.form("Add Book", description="Add a new book to the catalog") +async def add_book(body: AddBookBody, session: AsyncSessionDep) -> spec.Form: + book = models.Book(**body.model_dump()) + session.add(book) + await session.commit() + await session.refresh(book) + return {"message": f"Added book '{book.title}'"} +``` + +## Decorator + +```python +page.form( + name: str, + *, + method: spec.HttpMethod = "post", + fields: dict[str, spec.FieldConfig] | None = None, + description: str | None = None, + is_hidden: bool = False, + icon: spec.Icon | None = None, + color: spec.Color | None = None, +) +``` + +The decorated function is registered at ` //form/`, using whichever HTTP method you pass — `get`, `post` (the default), `put`, `patch`, `delete`, or `head`. + +## Return value + +```python +class FormResponse(TypedDict): + icon: NotRequired[Icon] + color: NotRequired[Color] + toast: NotRequired[str] + table: NotRequired[dict | object] + message: NotRequired[str] + +type Form = FormResponse | None | str +``` + +Return `None` for a bare success with no message, a plain string as a shorthand for `message`, or a `FormResponse` dict for full control: + +```python +@page.form("Create Webhook", method="post") +async def create_webhook(body: WebhookBody) -> spec.Form: + return { + "icon": "webhook", + "color": "teal", + "toast": "Webhook created", + "message": f"Would create a webhook for '{body.event}' -> {body.url}", + "table": {"id": 1, "url": body.url, "event": body.event}, + } +``` + +`toast` is a brief transient notification; `message` is shown inline after submission; `table` is an arbitrary JSON payload echoed back to the user (typically the created or affected record) — it's for feedback, not for updating other components. + +## Reference fields + +`fields` maps a request field name to a `FieldConfig`, turning a plain input into a picker sourced from another table: + +```python +class FieldConfig(TypedDict): + reference: NotRequired[str | None] + reference_field: NotRequired[str] + icon: NotRequired[Icon] + color: NotRequired[Color] +``` + +```python +from openadmin.fastapi import reference_table + +@page.form( + "Add Author", + fields={ + "friend": { + "reference": reference_table(get_all_authors), + "reference_field": "id", + "icon": "user", + "color": "blue", + } + }, +) +async def add_author(body: AddAuthorBody, session: AsyncSessionDep) -> spec.Form: ... +``` + +`reference` is the ID of a table (obtained with `reference_table`, so `get_all_authors` must already be decorated with `@page.table(...)`); `reference_field` is the column of that table's rows to submit as the field's value. See [Implementing a Form with a Reference](/cookbook/implementing-form-with-reference) for the full walkthrough. + +## Other HTTP methods + +```python +@page.form("Rename Environment", method="patch") +async def rename_environment( + environment_id: int = Query(..., description="Environment to rename"), + new_name: str = Body(..., embed=True, description="New environment name"), +) -> str: + return f"Renamed environment #{environment_id} to '{new_name}'" +``` + +## Hiding a form + +`is_hidden=True` keeps the endpoint and its spec entry but removes it from the visible page, the same as with tables and actions. diff --git a/docs/content/components/index.md b/docs/content/components/index.md new file mode 100644 index 00000000..0db833b5 --- /dev/null +++ b/docs/content/components/index.md @@ -0,0 +1,38 @@ +# Components + +A component is a single widget on an `AdminPage`. Every component is registered with a decorator, and every decorator does the same two things: it registers a real FastAPI route, and it records the widget's metadata into the page's spec. + +| Component | Decorator | Purpose | +| --- | --- | --- | +| [Stat](/components/stat) | `@page.stat(...)` | A single value — a count, a percentage, a boolean | +| [Table](/components/table) | `@page.table(...)` | A paginated, searchable grid with optional per-row actions | +| [Form](/components/form) | `@page.form(...)` | A structured form that submits to your own endpoint | +| [Action](/components/action) | `@page.action(...)` | A one-off button that calls an endpoint | +| [Bar Chart](/components/bar-chart) | `@page.bar_chart(...)` | A categorical bar chart | +| [Pie Chart](/components/pie-chart) | `@page.pie_chart(...)` | A proportional breakdown chart | +| [Markdown](/components/markdown) | `@page.markdown(...)` | A static or dynamic rich text block | +| [Area Chart](/components/area-chart) | — not yet available | Defined in the spec, no decorator yet | +| [Line Chart](/components/line-chart) | — not yet available | Defined in the spec, no decorator yet | + +## Shared behavior + +A few things apply to every component, not just one: + +- **Parameters are inferred from the function signature.** `Query(...)`, `Body(...)`, `Form(...)`, pydantic models, and `Depends(...)` all work exactly as they do in any FastAPI route. OpenAdmin walks the resolved dependency tree and turns the query, body, and form parameters into JSON Schema, which becomes part of the component's spec. +- **`icon` and `color`** accept any value from the shared `Icon` and `Color` literal types in `openadmin.spec` (Lucide icon names and Tailwind-style color names, respectively). +- **`refresh`** (on `stat`, `table`, `markdown`, `bar_chart`, `pie_chart`) takes a `datetime.timedelta`. When set, the frontend polls the component's endpoint on that interval. +- **`is_hidden`** (on `table`, `action`, `form`) registers the endpoint and includes it in the spec, but keeps it out of the visible page layout. This is the mechanism behind row actions and reference fields — see [Implementing a Table](/cookbook/implementing-table) and [Implementing a Form with a Reference](/cookbook/implementing-form-with-reference). +- **`method`** (on `action`, `form`) is any of `get`, `post`, `put`, `patch`, `delete`, `head`, and maps directly to the matching FastAPI router method. + +## Referencing one component from another + +Two helpers let one component point at another by its generated ID, instead of hardcoding route paths: + +```python +from openadmin.fastapi import reference_action, reference_table +``` + +- `reference_table(table_func)` — returns the ID of a `@page.table(...)`-decorated function. Used in a form field's `reference` to source a value picker from that table's rows. +- `reference_action(action_func)` — returns the ID of a `@page.action(...)`-decorated function. Used in a table row's `__actions__` to attach that action as a row button. + +Both simply read an attribute (`__openadmin_table_id__` / `__openadmin_action_id__`) that the decorator stamped onto the function, so the referenced function must already be decorated — but not necessarily called — before you reference it. diff --git a/docs/content/components/line-chart.md b/docs/content/components/line-chart.md new file mode 100644 index 00000000..f65629cd --- /dev/null +++ b/docs/content/components/line-chart.md @@ -0,0 +1,24 @@ +# Line Chart + +::: warning Not yet available +`LineChart` is defined in `openadmin.spec` and included in the `Component` union, but `AdminPage` doesn't expose a `line_chart(...)` decorator yet — the implementation is present in the source but commented out. This page documents the spec shape as a preview of what's coming; there's no way to register one today. +::: + +## Spec shape + +```python +class LineChartComponent(TypedDict): + type: Literal["line-chart"] + id: str + name: str + description: str | None + refresh: int | None + method: HttpMethod + form: JsonSchema | None + body: JsonSchema | None + query: JsonSchema | None + +type LineChart = LineChartComponent +``` + +In the meantime, a trend can be approximated with a [bar chart](/components/bar-chart). diff --git a/docs/content/components/markdown.md b/docs/content/components/markdown.md new file mode 100644 index 00000000..cabcb42f --- /dev/null +++ b/docs/content/components/markdown.md @@ -0,0 +1,84 @@ +# Markdown + +A markdown component renders static or dynamic rich text on a page — guides, changelogs, release notes, or any contextual copy. + +```python +@page.markdown("Release Notes", icon="scroll-text", color="indigo") +def release_notes() -> spec.Markdown: + return """ +# 1.4.0 + +- Added CSV export to tables +- Fixed pagination off-by-one on the last page +""" +``` + +## Decorator + +```python +page.markdown( + name: str, + *, + description: str | None = None, + color: spec.Color | None = None, + icon: spec.Icon | None = None, + refresh: timedelta | None = None, +) +``` + +The decorated function is registered as `GET //markdown/`. + +## Return value + +```python +type MarkdownContent = str + +class MarkdownResponse(TypedDict): + icon: NotRequired[Icon] + color: NotRequired[Color] + refresh: NotRequired[int | None] + content: MarkdownContent + +type Markdown = MarkdownContent | MarkdownResponse +``` + +Return a bare string for the common case, or a `MarkdownResponse` dict to override `icon`/`color`/`refresh` per response: + +```python +@page.markdown("Status", refresh=timedelta(seconds=30)) +async def status_markdown(session: AsyncSessionDep) -> spec.MarkdownResponse: + healthy = await check_health(session) + return { + "content": "All systems operational." if healthy else "**Degraded** — investigating.", + "color": "emerald" if healthy else "amber", + } +``` + +## Supported syntax + +The renderer supports GitHub-flavored Markdown plus a handful of inline HTML extras: + +- Headings (`#` through `######`), emphasis (`**bold**`, `*italic*`, `~~strikethrough~~`), `highlight` +- Inline code and fenced code blocks with syntax highlighting (` ```python `, ` ```bash `, ` ```json `, ...) +- Inline, reference-style, and bare autolinks +- Superscript/subscript (``, ``), ``, `` +- Ordered, unordered, and task lists (`- [x]`), including nesting +- Definition lists (`
`/`
`/`
`) +- Blockquotes, including nested blockquotes +- Tables, with left/center/right column alignment +- Images (`![alt](src)`) +- Disclosures (`
`/``) + +```markdown +## Task lists + +- [x] Write the first draft +- [ ] Ship it + +
+Click to expand + +Hidden by default. + +
+``` diff --git a/docs/content/components/pie-chart.md b/docs/content/components/pie-chart.md new file mode 100644 index 00000000..448d9e86 --- /dev/null +++ b/docs/content/components/pie-chart.md @@ -0,0 +1,77 @@ +# Pie Chart + +A pie chart for showing a proportional breakdown across categories. + +```python +@page.pie_chart("Genre Distribution", description="Share of books across all genres") +async def get_genre_distribution(session: AsyncSessionDep) -> spec.PieChart: + ... + return {"data": [{"name": row.name, "value": row.count} for row in result]} +``` + +## Decorator + +```python +page.pie_chart( + name: str, + *, + description: str | None = None, + config: dict[str, spec.PieChartConfigValue] | None = None, + icon: spec.Icon | None = None, + name_key: str | None = None, + value_key: str | None = None, + color: spec.Color | None = None, + caption: str | None = None, + caption_description: str | None = None, + caption_icon: spec.Icon | None = None, + refresh: timedelta | None = None, +) +``` + +The decorated function is registered as `GET //pie-chart/`. + +## Return value + +```python +type PieChartData = ( + list[dict[str, int | float | str]] + | list[dict[Literal["name", "value"], int | float | str]] + | object +) + +class PieChartResponce(TypedDict): + config: NotRequired[dict[str, PieChartConfigValue]] + icon: NotRequired[Icon] + color: NotRequired[Color] + data: PieChartData + refresh: NotRequired[int | None] + +type PieChart = PieChartData | PieChartResponce +``` + +There are two ways to shape the data. Use the default `name`/`value` keys and return a bare list: + +```python +@page.pie_chart("Genre Distribution") +async def get_genre_distribution(session: AsyncSessionDep) -> spec.PieChart: + return {"data": [{"name": g.name, "value": count} for g, count in rows]} +``` + +Or use your own field names and tell the chart which ones to read with `name_key`/`value_key`: + +```python +@page.pie_chart("Genre Distribution", name_key="genre", value_key="count") +async def get_genre_distribution(session: AsyncSessionDep): + return [{"genre": g.name, "count": count} for g, count in rows] +``` + +## Config and legend + +`config` maps a data key to display metadata used for the legend, the same shape as on [bar charts](/components/bar-chart): + +```python +class PieChartConfigValue(TypedDict): + name: NotRequired[str] + color: NotRequired[Color] + icon: NotRequired[Icon] +``` diff --git a/docs/content/components/stat.md b/docs/content/components/stat.md new file mode 100644 index 00000000..7d963b73 --- /dev/null +++ b/docs/content/components/stat.md @@ -0,0 +1,80 @@ +# Stat + +A stat displays a single value — a count, a percentage, a boolean, or plain text. + +```python +@page.stat("Total Users") +async def total_users() -> spec.Stat: + return 1_024 +``` + +## Decorator + +```python +page.stat( + name: str, + *, + icon: spec.Icon | None = None, + color: spec.Color | None = None, + description: str | None = None, + refresh: timedelta | None = None, +) +``` + +The decorated function is registered as `GET //stat/`. + +## Return value + +```python +type StatValue = str | int | float | bool | None + +class StatResponse(TypedDict): + value: StatValue + refresh: NotRequired[int | None] + icon: NotRequired[Icon] + color: NotRequired[Color] + +type Stat = StatValue | StatResponse +``` + +Return the bare value when you have nothing else to say: + +```python +@page.stat("Active Sessions") +async def active_sessions() -> spec.Stat: + return 42 +``` + +Return a `StatResponse` dict to override the icon, color, or refresh interval per-response (useful when they depend on the value itself, e.g. a red icon when a count is above a threshold): + +```python +@page.stat("Total Authors") +async def get_total_authors(session: AsyncSessionDep) -> spec.StatResponse: + result = await session.execute(select(func.count(models.Author.id))) + return {"value": result.scalar_one(), "icon": "users", "color": "indigo"} +``` + +## Auto-refreshing + +Pass `refresh` to poll the stat's endpoint automatically: + +```python +from datetime import timedelta + +@page.stat("Random Number", icon="sun", color="yellow", refresh=timedelta(seconds=1)) +def random_number(): + return randint(100, 1000) +``` + +## Parameters + +Like every component, a stat function can take `Query`, `Body`, `Depends`, and pydantic parameters — they're inferred from the signature and become part of the component's spec, same as any other FastAPI endpoint: + +```python +@page.stat("Books Without Publisher") +async def get_books_without_publisher(session: AsyncSessionDep) -> int: + result = await session.execute( + select(func.count(models.Book.id)).where(models.Book.publisher_id.is_(None)) + ) + return result.scalar_one() +``` diff --git a/docs/content/components/table.md b/docs/content/components/table.md new file mode 100644 index 00000000..a5f6e9b1 --- /dev/null +++ b/docs/content/components/table.md @@ -0,0 +1,144 @@ +# Table + +A table is a paginated, searchable grid, with optional styled columns and per-row actions. + +```python +@page.table("Recent Users") +async def recent_users() -> spec.Table: + return { + "data": [ + {"id": 1, "name": "Alice", "role": "admin"}, + {"id": 2, "name": "Bob", "role": "viewer"}, + ] + } +``` + +## Decorator + +```python +page.table( + name: str, + *, + description: str | None = None, + is_hidden: bool = False, + columns: dict[str, spec.ColumnConfigValue] | None = None, + icon: spec.Icon | None = None, + color: spec.Color | None = None, + refresh: timedelta | None = None, +) +``` + +The decorated function is registered as `GET //table/`. Unlike actions and forms, a table's method is always `get`. + +## Return value + +```python +class TableResponse(TypedDict): + data: TableData + icon: NotRequired[Icon] + color: NotRequired[Color] + refresh: NotRequired[int | None] + total: NotRequired[int] + +type Table = TableData | TableResponse +``` + +`TableData` is an iterable of row dicts. The simplest table returns a bare list: + +```python +@page.table("Books by Author", description="Number of books per author") +async def get_books_by_author(session: AsyncSessionDep): + ... + return [{"author": name, "book_count": count} for name, count in rows] +``` + +Return a `TableResponse` dict instead when you need `total` for pagination (see below) or want to override `icon`/`color`/`refresh` per response. + +## Columns + +`columns` maps a field name in your row dicts to display metadata: + +```python +class ColumnConfigValue(TypedDict): + style: NotRequired[Literal["image", "badge", "link", "file"]] + label: NotRequired[str] + icon: NotRequired[Icon] + color: NotRequired[Color] +``` + +```python +@page.table( + "All Books", + columns={ + "id": {"label": "ID", "icon": "hash", "color": "slate"}, + "cover": {"style": "image", "label": "Cover", "icon": "image", "color": "sky"}, + "title": {"label": "Title", "icon": "book-text", "color": "blue"}, + "status": {"style": "badge", "label": "Status", "icon": "badge-check", "color": "emerald"}, + "reference": {"style": "link", "label": "Reference", "icon": "external-link", "color": "indigo"}, + "attachment": {"style": "file", "label": "Attachment", "icon": "file", "color": "amber"}, + }, +) +async def get_all_books(...) -> spec.Table: ... +``` + +A column left out of `columns` is still shown, using its raw key as the label. + +## Pagination and search + +`openadmin.fastapi.deps` ships two dependencies for the common case: + +```python +from openadmin.fastapi.deps import PageDep, SearchDep + +@page.table("All Books") +async def get_all_books( + session: AsyncSessionDep, pagination: PageDep, search: SearchDep +) -> spec.Table: + stmt = ( + select(models.Book) + .offset((pagination.page - 1) * pagination.per_page) + .limit(pagination.per_page) + ) + if search: + stmt = stmt.where(models.Book.title.ilike(f"%{search}%")) + ... +``` + +`PageDep` resolves to a `PaginationParams(page: int, per_page: int)` read from the `page`/`per_page` query parameters (default `page=1`, `per_page=10`). `SearchDep` resolves to `str | None` from a `search` query parameter. Return `total` in a `TableResponse` so the frontend knows how many pages exist: + +```python +return {"data": [...], "total": total_count} +``` + +## Per-row keys + +A row dict can include three special keys alongside your own columns: + +- **`__view__`** — a compact label used to represent the whole row elsewhere (for example, when the row is offered as an option in a form's reference picker — see [Implementing a Form with a Reference](/cookbook/implementing-form-with-reference)). +- **`__actions__`** — a list of buttons attached to that row. Each entry references an action registered elsewhere on the page via `reference_action`: + + ```python + from openadmin.fastapi import reference_action + + "__actions__": [ + { + "label": "Delete this user", + "action": reference_action(delete_author), + "query": {"id": author.id}, + "color": "red", + "icon": "trash", + }, + ], + ``` + +- **`__values__`** — per-cell overrides of `style`/`label`/`icon`/`color` for that row's specific columns, when a single column's styling depends on the row's data: + + ```python + "__values__": { + "status": {"color": "green" if flag["enabled"] else "red", "icon": "antenna"}, + }, + ``` + +## Hiding a table + +`is_hidden=True` keeps the route and spec entry but removes the table from the visible page. This is how a table can act purely as a data source for a form's reference field, without also cluttering the page it's declared on. See [Implementing a Table](/cookbook/implementing-table) for the full pattern. diff --git a/docs/content/cookbook/implementing-auth.md b/docs/content/cookbook/implementing-auth.md new file mode 100644 index 00000000..f8db0822 --- /dev/null +++ b/docs/content/cookbook/implementing-auth.md @@ -0,0 +1,93 @@ +# Implementing Auth + +A step-by-step recipe for gating a panel behind a login screen. Background on how the pieces fit together is in [Authentication](/auth/). + +## 1. Add session support to the outer app + +OpenAdmin doesn't ship a session store — use Starlette's `SessionMiddleware`, added to the app you mount the panel onto (not to `admin.app` itself): + +```python +# main.py +from fastapi import FastAPI +from starlette.middleware.sessions import SessionMiddleware + +app = FastAPI() +app.add_middleware(SessionMiddleware, secret_key=os.environ["SESSION_SECRET"]) +``` + +## 2. Create an `AdminAuth` instance + +```python +# admin/auth.py +from openadmin.fastapi import AdminAuth + +auth = AdminAuth() +``` + +## 3. Implement login + +Validate credentials against your real user store, then write a session marker: + +```python +from fastapi import HTTPException, Request, status + +from openadmin.fastapi import LoginReq +from .users import verify_password, get_user_by_username + + +@auth.login() +async def login(req: Request, login_req: LoginReq) -> None: + user = await get_user_by_username(login_req.username) + if user is None or not verify_password(login_req.password, user.password_hash): + raise HTTPException(status.HTTP_401_UNAUTHORIZED, "Invalid username or password") + + req.session["user_id"] = user.id +``` + +`LoginReq` is a pydantic model with `username: str` and `password: str`. Raising `HTTPException` here is what makes `POST /auth/login` return an error instead of `204`. + +## 4. Implement authenticate + +This runs before every request under `/api/*`, so keep it cheap — a session lookup, not a full user fetch, if you can avoid it: + +```python +@auth.authenticate() +def authenticate(req: Request) -> None: + if "user_id" not in req.session: + raise HTTPException(status.HTTP_401_UNAUTHORIZED, "Unauthorized") +``` + +## 5. Implement logout + +```python +@auth.logout() +def logout(req: Request) -> None: + req.session.clear() +``` + +## 6. Wire it into the panel + +```python +# admin/panel.py +from openadmin.fastapi import AdminPanel + +from .auth import auth + +admin = AdminPanel("My Admin", auth=auth) +``` + +At this point every `/api/*` call — every stat, table, form, and action on every page — requires a valid session, and `/admin/` still serves the frontend so a logged-out visitor can reach the login screen at all. + +## Alternative: token-based auth + +`authenticate_func` just receives the `Request`, so nothing ties you to cookies. To check a bearer token instead: + +```python +@auth.authenticate() +def authenticate(req: Request) -> None: + token = req.headers.get("authorization", "").removeprefix("Bearer ") + if not is_valid_token(token): + raise HTTPException(status.HTTP_401_UNAUTHORIZED, "Unauthorized") +``` + +In that case `login_func` can simply be left as a no-op (or removed from your flow entirely) if tokens are issued out of band, since nothing requires you to use `/auth/login` at all. diff --git a/docs/content/cookbook/implementing-form-with-reference.md b/docs/content/cookbook/implementing-form-with-reference.md new file mode 100644 index 00000000..5c9cf6ca --- /dev/null +++ b/docs/content/cookbook/implementing-form-with-reference.md @@ -0,0 +1,88 @@ +# Implementing a Form with a Reference + +A step-by-step recipe for a form field that picks its value from another table's rows — for example, choosing a book's author from a list of authors instead of typing an author ID by hand. Full option reference is in [Form](/components/form) and [Table](/components/table). + +::: info +`AsyncSessionDep`, `Author`, `Book`, and the `select` calls below stand in for your own database session dependency and models — OpenAdmin has no opinion on persistence. +::: + +## 1. Define the table the field will reference + +The referenced table has to be a normal `@page.table(...)`, decorated before you reference it. It doesn't need to live on the same page as the form: + +```python +# admin/authors.py +from openadmin import spec +from openadmin.fastapi import AdminPage + +page = AdminPage("Authors", icon="users") + + +@page.table( + "All Authors", + columns={"id": {"label": "ID"}, "name": {"label": "Name"}}, +) +async def get_all_authors(session: AsyncSessionDep) -> spec.Table: + authors = (await session.execute(select(Author))).scalars().all() + return { + "data": [ + {"id": a.id, "name": a.name, "__view__": a.name} + for a in authors + ], + } +``` + +`__view__` is what the reference picker displays for each row — set it to whatever best identifies the row to a human, here the author's name instead of their raw ID. + +## 2. Define the form's request body + +```python +# admin/books.py +from pydantic import BaseModel + +class AddBookBody(BaseModel): + title: str + author_id: int +``` + +## 3. Point the field at the table + +```python +from openadmin.fastapi import AdminPage, reference_table + +from .authors import get_all_authors + +page = AdminPage("Books", icon="book") + + +@page.form( + "Add Book", + fields={ + "author_id": { + "reference": reference_table(get_all_authors), + "reference_field": "id", + "icon": "user-pen", + "color": "violet", + } + }, +) +async def add_book(body: AddBookBody, session: AsyncSessionDep) -> spec.Form: + book = Book(**body.model_dump()) + session.add(book) + await session.commit() + await session.refresh(book) + return {"message": f"Added book '{book.title}'"} +``` + +- **`reference`** is the referenced table's generated ID, obtained with `reference_table(get_all_authors)` — pass the function itself, not a string, so a rename of the table doesn't silently break the link. +- **`reference_field`** is the column of the referenced table's rows to actually submit as `author_id`'s value. The picker shows each row's `__view__` label, but writes `row[reference_field]` when one is chosen — here, the author's `id`. + +Because `reference_table` reads an attribute the `@page.table(...)` decorator stamps directly onto `get_all_authors`, that function must already be decorated by the time `add_book` is defined — in practice, just make sure the module defining the table is imported before the module defining the form runs, as in the example above. + +## 4. Cross-page references + +The referenced table doesn't have to live on the same `AdminPage` as the form — import the decorated function from wherever it's defined, same as the `from .authors import get_all_authors` above. Only the function object matters; the pages don't need any other relationship. + +## Referencing an action instead of a table + +The equivalent helper for attaching an action to a table row is `reference_action`, covered in [Implementing a Table](/cookbook/implementing-table). Both helpers work the same way — they read an ID the decorator stamped onto an already-decorated function. diff --git a/docs/content/cookbook/implementing-table.md b/docs/content/cookbook/implementing-table.md new file mode 100644 index 00000000..844625a6 --- /dev/null +++ b/docs/content/cookbook/implementing-table.md @@ -0,0 +1,117 @@ +# Implementing a Table + +A step-by-step recipe for a searchable, paginated table with styled columns and a per-row delete action. Full option reference is in [Table](/components/table) and [Action](/components/action). + +::: info +`AsyncSessionDep`, `User`, and the `select`/`func` calls below stand in for your own database session dependency and models — OpenAdmin has no opinion on persistence. `PageDep`/`SearchDep` are the only dependencies that come from OpenAdmin itself. +::: + +## 1. Define the page + +```python +# admin/users.py +from openadmin import spec +from openadmin.fastapi import AdminPage, reference_action +from openadmin.fastapi.deps import PageDep, SearchDep + +page = AdminPage("Users", icon="users") +``` + +## 2. Add pagination and search + +`PageDep` and `SearchDep` cover the common case — `page`/`per_page` and `search` query parameters, parsed for you: + +```python +@page.table( + "All Users", + description="Browse users with search and pagination", + icon="users", + color="blue", + columns={ + "id": {"label": "ID", "icon": "hash", "color": "slate"}, + "name": {"label": "Name", "icon": "user", "color": "indigo"}, + "role": {"style": "badge", "label": "Role", "icon": "shield", "color": "emerald"}, + }, +) +async def get_all_users(session: AsyncSessionDep, pagination: PageDep, search: SearchDep) -> spec.Table: + stmt = select(User).offset((pagination.page - 1) * pagination.per_page).limit(pagination.per_page) + count_stmt = select(func.count(User.id)) + if search: + stmt = stmt.where(User.name.ilike(f"%{search}%")) + count_stmt = count_stmt.where(User.name.ilike(f"%{search}%")) + + users = (await session.execute(stmt)).scalars().all() + total = (await session.execute(count_stmt)).scalar_one() + + return { + "data": [{"id": u.id, "name": u.name, "role": u.role} for u in users], + "total": total, + } +``` + +Returning `total` is what lets the frontend render correct page controls — without it, pagination still works, but it can't show a page count. + +`columns` controls display, not data: `style: "badge"` renders the `role` cell as a badge; leave a column out of `columns` and it still shows, using its raw key as the label. See [Table](/components/table) for the other styles (`image`, `link`, `file`). + +## 3. Add a delete action + +Declare the action with `is_hidden=True` — it exists purely to be triggered from a row, not to be shown on the page as its own button: + +```python +@page.action("Delete User", method="delete", is_hidden=True) +async def delete_user(session: AsyncSessionDep, user_id: int = Query(...)) -> spec.Action: + user = await session.get(User, user_id) + if user: + await session.delete(user) + await session.commit() + return {"toast": f"User #{user_id} deleted" if user else f"User #{user_id} not found"} +``` + +## 4. Attach it to each row + +Reference the action by function, not by string ID, so a rename doesn't silently break the link: + +```python +@page.table("All Users", ...) +async def get_all_users(...) -> spec.Table: + ... + return { + "data": [ + { + "id": u.id, + "name": u.name, + "role": u.role, + "__actions__": [ + { + "label": "Delete", + "action": reference_action(delete_user), + "query": {"user_id": u.id}, + "icon": "trash", + "color": "red", + }, + ], + } + for u in users + ], + "total": total, + } +``` + +`reference_action` reads the ID that `@page.action(...)` stamped onto `delete_user`, so `delete_user` needs to be defined (decorated) above `get_all_users` in the file, or at least imported before this function runs — plain Python name resolution, nothing OpenAdmin-specific. + +The action's own `query`/`body`/`form` schema (inferred from `Query(...)` on `user_id`) still applies — the `query` dict in `__actions__` is what actually gets sent as that action's parameters when the row's button is clicked. + +## 5. Per-cell overrides + +When a single column's icon or color depends on the row's data rather than being fixed in `columns`, use `__values__`: + +```python +"__values__": { + "role": {"color": "amber" if u.role == "admin" else "slate"}, +}, +``` + +## Next + +- Use a table as the source for a form's reference picker: [Implementing a Form with a Reference](/cookbook/implementing-form-with-reference). +- Full column style and per-row key reference: [Table](/components/table). diff --git a/docs/content/introduction/getting-started.md b/docs/content/introduction/getting-started.md new file mode 100644 index 00000000..2d108825 --- /dev/null +++ b/docs/content/introduction/getting-started.md @@ -0,0 +1,96 @@ +# Getting Started + +This walks through building a minimal admin panel from scratch: one page, one stat, one table, mounted onto a FastAPI app. + +## Requirements + +- Python 3.14+ +- FastAPI + +## Installation + +```bash +pip install openadmin-py +``` + +or with `uv`: + +```bash +uv add openadmin-py +``` + +## Build a page + +An `AdminPage` holds the widgets for one page in the navigation. Widgets are added by decorating functions with `@page.stat`, `@page.table`, `@page.form`, and `@page.action`. + +```python +# app/admin/dashboard.py +from openadmin import spec +from openadmin.fastapi import AdminPage + +page = AdminPage("Dashboard", icon="layout-dashboard") + + +@page.stat("Total Users") +async def total_users() -> spec.Stat: + return 1_024 + + +@page.table("Recent Users") +async def recent_users() -> spec.Table: + return { + "data": [ + {"id": 1, "name": "Alice", "role": "admin"}, + {"id": 2, "name": "Bob", "role": "viewer"}, + ] + } +``` + +Each decorated function is a normal `async def` (or sync `def`) handler — it can take `Query`/`Body` parameters, use `Depends(...)` for a database session, and return whatever your return type declares. The decorator infers the parameter schema from the function signature the same way FastAPI does. + +## Assemble the panel + +`AdminPanel` is the top-level object. Pages are grouped into named sections: + +```python +# app/admin/panel.py +from openadmin.fastapi import AdminPanel + +from .dashboard import page as dashboard_page + +admin = AdminPanel("My Admin", description="Internal operations panel") + +admin.section("General", icon="layout-grid", pages=[dashboard_page]) +``` + +## Mount it on your app + +`AdminPanel.app` is a real `FastAPI()` instance, so it mounts like any sub-application: + +```python +# app/main.py +from fastapi import FastAPI + +from .admin.panel import admin + +app = FastAPI() +app.mount("/admin", admin.app) +``` + +## Run it + +```bash +fastapi dev app/main.py +``` + +Visit `http://localhost:8000/admin/` for the UI, or `http://localhost:8000/admin/api/openadmin.json` to see the raw spec that drives it. + +::: tip +The panel is unprotected by default — anyone who can reach `/admin` can view and act on every widget. See [Authentication](/auth/) before deploying anywhere reachable by untrusted users. +::: + +## Next steps + +- [Components](/components/) — every widget type, with its full option list. +- [Authentication](/auth/) — add a login screen. +- [Cookbook](/cookbook/implementing-table) — worked recipes for common patterns like reference fields and row actions. diff --git a/docs/content/introduction/what-is-openadmin.md b/docs/content/introduction/what-is-openadmin.md new file mode 100644 index 00000000..01d9cef9 --- /dev/null +++ b/docs/content/introduction/what-is-openadmin.md @@ -0,0 +1,58 @@ +# What is OpenAdmin? + +OpenAdmin is a FastAPI-native library for building admin dashboards. You define pages as plain Python functions decorated with typed helpers — no templates, no separate frontend project, no configuration files to maintain. Every page is a FastAPI router; every widget on that page is just an endpoint. + +```python +@page.stat("Total Users") +async def total_users() -> spec.Stat: + return 1_024 +``` + +That function is a normal `async def` FastAPI handler. It also happens to describe a stat tile that shows up in the admin UI, because OpenAdmin inspects its signature and return type to build a specification the bundled frontend renders. + +## The core idea + +An admin panel is a tree of four kinds of objects: + +- **`AdminPanel`** — the panel itself. It is a small wrapper around a `FastAPI()` app, meant to be mounted onto your real application with `app.mount("/admin", admin.app)`. +- **Sections** — top-level groups in the navigation, registered on the panel with `admin.section(...)`. A section is just a name plus a list of pages. +- **`AdminPage`** — a page inside a section. Internally it holds a FastAPI `APIRouter`, so everything you already know about routers, dependencies, and path operations applies. +- **Components** — the widgets on a page: stats, tables, forms, actions, charts, and markdown blocks. Each one is registered with a decorator on an `AdminPage` (`@page.stat(...)`, `@page.table(...)`, etc.) and becomes both a real HTTP endpoint and an entry in the page's spec. + +``` +AdminPanel +└── section("Library") + └── AdminPage("Books") + ├── @page.stat("Total Books") + ├── @page.table("All Books") + ├── @page.form("Add Book") + └── @page.action("Delete Book") +``` + +See [Components](/components/) for a full walkthrough of each widget type. + +## How it fits together + +Every decorator does two things at once: + +1. It registers your function as a real route on the page's router — with FastAPI's usual dependency injection, `Query`/`Body`/`Form` parameters, pydantic models, and OpenAPI docs. +2. It records metadata (name, icon, color, the parameters it accepts, and so on) into a `Spec` — a plain nested `TypedDict` structure (`openadmin.spec.Spec` → `Section` → `Page` → `Component`) describing the whole panel. + +The panel exposes that structure at `GET /api/openadmin.json`. The bundled frontend (served from the same panel at `/`) fetches this spec once to build the navigation and layout, then calls each component's own endpoint — the same one your decorator wrapped — to fetch or refresh its data. Because it's the same endpoint either way, you can `curl` any widget directly, exercise it from FastAPI's `/docs`, or write ordinary tests against it. + +## What's included + +- **Stats** — a single value: a count, a percentage, a boolean. +- **Tables** — paginated, searchable grids with per-row actions and styled columns (badges, images, links, files). +- **Forms** — structured forms that submit to your own endpoint, with optional reference fields that look values up from another table. +- **Actions** — one-off buttons that call an endpoint, either standalone or attached to a table row. +- **Charts** — bar and pie charts today, with area and line chart types defined in the spec for future support. +- **Markdown** — static or dynamic rich text blocks for guides, changelogs, or contextual notes. + +Because a page is a router and a widget is an endpoint, everything FastAPI already gives you — dependency injection, background tasks, database sessions, response validation, middleware — works unchanged. There is no separate query language or templating layer to learn. + +## Next steps + +- [Getting Started](/introduction/getting-started) — build a minimal panel end to end. +- [Components](/components/) — the full reference for every widget type. +- [Authentication](/auth/) — gate the panel behind a login screen. From 03cb1f9db28817118b0608c8bcec4078a9436d68 Mon Sep 17 00:00:00 2001 From: Mykyta Date: Sat, 22 Aug 2026 13:23:45 +0200 Subject: [PATCH 7/9] ref --- .claude/launch.json | 7 ++++++ docs/config/.vitepress/config.mts | 40 ++++++++++++++++++++++++++++--- docs/content/index.md | 25 ++++++++++--------- 3 files changed, 58 insertions(+), 14 deletions(-) diff --git a/.claude/launch.json b/.claude/launch.json index b1d69957..8d99b531 100644 --- a/.claude/launch.json +++ b/.claude/launch.json @@ -7,6 +7,13 @@ "runtimeArgs": ["run", "--cwd", "client", "dev"], "port": 5173, "autoPort": true + }, + { + "name": "docs-dev", + "runtimeExecutable": "bun", + "runtimeArgs": ["run", "--cwd", "docs", "dev"], + "port": 5174, + "autoPort": true } ] } diff --git a/docs/config/.vitepress/config.mts b/docs/config/.vitepress/config.mts index a3af4c84..6c892425 100644 --- a/docs/config/.vitepress/config.mts +++ b/docs/config/.vitepress/config.mts @@ -10,19 +10,53 @@ export default defineConfig({ // https://vitepress.dev/reference/default-theme-config nav: [ { text: 'Home', link: '/' }, - { text: 'Examples', link: '/markdown-examples' } + { text: 'Introduction', link: '/introduction/what-is-openadmin' }, + { text: 'Components', link: '/components/' }, + { text: 'Auth', link: '/auth/' }, + { text: 'Cookbook', link: '/cookbook/implementing-auth' } ], sidebar: [ { - text: 'Examples', + text: 'Introduction', items: [ + { text: 'What is OpenAdmin?', link: '/introduction/what-is-openadmin' }, + { text: 'Getting Started', link: '/introduction/getting-started' } + ] + }, + { + text: 'Components', + items: [ + { text: 'Overview', link: '/components/' }, + { text: 'Stat', link: '/components/stat' }, + { text: 'Table', link: '/components/table' }, + { text: 'Form', link: '/components/form' }, + { text: 'Action', link: '/components/action' }, + { text: 'Markdown', link: '/components/markdown' }, + { text: 'Bar Chart', link: '/components/bar-chart' }, + { text: 'Pie Chart', link: '/components/pie-chart' }, + { text: 'Area Chart', link: '/components/area-chart' }, + { text: 'Line Chart', link: '/components/line-chart' } + ] + }, + { + text: 'Auth', + items: [ + { text: 'Authentication', link: '/auth/' } + ] + }, + { + text: 'Cookbook', + items: [ + { text: 'Implementing Auth', link: '/cookbook/implementing-auth' }, + { text: 'Implementing a Table', link: '/cookbook/implementing-table' }, + { text: 'Implementing a Form with a Reference', link: '/cookbook/implementing-form-with-reference' } ] } ], socialLinks: [ - { icon: 'github', link: 'https://github.com/vuejs/vitepress' } + { icon: 'github', link: 'https://github.com/openadmin-team/openadmin-py' } ] } }) diff --git a/docs/content/index.md b/docs/content/index.md index 076f79af..c190fd35 100644 --- a/docs/content/index.md +++ b/docs/content/index.md @@ -5,21 +5,24 @@ layout: home hero: name: "OpenAdmin" text: "Admin panel for python" - tagline: My great project tagline + tagline: Build admin dashboards as FastAPI routes — stats, tables, charts, and actions, all in pure Python. actions: - theme: brand - text: Markdown Examples - link: /markdown-examples + text: What is OpenAdmin? + link: /introduction/what-is-openadmin - theme: alt - text: API Examples - link: /api-examples + text: Getting Started + link: /introduction/getting-started features: - - title: Feature A - details: Lorem ipsum dolor sit amet, consectetur adipiscing elit - - title: Feature B - details: Lorem ipsum dolor sit amet, consectetur adipiscing elit - - title: Feature C - details: Lorem ipsum dolor sit amet, consectetur adipiscing elit + - icon: 📊 + title: Stats, Tables & Charts + details: Single-value stats, paginated searchable tables, and bar/pie charts — each just a decorated FastAPI endpoint. + - icon: 📝 + title: Forms & Actions + details: Structured forms and one-off action buttons that submit to your own endpoints, with typed parameters inferred automatically. + - icon: 🔌 + title: FastAPI-native + details: A page is a router, a widget is an endpoint. Full dependency injection, OpenAPI docs, and middleware compatibility for free. --- From 30ddebd692514859d87c9d86780a898a3f5f58c9 Mon Sep 17 00:00:00 2001 From: Mykyta Date: Sat, 22 Aug 2026 13:26:53 +0200 Subject: [PATCH 8/9] ref --- docs/config/.vitepress/config.mts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/config/.vitepress/config.mts b/docs/config/.vitepress/config.mts index 6c892425..22e4d697 100644 --- a/docs/config/.vitepress/config.mts +++ b/docs/config/.vitepress/config.mts @@ -42,7 +42,7 @@ export default defineConfig({ { text: 'Auth', items: [ - { text: 'Authentication', link: '/auth/' } + { text: 'Email Authentication', link: '/auth/' } ] }, { From e3b6e67f154af2e1992884e9d47eb3c67b886b3a Mon Sep 17 00:00:00 2001 From: Mykyta Date: Sat, 22 Aug 2026 13:27:14 +0200 Subject: [PATCH 9/9] ref --- docs/config/.vitepress/config.mts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/config/.vitepress/config.mts b/docs/config/.vitepress/config.mts index 22e4d697..4079d069 100644 --- a/docs/config/.vitepress/config.mts +++ b/docs/config/.vitepress/config.mts @@ -42,7 +42,7 @@ export default defineConfig({ { text: 'Auth', items: [ - { text: 'Email Authentication', link: '/auth/' } + { text: 'Password Authentication', link: '/auth/' } ] }, {