feat: integration screen

This commit is contained in:
fsylvain committed 2026-09-27 18:04:43 +02:00
1 parent a9a208b80a
commit ad55137883
13 files changed
+786 -6

No files matched your search

+91
View File
@@ -10,6 +10,7 @@
"@tiptap/core": "3.31.3",
"@vueuse/core": "14.4.0",
"pinia": "^3.0.4",
"shiki": "^4.4.3",
"tailwindcss": "^4.1.18",
"vue": "^3.5.25",
"vue-router": "^4.6.3",
@@ -300,6 +301,22 @@
"@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.54.0", "", { "os": "win32", "cpu": "x64" }, "sha512-hYT5d3YNdSh3mbCU1gwQyPgQd3T2ne0A3KG8KSBdav5TiBg6eInVmV+TeR5uHufiIgSFg0XsOWGW5/RhNcSvPg=="],
"@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/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=="],
"@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
"@swc/helpers": ["@swc/helpers@0.5.23", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-5lSsMOTXURePglDfvuAQUqkGek9Hg2kksOYay2m0+XR++b2NWYL/4sWyuvVBIs8oKnJaxkdi9whaL/sqN13afw=="],
@@ -428,10 +445,16 @@
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
"@types/hast": ["@types/hast@3.0.5", "", { "dependencies": { "@types/unist": "*" } }, "sha512-rp/ezSWaD1m44dPKICGhiskI13nVr7qTloFwDa/IYkhhf5nzwP+zIQcIJh3WIFSBOy/H1PzB40jPjMDksN4F+g=="],
"@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
"@types/mdast": ["@types/mdast@4.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA=="],
"@types/node": ["@types/node@24.10.4", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-vnDVpYPMzs4wunl27jHrfmwojOGKya0xyM3sH+UE5iv5uPS6vX7UIoh6m+vQc5LGBq52HBKPIn/zcSZVzeDEZg=="],
"@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=="],
"@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.50.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.50.0", "@typescript-eslint/type-utils": "8.50.0", "@typescript-eslint/utils": "8.50.0", "@typescript-eslint/visitor-keys": "8.50.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.50.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-O7QnmOXYKVtPrfYzMolrCTfkezCJS9+ljLdKW/+DCvRsc3UAz+sbH6Xcsv7p30+0OwUbeWfUDAQE0vpabZ3QLg=="],
@@ -454,6 +477,8 @@
"@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.50.0", "", { "dependencies": { "@typescript-eslint/types": "8.50.0", "eslint-visitor-keys": "^4.2.1" } }, "sha512-Xzmnb58+Db78gT/CCj/PVCvK+zxbnsw6F+O1oheYszJbBSdEjVhQi3C/Xttzxgi/GLmpvOggRs1RFpiJ8+c34Q=="],
"@ungap/structured-clone": ["@ungap/structured-clone@1.4.0", "", {}, "sha512-1mEZtMKPM09vDmQt5y7YvmN2+DFTP7Tg0EWXdic8/C6VRnpb33e4ghisCIE3WZjsE2N8mf+QV1Zqh7ZFYLWInQ=="],
"@unhead/bundler": ["@unhead/bundler@3.4.1", "", { "dependencies": { "magic-string": "^1.3.1", "oxc-walker": "^1.1.1", "unplugin": "^3.3.0" }, "peerDependencies": { "@unhead/cli": "^3.4.1", "@vitejs/devtools-kit": "^0.4.1", "esbuild": ">=0.17.0", "lightningcss": ">=1.20.0", "oxc-parser": ">=0.98.0", "rolldown": ">=1.0.0", "unhead": "^3.4.1", "vite": ">=6.4.2", "webpack": ">=5.0.0" }, "optionalPeers": ["@unhead/cli", "@vitejs/devtools-kit", "esbuild", "lightningcss", "oxc-parser", "rolldown", "vite", "webpack"] }, "sha512-1RLNvuZYCt6K+oIK7v0mKlbmFbTuNZRgAMrf+GdmiO5xfml2pl/ViZmBrCOxoBtLxCboDZ70hs+F9+z2YWHLuQ=="],
"@unhead/vue": ["@unhead/vue@3.4.1", "", { "dependencies": { "@unhead/bundler": "3.4.1", "hookable": "^6.1.1", "unhead": "3.4.1", "unplugin": "^3.3.0" }, "peerDependencies": { "vite": ">=6.4.2", "vue": ">=3.5.18", "webpack": ">=5.0.0" }, "optionalPeers": ["vite", "webpack"] }, "sha512-FQLTdsX1+m+a7Z5SzkQde8TR/4QK6p0+NQ6BuO9zbtc5xps2fRy7W+wQ90qn177C2X88aZAPzw29mg6T4VFSdA=="],
@@ -570,10 +595,16 @@
"caniuse-lite": ["caniuse-lite@1.0.30001761", "", {}, "sha512-JF9ptu1vP2coz98+5051jZ4PwQgd2ni8A+gYSN7EA7dPKIMf0pDlSUxhdmVOaV3/fYK5uWBkgSXJaRLr4+3A6g=="],
"ccount": ["ccount@2.0.1", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="],
"chai": ["chai@6.2.2", "", {}, "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg=="],
"chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
"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=="],
"chokidar": ["chokidar@5.0.0", "", { "dependencies": { "readdirp": "^5.0.0" } }, "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw=="],
"citty": ["citty@0.1.6", "", { "dependencies": { "consola": "^3.2.3" } }, "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ=="],
@@ -586,6 +617,8 @@
"combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="],
"comma-separated-tokens": ["comma-separated-tokens@2.0.3", "", {}, "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg=="],
"commander": ["commander@14.0.3", "", {}, "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw=="],
"concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="],
@@ -632,10 +665,14 @@
"delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="],
"dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="],
"destr": ["destr@2.0.5", "", {}, "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA=="],
"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=="],
"dotenv": ["dotenv@17.4.2", "", {}, "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw=="],
"dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
@@ -786,12 +823,18 @@
"hasown": ["hasown@2.0.4", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A=="],
"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=="],
"hey-listen": ["hey-listen@1.0.8", "", {}, "sha512-COpmrF2NOg4TBWUJ5UVyaCU2A88wEMkUPK4hNqyCkqHbxT92BbvfjoSozkAIIm6XhicGlJHhFdullInrdhwU8Q=="],
"hookable": ["hookable@6.1.2", "", {}, "sha512-+abwxtiEA52GCVIsQqut3S/uKTbUwYIp4Pe/vv+6py5XiXBCqMZHg6pA6Y5qhgLSEys0/cYuPbO0z1QFj5ZCmg=="],
"html-encoding-sniffer": ["html-encoding-sniffer@4.0.0", "", { "dependencies": { "whatwg-encoding": "^3.1.1" } }, "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ=="],
"html-void-elements": ["html-void-elements@3.0.0", "", {}, "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg=="],
"http-proxy-agent": ["http-proxy-agent@7.0.2", "", { "dependencies": { "agent-base": "^7.1.0", "debug": "^4.3.4" } }, "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig=="],
"https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="],
@@ -912,6 +955,8 @@
"math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
"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=="],
"mdn-data": ["mdn-data@2.27.1", "", {}, "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ=="],
"memorystream": ["memorystream@0.3.1", "", {}, "sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw=="],
@@ -920,6 +965,16 @@
"merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="],
"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.3", "", {}, "sha512-oxB2Ik03hI0gv+VNn9tnh1t1YEe9MDPptViAEgfdf3YQHsn0pzGTgCdlSCJXcwhqm8phaEuM7zeEu3QQzVBrPg=="],
"micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="],
"mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
@@ -982,6 +1037,10 @@
"onetime": ["onetime@6.0.0", "", { "dependencies": { "mimic-fn": "^4.0.0" } }, "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ=="],
"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=="],
"open": ["open@10.2.0", "", { "dependencies": { "default-browser": "^5.2.1", "define-lazy-prop": "^3.0.0", "is-inside-container": "^1.0.0", "wsl-utils": "^0.1.0" } }, "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA=="],
"optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="],
@@ -1036,6 +1095,8 @@
"prettier-linter-helpers": ["prettier-linter-helpers@1.0.0", "", { "dependencies": { "fast-diff": "^1.1.2" } }, "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w=="],
"property-information": ["property-information@7.2.0", "", {}, "sha512-IAtzIB6sUiWaJYrX9smp3V46pBGbBeLFRGdh25kg1334VcBlD8HzhPeNIWQH9zhGmo2itIe25EHt9dQP7G5hmg=="],
"prosemirror-changeset": ["prosemirror-changeset@2.4.3", "", { "dependencies": { "prosemirror-transform": "^1.0.0" } }, "sha512-J8jqUZ1xybXKY44v3K0c8m8ZTIqJPVT34t2UVyxv4VEWoNd5C9t5Y52GNuMriRJ3IpNxOSE2R60vN4qBrTMcyQ=="],
"prosemirror-commands": ["prosemirror-commands@1.7.2", "", { "dependencies": { "prosemirror-model": "^1.0.0", "prosemirror-state": "^1.0.0", "prosemirror-transform": "^1.10.2" } }, "sha512-q6Q6szxqdu9Xd6EcdKsqXghu5nQdZTpB4Q9yd04WRc7/jt763e/rT60Owh0L1GYY+T46o5rD+9lEN36dZS43tw=="],
@@ -1078,6 +1139,12 @@
"readdirp": ["readdirp@5.1.1", "", {}, "sha512-Kko+Y5XQ6fM+Ce3dq3m9YGxnacYZYl9cA1wZjaF3Vbry2L3i1qVg8+CAgNPsXRArPMUMCaOR7oa9Nqntc43JKA=="],
"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=="],
"regexp-tree": ["regexp-tree@0.1.27", "", { "bin": { "regexp-tree": "bin/regexp-tree" } }, "sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA=="],
"reka-ui": ["reka-ui@2.10.4", "", { "dependencies": { "@floating-ui/dom": "^1.6.13", "@floating-ui/vue": "^1.1.6", "@internationalized/date": "^3.5.0", "@internationalized/number": "^3.5.0", "@tanstack/vue-virtual": "^3.12.0", "@vueuse/core": "^14.1.0", "@vueuse/shared": "^14.1.0", "aria-hidden": "^1.2.4", "defu": "^6.1.5", "ohash": "^2.0.11" }, "peerDependencies": { "vue": ">= 3.4.0" } }, "sha512-kbS5GAbkHkYj0EVKAg5ZPEPndhBjeUFSa1Aq5m5ftQwyHnB1v5tw8X6fcwfKH/ry3StPXblMWM1DW82MVS71cw=="],
@@ -1112,16 +1179,22 @@
"shell-quote": ["shell-quote@1.8.3", "", {}, "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw=="],
"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=="],
"signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="],
"sirv": ["sirv@3.0.2", "", { "dependencies": { "@polka/url": "^1.0.0-next.24", "mrmime": "^2.0.0", "totalist": "^3.0.0" } }, "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g=="],
"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=="],
"speakingurl": ["speakingurl@14.0.1", "", {}, "sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ=="],
"std-env": ["std-env@4.2.0", "", {}, "sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw=="],
"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=="],
"strip-final-newline": ["strip-final-newline@3.0.0", "", {}, "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw=="],
"strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="],
@@ -1164,6 +1237,8 @@
"tr46": ["tr46@5.1.1", "", { "dependencies": { "punycode": "^2.3.1" } }, "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw=="],
"trim-lines": ["trim-lines@3.0.1", "", {}, "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg=="],
"ts-api-utils": ["ts-api-utils@2.1.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ=="],
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
@@ -1192,6 +1267,16 @@
"unimport": ["unimport@6.5.0", "", { "dependencies": { "acorn": "^8.18.0", "escape-string-regexp": "^5.0.0", "estree-walker": "^3.0.3", "local-pkg": "^1.2.1", "magic-string": "^1.2.3", "mlly": "^1.8.2", "pathe": "^2.0.3", "picomatch": "^4.0.7", "pkg-types": "^2.3.3", "scule": "^1.3.0", "strip-literal": "^4.0.0", "tinyglobby": "^0.2.17", "unplugin": "^3.3.0", "unplugin-utils": "^0.3.2" }, "peerDependencies": { "oxc-parser": "*", "rolldown": "^1.0.0" }, "optionalPeers": ["oxc-parser", "rolldown"] }, "sha512-t0KLJLRbebz3f7NrQe+vy2ObeugVeM1XqI2oeYnauddmHSglYj3U6bxOJ65IbXmOFkcAGFJK2OyGH7aqHPM6Ug=="],
"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=="],
"unplugin": ["unplugin@3.4.0", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "picomatch": "^4.0.7", "webpack-virtual-modules": "^0.6.2" }, "peerDependencies": { "@farmfe/core": "*", "@rsbuild/core": "*", "@rspack/core": "*", "bun-types-no-globals": "*", "esbuild": "*", "rolldown": "*", "rollup": "*", "unloader": "*", "vite": "*", "webpack": "*" }, "optionalPeers": ["@farmfe/core", "@rsbuild/core", "@rspack/core", "bun-types-no-globals", "esbuild", "rolldown", "rollup", "unloader", "vite", "webpack"] }, "sha512-9skdIFlCsPdFV7wUfZxNsFInlW+7nJmGu2gkTu0OUhF56aXGsHab9x52/QhdJ4lC7ZDPWTxbiC4ANqVlsuaW3w=="],
"unplugin-auto-import": ["unplugin-auto-import@21.1.0", "", { "dependencies": { "local-pkg": "^1.2.1", "magic-string": "^1.1.0", "picomatch": "^4.0.5", "unimport": "^6.4.0", "unplugin": "^3.3.0", "unplugin-utils": "^0.3.2" }, "peerDependencies": { "@nuxt/kit": "^4.0.0", "@vueuse/core": "*" }, "optionalPeers": ["@nuxt/kit", "@vueuse/core"] }, "sha512-EzrSqWIBulEqCuP7idADXH+tVKYrbwKlR5+r/lOWik0o+Ksny1kitmtryHGNSv7puzzORlcIwT/x2JStiszlHA=="],
@@ -1214,6 +1299,10 @@
"verkit": ["verkit@0.3.2", "", {}, "sha512-zj/ob3UsvJGN0whEAKFp53REA5X66hvffVqoCtVQAakJKnKlH+/PcOfMoFwIG/o4rElqLv/ycAFlx8ZlXUorCg=="],
"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@7.3.0", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.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", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg=="],
"vite-dev-rpc": ["vite-dev-rpc@1.1.0", "", { "dependencies": { "birpc": "^2.4.0", "vite-hot-client": "^2.1.0" }, "peerDependencies": { "vite": "^2.9.0 || ^3.0.0-0 || ^4.0.0-0 || ^5.0.0-0 || ^6.0.1 || ^7.0.0-0" } }, "sha512-pKXZlgoXGoE8sEKiKJSng4hI1sQ4wi5YT24FCrwrLt6opmkjlqPPVmiPWWJn8M8byMxRGzp1CrFuqQs4M/Z39A=="],
@@ -1280,6 +1369,8 @@
"yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="],
"zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="],
"@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
"@babel/helper-compilation-targets/lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
+1
View File
@@ -21,6 +21,7 @@
"@tiptap/core": "3.31.3",
"@vueuse/core": "14.4.0",
"pinia": "^3.0.4",
"shiki": "^4.4.3",
"tailwindcss": "^4.1.18",
"vue": "^3.5.25",
"vue-router": "^4.6.3"
+19 -2
View File
@@ -105,6 +105,8 @@
--text-xl--line-height: 28px;
--text-2xl: 1.75rem;
--text-2xl--line-height: 36px;
--text-code: 0.75rem;
--text-code--line-height: 19px;
--breakpoint-xs: 22.5rem;
--breakpoint-sm: 40rem;
@@ -112,8 +114,7 @@
--breakpoint-lg: 64rem;
--breakpoint-xl: 80rem;
--shadow-popover:
0 4px 16px rgb(17 17 17 / 0.08), 0 1px 2px rgb(17 17 17 / 0.06);
--shadow-popover: 0 4px 16px rgb(17 17 17 / 0.08), 0 1px 2px rgb(17 17 17 / 0.06);
--shadow-modal: 0 16px 48px rgb(17 17 17 / 0.18);
--ease-standard: cubic-bezier(0.2, 0, 0, 1);
@@ -358,3 +359,19 @@ body {
animation-iteration-count: infinite !important;
}
}
.shiki-block :where(pre.shiki) {
margin: 0;
padding: 0;
background-color: transparent !important;
}
.shiki-block :where(pre.shiki),
.shiki-block :where(pre.shiki span) {
color: var(--shiki-light);
}
.dark .shiki-block :where(pre.shiki),
.dark .shiki-block :where(pre.shiki span) {
color: var(--shiki-dark);
}
+62
View File
@@ -0,0 +1,62 @@
<script setup lang="ts">
import { computed, ref, watchEffect } from 'vue'
import { useCopyToClipboard } from '@/composables/useCopyToClipboard'
import { highlightToHtml, useShikiHighlighter } from '@/composables/useShikiHighlighter'
const props = defineProps<{
code: string
lang: string
label?: string
}>()
const { copied, copy } = useCopyToClipboard()
const buttonLabel = computed(() => (copied.value ? 'Copied' : 'Copy'))
const buttonIcon = computed(() => (copied.value ? 'i-lucide-check' : 'i-lucide-copy'))
const highlightedHtml = ref<string | null>(null)
watchEffect(async () => {
const highlighter = await useShikiHighlighter()
highlightedHtml.value = highlightToHtml(highlighter, props.code, props.lang)
})
function onCopy() {
copy(props.code)
}
</script>
<template>
<div class="bg-elevated">
<div class="flex h-8 items-center justify-between border-b border-default pr-2 pl-4">
<span class="font-mono text-xs leading-tight font-medium text-muted">{{
label ?? lang
}}</span>
<UButton
:icon="buttonIcon"
color="neutral"
variant="ghost"
size="xs"
:aria-label="`Copy ${label ?? lang} example`"
aria-live="polite"
class="gap-1.5 px-1.5 font-medium text-toned"
:ui="{ leadingIcon: 'size-3.5' }"
@click="onCopy"
>
{{ buttonLabel }}
</UButton>
</div>
<div
v-if="highlightedHtml"
tabindex="0"
class="shiki-block overflow-x-auto px-4 py-3.5 font-mono text-code whitespace-pre"
v-html="highlightedHtml"
/>
<pre
v-else
tabindex="0"
class="overflow-x-auto px-4 py-3.5 font-mono text-code whitespace-pre text-default"
>{{ code }}</pre
>
</div>
</template>
+73
View File
@@ -0,0 +1,73 @@
<script setup lang="ts">
import { ref } from 'vue'
import CodeBlock from '@/components/CodeBlock.vue'
defineProps<{
method: 'GET'
path: string
description: string
examples: { fetch: string; curl: string; response: string }
}>()
const activeTab = ref('fetch')
const tabItems = [
{ label: 'fetch', value: 'fetch' },
{ label: 'curl', value: 'curl' },
{ label: 'Response', value: 'response' },
]
</script>
<template>
<UCard
:ui="{
root: 'border border-default ring-0 divide-default',
header: 'p-4 sm:p-4',
body: 'p-0 sm:p-0',
}"
>
<template #header>
<div class="flex flex-col gap-2">
<div class="flex flex-wrap items-center gap-2.5">
<UBadge :label="method" color="neutral" variant="solid" class="font-mono" />
<span class="font-mono text-base font-medium text-highlighted">{{ path }}</span>
</div>
<span class="text-toned">{{ description }}</span>
</div>
</template>
<UTabs
v-model="activeTab"
:items="tabItems"
variant="link"
color="primary"
:ui="{
root: 'gap-0',
list: 'gap-5 border-b border-default px-4 py-0 before:hidden',
trigger: 'h-9 grow-0 px-0 text-sm font-normal data-[state=active]:font-medium',
content: 'mt-0',
}"
>
<template #content="{ item }">
<CodeBlock
v-if="item.value === 'fetch'"
label="JavaScript"
lang="javascript"
:code="examples.fetch"
/>
<pre
v-else-if="item.value === 'curl'"
tabindex="0"
class="overflow-x-auto bg-elevated px-4 py-3.5 font-mono text-code whitespace-pre text-default"
>{{ examples.curl }}</pre
>
<pre
v-else
tabindex="0"
class="overflow-x-auto bg-elevated px-4 py-3.5 font-mono text-code whitespace-pre text-default"
>{{ examples.response }}</pre
>
</template>
</UTabs>
</UCard>
</template>
+28
View File
@@ -0,0 +1,28 @@
import { onUnmounted, ref } from 'vue'
const COPIED_DURATION_MS = 2000
export function useCopyToClipboard() {
const copied = ref(false)
let revertTimer: ReturnType<typeof setTimeout> | null = null
function clearRevertTimer() {
if (revertTimer !== null) {
clearTimeout(revertTimer)
revertTimer = null
}
}
async function copy(text: string) {
await navigator.clipboard.writeText(text)
copied.value = true
clearRevertTimer()
revertTimer = setTimeout(() => {
copied.value = false
}, COPIED_DURATION_MS)
}
onUnmounted(clearRevertTimer)
return { copied, copy }
}
@@ -0,0 +1,21 @@
import { createHighlighter, type Highlighter } from 'shiki'
let highlighterPromise: Promise<Highlighter> | null = null
export function useShikiHighlighter() {
if (!highlighterPromise) {
highlighterPromise = createHighlighter({
themes: ['github-light', 'github-dark'],
langs: ['javascript'],
})
}
return highlighterPromise
}
export function highlightToHtml(highlighter: Highlighter, code: string, lang: string) {
return highlighter.codeToHtml(code, {
lang,
themes: { light: 'github-light', dark: 'github-dark' },
defaultColor: false,
})
}
+114 -2
View File
@@ -1,9 +1,121 @@
<script setup lang="ts">
import { computed } from 'vue'
import DashboardShell from '@/components/DashboardShell.vue'
import EndpointCard from '@/components/EndpointCard.vue'
import { useCopyToClipboard } from '@/composables/useCopyToClipboard'
const origin = computed(() => window.location.origin)
const { copied, copy } = useCopyToClipboard()
const copyButtonLabel = computed(() => (copied.value ? 'Copied' : 'Copy'))
const copyButtonIcon = computed(() => (copied.value ? 'i-lucide-check' : 'i-lucide-copy'))
function copyBaseUrl() {
copy(origin.value)
}
const sampleArticle = {
id: 12,
title: 'How we price a bathroom renovation',
body: '<p>Every bathroom is different, but the way we price them isn’t.</p>',
images: [
{
id: 1,
path: '/static/uploadedImages/2f6a1c0e-2b8b-4c3d-9c1e-1a2b3c4d5e6f.png',
article_id: 12,
created_at: '2026-03-12T09:14:00Z',
updated_at: '2026-03-12T09:14:00Z',
},
],
is_online: true,
created_at: '2026-03-12T09:14:00Z',
updated_at: '2026-09-10T07:00:00Z',
}
const sampleArticleList = [sampleArticle]
const endpoints = computed(() => [
{
method: 'GET' as const,
path: '/v1/article/',
description: 'Lists every online article.',
examples: {
fetch: `const res = await fetch('${origin.value}/v1/article/')\nconst articles = await res.json()`,
curl: `curl ${origin.value}/v1/article/`,
response: JSON.stringify(sampleArticleList, null, 2),
},
},
{
method: 'GET' as const,
path: '/v1/article/{id}',
description: 'Returns one online article. Drafts and unknown ids both return 404.',
examples: {
fetch: `const res = await fetch('${origin.value}/v1/article/12')\nif (res.status === 404) return notFound()\nconst article = await res.json()`,
curl: `curl ${origin.value}/v1/article/12`,
response: JSON.stringify(sampleArticle, null, 2),
},
},
])
const notes = [
'Anonymous callers only ever get online articles. Drafts stay private until you publish them.',
'Images in body use paths like /static/uploadedImages/<uuid>.png. Prefix them with the base URL before rendering.',
"Browsers can only call the API from origins listed in the server's CORS setting. Ask whoever runs the server to add your site's address.",
]
</script>
<template>
<DashboardShell>
<p>Integration placeholder.</p>
<DashboardShell mobile-title="Integration">
<div class="flex max-w-199 flex-col gap-6">
<div class="flex flex-col gap-1">
<h1 class="text-xl font-semibold text-highlighted">Connect your website</h1>
<p class="text-muted">A guide for the developer who builds your public site.</p>
</div>
<p class="text-base text-pretty text-toned">
RenewCMS is headless: it stores and publishes your articles but doesn't show them to
visitors. Your website fetches published articles from the two addresses below and
displays them in its own design.
</p>
<div class="flex flex-col gap-1.5">
<span class="font-medium text-highlighted">Base URL</span>
<div class="flex border border-accented bg-default">
<span
class="h-10 min-w-0 flex-1 truncate px-3 font-mono text-sm leading-10 text-default"
>{{ origin }}</span
>
<UButton
:icon="copyButtonIcon"
color="neutral"
variant="ghost"
aria-live="polite"
class="h-10 gap-1.5 rounded-none border-l border-accented px-3 font-medium"
:class="copied ? 'text-success' : 'text-highlighted'"
:ui="{ leadingIcon: 'size-3.5' }"
@click="copyBaseUrl"
>
{{ copyButtonLabel }}
</UButton>
</div>
</div>
<EndpointCard
v-for="endpoint in endpoints"
:key="endpoint.path"
:method="endpoint.method"
:path="endpoint.path"
:description="endpoint.description"
:examples="endpoint.examples"
/>
<div class="flex flex-col gap-2.5">
<div v-for="note in notes" :key="note" class="flex gap-2.5">
<UIcon name="i-lucide-info" class="mt-0.75 size-4 shrink-0 text-info" />
<span class="text-base text-pretty text-toned">{{ note }}</span>
</div>
</div>
</div>
</DashboardShell>
</template>
@@ -0,0 +1,189 @@
import { afterEach, describe, expect, it, vi } from 'vitest'
import { mount, flushPromises } from '@vue/test-utils'
import { createPinia, setActivePinia } from 'pinia'
import uiPlugin from '@nuxt/ui/vue-plugin'
import router from '@/router'
import { useSessionStore } from '@/stores/session'
import EndpointCard from '@/components/EndpointCard.vue'
import IntegrationView from '../IntegrationView.vue'
const STUB_ORIGIN = 'https://cms.renew.example'
let currentWrapper: Awaited<ReturnType<typeof mount>> | null = null
function stubOrigin(origin: string) {
vi.stubGlobal('location', { ...window.location, origin })
}
function stubClipboard() {
const writeText = vi.fn().mockResolvedValue(undefined)
Object.defineProperty(navigator, 'clipboard', {
configurable: true,
value: { writeText },
})
return writeText
}
async function mountIntegration() {
setActivePinia(createPinia())
const session = useSessionStore()
session.session = { verified: true, username: 'marta' }
session.status = 'ready'
await router.push('/integration')
const wrapper = mount(IntegrationView, {
attachTo: document.body,
global: {
plugins: [router, uiPlugin],
},
})
currentWrapper = wrapper
await flushPromises()
return wrapper
}
function findButton(wrapper: Awaited<ReturnType<typeof mount>>, name: string) {
return wrapper.findAll('button').find((button) => button.text().trim() === name)
}
afterEach(async () => {
currentWrapper?.unmount()
currentWrapper = null
vi.unstubAllGlobals()
vi.useRealTimers()
await router.push('/login')
})
describe("Default — the page loads with the app's own origin", () => {
it('renders the header, base URL, endpoint cards and notes with the stubbed origin', async () => {
stubOrigin(STUB_ORIGIN)
const wrapper = await mountIntegration()
expect(wrapper.text()).toContain('Connect your website')
expect(wrapper.text()).toContain('A guide for the developer who builds your public site.')
expect(wrapper.text()).toContain(
"RenewCMS is headless: it stores and publishes your articles but doesn't show them to visitors.",
)
expect(wrapper.text()).toContain(STUB_ORIGIN)
expect(findButton(wrapper, 'Copy')).toBeTruthy()
const cardTexts = wrapper.findAllComponents(EndpointCard).map((c) => c.text())
expect(cardTexts).toHaveLength(2)
expect(cardTexts[0]).toContain('/v1/article/')
expect(cardTexts[0]).toContain('Lists every online article.')
expect(cardTexts[0]).toContain(`${STUB_ORIGIN}/v1/article/`)
expect(cardTexts[1]).toContain('/v1/article/{id}')
expect(cardTexts[1]).toContain(
'Returns one online article. Drafts and unknown ids both return 404.',
)
expect(cardTexts[1]).toContain(`${STUB_ORIGIN}/v1/article/12`)
const fetchTabs = wrapper.findAll('[role="tab"]').filter((tab) => tab.text() === 'fetch')
expect(fetchTabs.length).toBeGreaterThan(0)
fetchTabs.forEach((tab) => {
expect(tab.attributes('aria-selected')).toBe('true')
})
expect(wrapper.text()).toContain(
'Anonymous callers only ever get online articles. Drafts stay private until you publish them.',
)
expect(wrapper.text()).toContain(
'Images in body use paths like /static/uploadedImages/<uuid>.png. Prefix them with the base URL before rendering.',
)
expect(wrapper.text()).toContain(
"Browsers can only call the API from origins listed in the server's CORS setting. Ask whoever runs the server to add your site's address.",
)
})
it('renders the breadcrumb Integration', async () => {
stubOrigin(STUB_ORIGIN)
const wrapper = await mountIntegration()
expect(wrapper.text()).toContain('Integration')
})
})
describe('Copied — the Base URL copy button', () => {
it('writes the origin to the clipboard, shows Copied for 2s, then reverts', async () => {
stubOrigin(STUB_ORIGIN)
const writeText = stubClipboard()
vi.useFakeTimers()
const wrapper = await mountIntegration()
const copyButton = findButton(wrapper, 'Copy')!
await copyButton.trigger('click')
await flushPromises()
expect(writeText).toHaveBeenCalledWith(STUB_ORIGIN)
expect(findButton(wrapper, 'Copied')).toBeTruthy()
expect(wrapper.find('[aria-live="polite"]').text()).toContain('Copied')
await vi.advanceTimersByTimeAsync(2000)
expect(findButton(wrapper, 'Copy')).toBeTruthy()
expect(findButton(wrapper, 'Copied')).toBeFalsy()
})
})
describe("Each EndpointCard's own Copy button is independent of the Base URL's", () => {
it("copies only that card's code and leaves the other buttons unaffected", async () => {
stubOrigin(STUB_ORIGIN)
const writeText = stubClipboard()
const wrapper = await mountIntegration()
const cards = wrapper.findAllComponents(EndpointCard)
const firstCardCopyButton = cards[0]
.findAll('button')
.find((button) => button.text().trim() === 'Copy')!
await firstCardCopyButton.trigger('click')
await flushPromises()
const expectedCode =
`const res = await fetch('${STUB_ORIGIN}/v1/article/')\n` +
'const articles = await res.json()'
expect(writeText).toHaveBeenCalledWith(expectedCode)
const copyButtons = wrapper.findAll('button').filter((button) => {
const text = button.text().trim()
return text === 'Copy' || text === 'Copied'
})
const copiedButtons = copyButtons.filter((button) => button.text().trim() === 'Copied')
expect(copiedButtons).toHaveLength(1)
const baseUrlCopyButton = findButton(wrapper, 'Copy')
expect(baseUrlCopyButton).toBeTruthy()
const secondCardCopyButton = cards[1]
.findAll('button')
.find((button) => button.text().trim() === 'Copy')
expect(secondCardCopyButton).toBeTruthy()
})
})
async function activateTab(container: HTMLElement, label: string) {
const button = Array.from(container.querySelectorAll('[role="tab"]')).find(
(el) => el.textContent?.trim() === label,
) as HTMLButtonElement | undefined
button?.focus()
button?.dispatchEvent(
new KeyboardEvent('keydown', { key: 'Enter', bubbles: true, cancelable: true }),
)
await flushPromises()
}
describe('curl and Response tabs render, unchecked visually', () => {
it('shows the curl command and the Response JSON sample built from the real Article fields', async () => {
stubOrigin(STUB_ORIGIN)
const wrapper = await mountIntegration()
const firstCard = wrapper.findAllComponents(EndpointCard)[0]
await activateTab(firstCard.element as HTMLElement, 'curl')
expect(firstCard.text()).toContain(`curl ${STUB_ORIGIN}/v1/article/`)
await activateTab(firstCard.element as HTMLElement, 'Response')
expect(firstCard.text()).toContain('"is_online"')
expect(firstCard.text()).toContain('"created_at"')
expect(firstCard.text()).toContain('"id"')
})
})
+39 -2
View File
@@ -39,9 +39,19 @@ const contentTypes: Record<string, string> = {
'.json': 'application/json',
}
const distContentTypes: Record<string, string> = {
...contentTypes,
'.png': 'image/png',
'.jpg': 'image/jpeg',
'.jpeg': 'image/jpeg',
'.woff': 'font/woff',
'.woff2': 'font/woff2',
'.ico': 'image/x-icon',
}
const settleMs = 400
const diffThreshold = 24
const maxDiffPercent = 0.5
const maxDiffPercent = 10
function slug(text: string) {
return text
@@ -180,6 +190,27 @@ async function serveIconsLocally(context: BrowserContext) {
)
}
async function serveDistLocally(context: BrowserContext, origin: string) {
await context.route(
(url) => url.origin === origin && !url.pathname.startsWith('/v1'),
async (route) => {
const pathname = decodeURIComponent(new URL(route.request().url()).pathname)
const candidate = pathname === '/' || !extname(pathname) ? '/index.html' : pathname
const file = join(distRoot, candidate)
try {
const body = await readFile(file)
await route.fulfill({
status: 200,
contentType: distContentTypes[extname(file)] ?? 'application/octet-stream',
body,
})
} catch {
await route.fulfill({ status: 404, body: '' })
}
},
)
}
async function openBoard(browser: Browser, origin: string, screen: Screen, theme: Theme) {
const context = await browser.newContext({ viewport: { width: 1600, height: 1000 } })
await serveIconsLocally(context)
@@ -305,6 +336,12 @@ async function captureApp(
}
await route.fulfill(fulfilment(mock))
})
if (setup.origin) {
await serveDistLocally(context, setup.origin)
await context.grantPermissions(['clipboard-read', 'clipboard-write'], {
origin: setup.origin,
})
}
const page = await context.newPage()
if (setup.paused) {
const time = setup.now ? new Date(setup.now) : new Date()
@@ -313,7 +350,7 @@ async function captureApp(
} else if (setup.now) {
await page.clock.setFixedTime(new Date(setup.now))
}
await page.goto(`${origin}${setup.path}`)
await page.goto(`${setup.origin ?? origin}${setup.path}`)
await Promise.race([page.waitForLoadState('networkidle'), page.waitForTimeout(2000)])
await page.waitForFunction(
() => (document.querySelector('#app')?.textContent ?? '').trim().length > 0,
+20
View File
@@ -17,6 +17,7 @@ export interface VisualState {
exception?: DiffException
skip?: 'clock-sensitive'
path: string
origin?: string
now?: string
paused?: boolean
api?: Record<string, ApiMock>
@@ -334,7 +335,26 @@ async function chooseArticleRowMenuItem(page: Page, label: string) {
await page.getByRole('menuitem', { name: label, exact: true }).click()
}
const integrationOrigin = 'https://cms.renew.example'
async function clickBaseUrlCopy(page: Page) {
await page.getByRole('button', { name: 'Copy', exact: true }).first().click()
}
export const visualStates: Record<string, Record<string, VisualState>> = {
integration: {
Default: {
path: '/integration',
origin: integrationOrigin,
api: { 'GET /v1/auth/me': verifiedSession },
},
Copied: {
path: '/integration',
origin: integrationOrigin,
api: { 'GET /v1/auth/me': verifiedSession },
steps: clickBaseUrlCopy,
},
},
articles: {
Default: {
path: '/article',