Compare commits
7 Commits
67140bb47e
...
0c8b891289
| Author | SHA1 | Date | |
|---|---|---|---|
| 0c8b891289 | |||
| 2cb7827938 | |||
| 8810e9759a | |||
| 8837be52cd | |||
| cec56f2896 | |||
| 36f0371132 | |||
| da5b2a1c23 |
25
app/bun.lock
Executable file → Normal file
25
app/bun.lock
Executable file → Normal file
@ -5,11 +5,16 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fastify/middie": "^9.0.3",
|
"@fastify/middie": "^9.0.3",
|
||||||
"@fastify/static": "^8.1.1",
|
"@fastify/static": "^8.1.1",
|
||||||
|
"@sindresorhus/slugify": "^2.2.1",
|
||||||
"@universal-middleware/core": "^0.4.7",
|
"@universal-middleware/core": "^0.4.7",
|
||||||
"@universal-middleware/fastify": "^0.5.16",
|
"@universal-middleware/fastify": "^0.5.16",
|
||||||
|
"clsx": "^2.1.1",
|
||||||
"fastify": "^5.3.0",
|
"fastify": "^5.3.0",
|
||||||
|
"solid-heroicons": "^3.2.4",
|
||||||
|
"solid-highlight": "^0.1.26",
|
||||||
"solid-js": "^1.9.5",
|
"solid-js": "^1.9.5",
|
||||||
"telefunc": "^0.2.3",
|
"telefunc": "^0.2.3",
|
||||||
|
"terracotta": "^1.0.6",
|
||||||
"vike": "^0.4.228",
|
"vike": "^0.4.228",
|
||||||
"vike-solid": "^0.7.9",
|
"vike-solid": "^0.7.9",
|
||||||
},
|
},
|
||||||
@ -257,6 +262,10 @@
|
|||||||
|
|
||||||
"@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.40.0", "", { "os": "win32", "cpu": "x64" }, "sha512-lpPE1cLfP5oPzVjKMx10pgBmKELQnFJXHgvtHCtuJWOv8MxqdEIMNtgHgBFf7Ea2/7EuVwa9fodWUfXAlXZLZQ=="],
|
"@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.40.0", "", { "os": "win32", "cpu": "x64" }, "sha512-lpPE1cLfP5oPzVjKMx10pgBmKELQnFJXHgvtHCtuJWOv8MxqdEIMNtgHgBFf7Ea2/7EuVwa9fodWUfXAlXZLZQ=="],
|
||||||
|
|
||||||
|
"@sindresorhus/slugify": ["@sindresorhus/slugify@2.2.1", "", { "dependencies": { "@sindresorhus/transliterate": "^1.0.0", "escape-string-regexp": "^5.0.0" } }, "sha512-MkngSCRZ8JdSOCHRaYd+D01XhvU3Hjy6MGl06zhOk614hp9EOAp5gIkBeQg7wtmxpitU6eAL4kdiRMcJa2dlrw=="],
|
||||||
|
|
||||||
|
"@sindresorhus/transliterate": ["@sindresorhus/transliterate@1.6.0", "", { "dependencies": { "escape-string-regexp": "^5.0.0" } }, "sha512-doH1gimEu3A46VX6aVxpHTeHrytJAG6HgdxntYnCFiIFHEM/ZGpG8KiZGBChchjQmG0XFIBL552kBTjVcMZXwQ=="],
|
||||||
|
|
||||||
"@tailwindcss/node": ["@tailwindcss/node@4.1.4", "", { "dependencies": { "enhanced-resolve": "^5.18.1", "jiti": "^2.4.2", "lightningcss": "1.29.2", "tailwindcss": "4.1.4" } }, "sha512-MT5118zaiO6x6hNA04OWInuAiP1YISXql8Z+/Y8iisV5nuhM8VXlyhRuqc2PEviPszcXI66W44bCIk500Oolhw=="],
|
"@tailwindcss/node": ["@tailwindcss/node@4.1.4", "", { "dependencies": { "enhanced-resolve": "^5.18.1", "jiti": "^2.4.2", "lightningcss": "1.29.2", "tailwindcss": "4.1.4" } }, "sha512-MT5118zaiO6x6hNA04OWInuAiP1YISXql8Z+/Y8iisV5nuhM8VXlyhRuqc2PEviPszcXI66W44bCIk500Oolhw=="],
|
||||||
|
|
||||||
"@tailwindcss/oxide": ["@tailwindcss/oxide@4.1.4", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.1.4", "@tailwindcss/oxide-darwin-arm64": "4.1.4", "@tailwindcss/oxide-darwin-x64": "4.1.4", "@tailwindcss/oxide-freebsd-x64": "4.1.4", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.4", "@tailwindcss/oxide-linux-arm64-gnu": "4.1.4", "@tailwindcss/oxide-linux-arm64-musl": "4.1.4", "@tailwindcss/oxide-linux-x64-gnu": "4.1.4", "@tailwindcss/oxide-linux-x64-musl": "4.1.4", "@tailwindcss/oxide-wasm32-wasi": "4.1.4", "@tailwindcss/oxide-win32-arm64-msvc": "4.1.4", "@tailwindcss/oxide-win32-x64-msvc": "4.1.4" } }, "sha512-p5wOpXyOJx7mKh5MXh5oKk+kqcz8T+bA3z/5VWWeQwFrmuBItGwz8Y2CHk/sJ+dNb9B0nYFfn0rj/cKHZyjahQ=="],
|
"@tailwindcss/oxide": ["@tailwindcss/oxide@4.1.4", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.1.4", "@tailwindcss/oxide-darwin-arm64": "4.1.4", "@tailwindcss/oxide-darwin-x64": "4.1.4", "@tailwindcss/oxide-freebsd-x64": "4.1.4", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.4", "@tailwindcss/oxide-linux-arm64-gnu": "4.1.4", "@tailwindcss/oxide-linux-arm64-musl": "4.1.4", "@tailwindcss/oxide-linux-x64-gnu": "4.1.4", "@tailwindcss/oxide-linux-x64-musl": "4.1.4", "@tailwindcss/oxide-wasm32-wasi": "4.1.4", "@tailwindcss/oxide-win32-arm64-msvc": "4.1.4", "@tailwindcss/oxide-win32-x64-msvc": "4.1.4" } }, "sha512-p5wOpXyOJx7mKh5MXh5oKk+kqcz8T+bA3z/5VWWeQwFrmuBItGwz8Y2CHk/sJ+dNb9B0nYFfn0rj/cKHZyjahQ=="],
|
||||||
@ -369,6 +378,8 @@
|
|||||||
|
|
||||||
"chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
|
"chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
|
||||||
|
|
||||||
|
"clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
|
||||||
|
|
||||||
"code-block-writer": ["code-block-writer@12.0.0", "", {}, "sha512-q4dMFMlXtKR3XNBHyMHt/3pwYNA69EDk00lloMOaaUMKPUXBw6lpXtbu3MMVG6/uOihGnRDOlkyqsONEUj60+w=="],
|
"code-block-writer": ["code-block-writer@12.0.0", "", {}, "sha512-q4dMFMlXtKR3XNBHyMHt/3pwYNA69EDk00lloMOaaUMKPUXBw6lpXtbu3MMVG6/uOihGnRDOlkyqsONEUj60+w=="],
|
||||||
|
|
||||||
"color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
|
"color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
|
||||||
@ -663,6 +674,8 @@
|
|||||||
|
|
||||||
"prettier-linter-helpers": ["prettier-linter-helpers@1.0.0", "", { "dependencies": { "fast-diff": "^1.1.2" } }, "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w=="],
|
"prettier-linter-helpers": ["prettier-linter-helpers@1.0.0", "", { "dependencies": { "fast-diff": "^1.1.2" } }, "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w=="],
|
||||||
|
|
||||||
|
"prismjs": ["prismjs@1.30.0", "", {}, "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw=="],
|
||||||
|
|
||||||
"process-warning": ["process-warning@5.0.0", "", {}, "sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA=="],
|
"process-warning": ["process-warning@5.0.0", "", {}, "sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA=="],
|
||||||
|
|
||||||
"punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
|
"punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
|
||||||
@ -719,10 +732,16 @@
|
|||||||
|
|
||||||
"sirv": ["sirv@3.0.1", "", { "dependencies": { "@polka/url": "^1.0.0-next.24", "mrmime": "^2.0.0", "totalist": "^3.0.0" } }, "sha512-FoqMu0NCGBLCcAkS1qA+XJIQTR6/JHfQXl+uGteNCQ76T91DMUjPa9xfmeqMY3z80nLSg9yQmNjK0Px6RWsH/A=="],
|
"sirv": ["sirv@3.0.1", "", { "dependencies": { "@polka/url": "^1.0.0-next.24", "mrmime": "^2.0.0", "totalist": "^3.0.0" } }, "sha512-FoqMu0NCGBLCcAkS1qA+XJIQTR6/JHfQXl+uGteNCQ76T91DMUjPa9xfmeqMY3z80nLSg9yQmNjK0Px6RWsH/A=="],
|
||||||
|
|
||||||
|
"solid-heroicons": ["solid-heroicons@3.2.4", "", { "dependencies": { "solid-js": "^1.7.6" } }, "sha512-u6BMdFLvkJnvUGYzdFcWp1wvJ4hb9Y1zd3AbZ9D3bUmmiy9jBzNZX+RcqBCI2EKRvdQwAb1UB9bkESfqfhayDg=="],
|
||||||
|
|
||||||
|
"solid-highlight": ["solid-highlight@0.1.26", "", { "peerDependencies": { "prismjs": "^1.29.0", "solid-js": "^1.8.0" } }, "sha512-Iw1mi3vE+YCBBBU/+HHc5y8VNULaGXUX4OK2c9TbzegOGCitzW0uNIvb0s3S0KoVv7Uma/KadWHNFXkwZCwPgQ=="],
|
||||||
|
|
||||||
"solid-js": ["solid-js@1.9.5", "", { "dependencies": { "csstype": "^3.1.0", "seroval": "^1.1.0", "seroval-plugins": "^1.1.0" } }, "sha512-ogI3DaFcyn6UhYhrgcyRAMbu/buBJitYQASZz5WzfQVPP10RD2AbCoRZ517psnezrasyCbWzIxZ6kVqet768xw=="],
|
"solid-js": ["solid-js@1.9.5", "", { "dependencies": { "csstype": "^3.1.0", "seroval": "^1.1.0", "seroval-plugins": "^1.1.0" } }, "sha512-ogI3DaFcyn6UhYhrgcyRAMbu/buBJitYQASZz5WzfQVPP10RD2AbCoRZ517psnezrasyCbWzIxZ6kVqet768xw=="],
|
||||||
|
|
||||||
"solid-refresh": ["solid-refresh@0.6.3", "", { "dependencies": { "@babel/generator": "^7.23.6", "@babel/helper-module-imports": "^7.22.15", "@babel/types": "^7.23.6" }, "peerDependencies": { "solid-js": "^1.3" } }, "sha512-F3aPsX6hVw9ttm5LYlth8Q15x6MlI/J3Dn+o3EQyRTtTxidepSTwAYdozt01/YA+7ObcciagGEyXIopGZzQtbA=="],
|
"solid-refresh": ["solid-refresh@0.6.3", "", { "dependencies": { "@babel/generator": "^7.23.6", "@babel/helper-module-imports": "^7.22.15", "@babel/types": "^7.23.6" }, "peerDependencies": { "solid-js": "^1.3" } }, "sha512-F3aPsX6hVw9ttm5LYlth8Q15x6MlI/J3Dn+o3EQyRTtTxidepSTwAYdozt01/YA+7ObcciagGEyXIopGZzQtbA=="],
|
||||||
|
|
||||||
|
"solid-use": ["solid-use@0.9.1", "", { "peerDependencies": { "solid-js": "^1.7" } }, "sha512-UwvXDVPlrrbj/9ewG9ys5uL2IO4jSiwys2KPzK4zsnAcmEl7iDafZWW1Mo4BSEWOmQCGK6IvpmGHo1aou8iOFw=="],
|
||||||
|
|
||||||
"sonic-boom": ["sonic-boom@4.2.0", "", { "dependencies": { "atomic-sleep": "^1.0.0" } }, "sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww=="],
|
"sonic-boom": ["sonic-boom@4.2.0", "", { "dependencies": { "atomic-sleep": "^1.0.0" } }, "sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww=="],
|
||||||
|
|
||||||
"source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
|
"source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
|
||||||
@ -757,6 +776,8 @@
|
|||||||
|
|
||||||
"telefunc": ["telefunc@0.2.3", "", { "dependencies": { "@brillout/import": "^0.2.6", "@brillout/json-serializer": "^0.5.6", "@brillout/picocolors": "^1.0.26", "@brillout/vite-plugin-server-entry": "^0.7.5", "es-module-lexer": "^1.6.0", "magic-string": "^0.30.17", "ts-morph": "^19.0.0" }, "peerDependencies": { "@babel/core": ">=7.0.0", "@babel/parser": ">=7.0.0", "@babel/types": ">=7.0.0", "react": ">=18.0.0", "react-streaming": ">=0.3.3" }, "optionalPeers": ["@babel/core", "@babel/parser", "@babel/types", "react", "react-streaming"] }, "sha512-BZejs1iUrsodzti34+QvgaBUPMIf9H0eUJB0f1VzwVeU3mflvdNKZ5KHOP7iiQYyiNfFjcUbUktMFrjA2J8sHg=="],
|
"telefunc": ["telefunc@0.2.3", "", { "dependencies": { "@brillout/import": "^0.2.6", "@brillout/json-serializer": "^0.5.6", "@brillout/picocolors": "^1.0.26", "@brillout/vite-plugin-server-entry": "^0.7.5", "es-module-lexer": "^1.6.0", "magic-string": "^0.30.17", "ts-morph": "^19.0.0" }, "peerDependencies": { "@babel/core": ">=7.0.0", "@babel/parser": ">=7.0.0", "@babel/types": ">=7.0.0", "react": ">=18.0.0", "react-streaming": ">=0.3.3" }, "optionalPeers": ["@babel/core", "@babel/parser", "@babel/types", "react", "react-streaming"] }, "sha512-BZejs1iUrsodzti34+QvgaBUPMIf9H0eUJB0f1VzwVeU3mflvdNKZ5KHOP7iiQYyiNfFjcUbUktMFrjA2J8sHg=="],
|
||||||
|
|
||||||
|
"terracotta": ["terracotta@1.0.6", "", { "dependencies": { "solid-use": "^0.9.0" }, "peerDependencies": { "solid-js": "^1.8" } }, "sha512-yVrmT/Lg6a3tEbeYEJH8ksb1PYkR5FA9k5gr1TchaSNIiA2ZWs5a+koEbePXwlBP0poaV7xViZ/v50bQFcMgqw=="],
|
||||||
|
|
||||||
"thread-stream": ["thread-stream@3.1.0", "", { "dependencies": { "real-require": "^0.2.0" } }, "sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A=="],
|
"thread-stream": ["thread-stream@3.1.0", "", { "dependencies": { "real-require": "^0.2.0" } }, "sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A=="],
|
||||||
|
|
||||||
"tinyglobby": ["tinyglobby@0.2.12", "", { "dependencies": { "fdir": "^6.4.3", "picomatch": "^4.0.2" } }, "sha512-qkf4trmKSIiMTs/E63cxH+ojC2unam7rJ0WrauAzpT3ECNTxGRMlaXxVbfxMUC/w0LaYk6jQ4y/nGR9uBO3tww=="],
|
"tinyglobby": ["tinyglobby@0.2.12", "", { "dependencies": { "fdir": "^6.4.3", "picomatch": "^4.0.2" } }, "sha512-qkf4trmKSIiMTs/E63cxH+ojC2unam7rJ0WrauAzpT3ECNTxGRMlaXxVbfxMUC/w0LaYk6jQ4y/nGR9uBO3tww=="],
|
||||||
@ -835,6 +856,10 @@
|
|||||||
|
|
||||||
"@humanfs/node/@humanwhocodes/retry": ["@humanwhocodes/retry@0.3.1", "", {}, "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA=="],
|
"@humanfs/node/@humanwhocodes/retry": ["@humanwhocodes/retry@0.3.1", "", {}, "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA=="],
|
||||||
|
|
||||||
|
"@sindresorhus/slugify/escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="],
|
||||||
|
|
||||||
|
"@sindresorhus/transliterate/escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="],
|
||||||
|
|
||||||
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.4.3", "", { "dependencies": { "@emnapi/wasi-threads": "1.0.2", "tslib": "^2.4.0" }, "bundled": true }, "sha512-4m62DuCE07lw01soJwPiBGC0nAww0Q+RY70VZ+n49yDIO13yyinhbWCeNnaob0lakDtWQzSdtNWzJeOJt2ma+g=="],
|
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.4.3", "", { "dependencies": { "@emnapi/wasi-threads": "1.0.2", "tslib": "^2.4.0" }, "bundled": true }, "sha512-4m62DuCE07lw01soJwPiBGC0nAww0Q+RY70VZ+n49yDIO13yyinhbWCeNnaob0lakDtWQzSdtNWzJeOJt2ma+g=="],
|
||||||
|
|
||||||
"@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.4.3", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-pBPWdu6MLKROBX05wSNKcNb++m5Er+KQ9QkB+WVM+pW2Kx9hoSrVTnu3BdkI5eBLZoKu/J6mW/B6i6bJB2ytXQ=="],
|
"@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.4.3", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-pBPWdu6MLKROBX05wSNKcNb++m5Er+KQ9QkB+WVM+pW2Kx9hoSrVTnu3BdkI5eBLZoKu/J6mW/B6i6bJB2ytXQ=="],
|
||||||
|
|||||||
52
app/components/Button.tsx
Normal file
52
app/components/Button.tsx
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
import type { JSX } from "solid-js";
|
||||||
|
|
||||||
|
import { Link } from "./Link";
|
||||||
|
import clsx from "clsx";
|
||||||
|
|
||||||
|
const variantStyles = {
|
||||||
|
primary:
|
||||||
|
"bg-violet-300 font-semibold text-slate-900 hover:bg-violet-200 focus:outline-hidden focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-violet-300/50 active:bg-violet-500",
|
||||||
|
secondary:
|
||||||
|
"bg-slate-800 font-medium text-white hover:bg-slate-700 focus:outline-hidden focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-white/50 active:text-slate-400",
|
||||||
|
ghost:
|
||||||
|
"bg-transparent font-medium text-slate-900 dark:text-slate-400 hover:bg-slate-100 focus:outline-hidden focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-slate-300/50 active:bg-slate-200",
|
||||||
|
};
|
||||||
|
|
||||||
|
const sizeStyles = {
|
||||||
|
sm: "rounded-md py-1 px-2 text-xs",
|
||||||
|
md: "rounded-full py-2 px-4 text-sm",
|
||||||
|
lg: "rounded-full py-3 px-6 text-base",
|
||||||
|
};
|
||||||
|
|
||||||
|
type ButtonProps = {
|
||||||
|
variant?: keyof typeof variantStyles;
|
||||||
|
size?: keyof typeof sizeStyles;
|
||||||
|
className?: string;
|
||||||
|
} & (
|
||||||
|
| JSX.IntrinsicElements["button"]
|
||||||
|
| (JSX.IntrinsicElements["a"] & { href: string })
|
||||||
|
);
|
||||||
|
|
||||||
|
export function Button({
|
||||||
|
variant = "primary",
|
||||||
|
size = "md",
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: ButtonProps) {
|
||||||
|
className = clsx(
|
||||||
|
variantStyles[variant],
|
||||||
|
sizeStyles[size],
|
||||||
|
"cursor-pointer",
|
||||||
|
className,
|
||||||
|
);
|
||||||
|
|
||||||
|
return "href" in props && props.href ? (
|
||||||
|
<Link
|
||||||
|
class={className}
|
||||||
|
href={props.href}
|
||||||
|
{...(props as JSX.IntrinsicElements["a"])}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<button class={className} {...(props as JSX.IntrinsicElements["button"])} />
|
||||||
|
);
|
||||||
|
}
|
||||||
74
app/components/Callout.tsx
Normal file
74
app/components/Callout.tsx
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
import type { JSX } from "solid-js";
|
||||||
|
|
||||||
|
import { Icon } from "./Icon";
|
||||||
|
import clsx from "clsx";
|
||||||
|
|
||||||
|
const styles = {
|
||||||
|
note: {
|
||||||
|
container:
|
||||||
|
"bg-violet-50 dark:bg-violet-800/60 dark:ring-1 dark:ring-violet-300/10",
|
||||||
|
title: "text-violet-900 dark:text-violet-400",
|
||||||
|
body: "text-slate-800 [--tw-prose-background:var(--color-slate-50)] prose-a:text-slate-900 prose-code:text-slate-900 dark:text-slate-300 dark:prose-code:text-slate-300",
|
||||||
|
},
|
||||||
|
warning: {
|
||||||
|
container:
|
||||||
|
"bg-amber-50 dark:bg-amber-800/60 dark:ring-1 dark:ring-amber-300/10",
|
||||||
|
title: "text-amber-900 dark:text-amber-500",
|
||||||
|
body: "text-slate-800 [--tw-prose-underline:var(--color-slate-400)] [--tw-prose-background:var(--color-slate-50)] prose-a:text-slate-900 prose-code:text-slate-900 dark:text-slate-300 dark:[--tw-prose-underline:var(--color-slate-700)] dark:prose-code:text-slate-300",
|
||||||
|
},
|
||||||
|
question: {
|
||||||
|
container:
|
||||||
|
"bg-amber-50 dark:bg-amber-800/60 dark:ring-1 dark:ring-amber-300/10",
|
||||||
|
title: "text-amber-900 dark:text-amber-500",
|
||||||
|
body: "text-slate-800 [--tw-prose-underline:var(--color-slate-400)] [--tw-prose-background:var(--color-slate-50)] prose-a:text-slate-900 prose-code:text-slate-900 dark:text-slate-300 dark:[--tw-prose-underline:var(--color-slate-700)] dark:prose-code:text-slate-300",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const icons = {
|
||||||
|
note: (props: { class?: string }) => <Icon icon="lightbulb" {...props} />,
|
||||||
|
warning: (props: { class?: string }) => (
|
||||||
|
<Icon icon="warning" color="amber" {...props} />
|
||||||
|
),
|
||||||
|
question: (props: { class?: string }) => (
|
||||||
|
<Icon icon="question" color="blue" {...props} />
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
export function Callout({
|
||||||
|
title,
|
||||||
|
children,
|
||||||
|
type = "note",
|
||||||
|
collapsible = false,
|
||||||
|
}: {
|
||||||
|
title: string;
|
||||||
|
children: JSX.Element;
|
||||||
|
type?: keyof typeof styles;
|
||||||
|
collapsible?: boolean;
|
||||||
|
}) {
|
||||||
|
const IconComponent = icons[type];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
class={clsx(
|
||||||
|
"my-8 flex flex-col rounded-3xl p-6",
|
||||||
|
styles[type].container,
|
||||||
|
{ "cursor-pointer": collapsible },
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div class="flex items-center gap-6">
|
||||||
|
<IconComponent class="h-8 w-8 flex-none" />
|
||||||
|
<p
|
||||||
|
class={clsx(
|
||||||
|
"!m-0 font-display text-xl text-balance",
|
||||||
|
styles[type].title,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="mt-4 flex-auto">
|
||||||
|
<div class={clsx("prose mt-2.5", styles[type].body)}>{children}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
51
app/components/Form.tsx
Normal file
51
app/components/Form.tsx
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
import clsx from "clsx";
|
||||||
|
|
||||||
|
type ToggleProps = {
|
||||||
|
id: string;
|
||||||
|
label: string;
|
||||||
|
onChange?: (checked: boolean) => void;
|
||||||
|
checked: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function Toggle(props: ToggleProps) {
|
||||||
|
return (
|
||||||
|
<div class="flex items-center justify-center">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
id={props.id}
|
||||||
|
class="sr-only"
|
||||||
|
onChange={(e) => props.onChange?.(e.target.checked)}
|
||||||
|
checked={props.checked}
|
||||||
|
aria-checked={props.checked}
|
||||||
|
role="switch"
|
||||||
|
aria-label={props.label}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<label
|
||||||
|
for={props.id}
|
||||||
|
class="flex cursor-pointer items-center justify-between rounded-full"
|
||||||
|
>
|
||||||
|
<span class="relative flex h-6 w-10 items-center">
|
||||||
|
<span
|
||||||
|
class={clsx(
|
||||||
|
"h-4 w-4 rounded-full bg-white shadow-md transition-transform duration-200 ease-in-out z-10",
|
||||||
|
props.checked
|
||||||
|
? "translate-x-[calc(100%+.25em)]"
|
||||||
|
: "translate-x-1",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
class={clsx(
|
||||||
|
"absolute top-1/2 left-1/2 h-full w-full -translate-x-1/2 -translate-y-1/2 rounded-full transition duration-200 ease-in-out z-0",
|
||||||
|
props.checked ? "bg-violet-500" : "bg-slate-300",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span class="ml-2 text-sm text-slate-700 dark:text-slate-300">
|
||||||
|
{props.label}
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
83
app/components/Icon.tsx
Normal file
83
app/components/Icon.tsx
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
import type { JSX } from "solid-js";
|
||||||
|
|
||||||
|
import { InstallationIcon } from "@/icons/InstallationIcon";
|
||||||
|
import { LightbulbIcon } from "@/icons/LightbulbIcon";
|
||||||
|
import { QuestionIcon } from "@/icons/QuestionIcon";
|
||||||
|
import { PluginsIcon } from "@/icons/PluginsIcon";
|
||||||
|
import { PresetsIcon } from "@/icons/PresetsIcon";
|
||||||
|
import { ThemingIcon } from "@/icons/ThemingIcon";
|
||||||
|
import { WarningIcon } from "@/icons/WarningIcon";
|
||||||
|
import { createUniqueId, For } from "solid-js";
|
||||||
|
import clsx from "clsx";
|
||||||
|
|
||||||
|
const icons = {
|
||||||
|
installation: InstallationIcon,
|
||||||
|
presets: PresetsIcon,
|
||||||
|
plugins: PluginsIcon,
|
||||||
|
theming: ThemingIcon,
|
||||||
|
lightbulb: LightbulbIcon,
|
||||||
|
warning: WarningIcon,
|
||||||
|
question: QuestionIcon,
|
||||||
|
};
|
||||||
|
|
||||||
|
const iconStyles = {
|
||||||
|
blue: "[--icon-foreground:var(--color-slate-900)] [--icon-background:var(--color-white)]",
|
||||||
|
amber:
|
||||||
|
"[--icon-foreground:var(--color-amber-900)] [--icon-background:var(--color-amber-100)]",
|
||||||
|
};
|
||||||
|
|
||||||
|
export type IconColor = keyof typeof iconStyles;
|
||||||
|
|
||||||
|
export type IconProps = JSX.IntrinsicElements["svg"] & {
|
||||||
|
color?: IconColor;
|
||||||
|
icon: keyof typeof icons;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function Icon(props: IconProps) {
|
||||||
|
const id = createUniqueId();
|
||||||
|
const IconComponent = icons[props.icon];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
viewBox="0 0 32 32"
|
||||||
|
fill="none"
|
||||||
|
class={clsx(props.class, iconStyles[props.color || "blue"])}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<IconComponent id={id} color={props.color || "blue"} />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const gradients = {
|
||||||
|
blue: [
|
||||||
|
{ stopColor: "#0EA5E9" },
|
||||||
|
{ stopColor: "#22D3EE", offset: ".527" },
|
||||||
|
{ stopColor: "#818CF8", offset: 1 },
|
||||||
|
],
|
||||||
|
amber: [
|
||||||
|
{ stopColor: "#FDE68A", offset: ".08" },
|
||||||
|
{ stopColor: "#F59E0B", offset: ".837" },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
type GradientProps = JSX.IntrinsicElements["radialGradient"] & {
|
||||||
|
color?: keyof typeof gradients;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function Gradient(props: GradientProps) {
|
||||||
|
return (
|
||||||
|
<radialGradient
|
||||||
|
cx={0}
|
||||||
|
cy={0}
|
||||||
|
r={1}
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<For each={gradients[props.color || "blue"]}>
|
||||||
|
{(stop) => <stop {...stop} />}
|
||||||
|
</For>
|
||||||
|
</radialGradient>
|
||||||
|
);
|
||||||
|
}
|
||||||
21
app/components/Iframe.tsx
Normal file
21
app/components/Iframe.tsx
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import clsx from "clsx";
|
||||||
|
|
||||||
|
type IframeProps = {
|
||||||
|
src: string;
|
||||||
|
title: string;
|
||||||
|
width?: string;
|
||||||
|
height?: string;
|
||||||
|
class?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function Iframe(props: IframeProps) {
|
||||||
|
return (
|
||||||
|
<iframe
|
||||||
|
src={props.src}
|
||||||
|
class={clsx("max-w-full pointer-events-none", props.class)}
|
||||||
|
width={props.width}
|
||||||
|
height={props.height}
|
||||||
|
title={props.title}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
18
app/components/Image.tsx
Normal file
18
app/components/Image.tsx
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import type { JSX } from "solid-js";
|
||||||
|
|
||||||
|
type ImageProps = JSX.IntrinsicElements["img"] & { src: string; alt: string };
|
||||||
|
|
||||||
|
export function Image(props: ImageProps) {
|
||||||
|
const isDecorationImage = props.alt === "";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<img
|
||||||
|
{...props}
|
||||||
|
src={props.src}
|
||||||
|
role={isDecorationImage ? "presentation" : "img"}
|
||||||
|
aria-hidden={isDecorationImage ? "true" : undefined}
|
||||||
|
alt={isDecorationImage ? undefined : props.alt}
|
||||||
|
loading="lazy"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -1,14 +1,37 @@
|
|||||||
import { createMemo } from "solid-js";
|
import type { JSX } from "solid-js";
|
||||||
|
|
||||||
import { usePageContext } from "vike-solid/usePageContext";
|
import { usePageContext } from "vike-solid/usePageContext";
|
||||||
|
|
||||||
export function Link(props: { href: string; children: string }) {
|
type LinkProps = JSX.IntrinsicElements["a"] & { href: string };
|
||||||
const pageContext = usePageContext();
|
|
||||||
const isActive = createMemo(() =>
|
export function Link(props: LinkProps) {
|
||||||
props.href === "/" ? pageContext.urlPathname === props.href : pageContext.urlPathname.startsWith(props.href),
|
const { urlPathname } = usePageContext();
|
||||||
);
|
|
||||||
return (
|
const isActive =
|
||||||
<a href={props.href} class={isActive() ? "is-active" : undefined}>
|
props.href === "/"
|
||||||
{props.children}
|
? urlPathname === props.href
|
||||||
</a>
|
: urlPathname.startsWith(props.href);
|
||||||
);
|
|
||||||
|
const isSameDomain = !(
|
||||||
|
props.href.startsWith("http") || props.href.startsWith("mailto")
|
||||||
|
);
|
||||||
|
|
||||||
|
const downloadExtensions = [".pdf", ".zip"];
|
||||||
|
|
||||||
|
const isDownload = downloadExtensions.some((extension) =>
|
||||||
|
props.href.endsWith(extension),
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<a
|
||||||
|
{...props}
|
||||||
|
{...(isActive && { ariaCurrent: "page" })}
|
||||||
|
{...(isDownload && { download: true })}
|
||||||
|
{...(!isSameDomain || isDownload
|
||||||
|
? { target: "_blank", rel: "noopener noreferrer" }
|
||||||
|
: { target: "_self" })}
|
||||||
|
>
|
||||||
|
{props.children}
|
||||||
|
</a>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
42
app/components/Logo.tsx
Normal file
42
app/components/Logo.tsx
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import type { JSX } from "solid-js";
|
||||||
|
|
||||||
|
import { createUniqueId } from "solid-js";
|
||||||
|
|
||||||
|
function LogomarkPaths() {
|
||||||
|
const id = createUniqueId();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<defs>
|
||||||
|
<linearGradient
|
||||||
|
id={id}
|
||||||
|
x1="0"
|
||||||
|
y1="0"
|
||||||
|
x2="1"
|
||||||
|
y2="0"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
gradientTransform="matrix(12.792,-21.32,-21.32,-12.792,5.208,23.32)"
|
||||||
|
>
|
||||||
|
<stop offset="0" stop-color="rgb(43,127,255)" />
|
||||||
|
<stop offset="1" stop-color="rgb(142,81,255)" />
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
|
||||||
|
<g transform="matrix(-1.76727,0,0,1.76727,49.1089,-3.53454)">
|
||||||
|
<path
|
||||||
|
d="M16.161,18.989L20.49,23.32L21.9,21.91L2.1,2.1L0.69,3.51L2.714,5.535L-4.085,11.253L-4.085,13.054L3.185,19.167L4.629,17.337L-1.61,12.165L4.397,7.219L9.588,12.412L6,16L6.01,16.01L6,16.01L6,22L18,22L18,20.83L16.161,18.989ZM14.417,17.244L16,18.83L16,20L8,20L8,16.5L10.837,13.663L14.417,17.244ZM8,4L16,4L16,7.5L13.16,10.34L14.41,11.59L18,8.01L17.99,8L18,8L18,2L6,2L6,3.17L8,5.17L8,4ZM25.294,12.164L19.071,17.34L20.542,19.164L27.788,13.075L27.788,11.274L20.597,5.22L19.158,7.075L25.294,12.164Z"
|
||||||
|
style={{ fill: `url(#${id})` }}
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Logo(props: JSX.IntrinsicElements["svg"]) {
|
||||||
|
return (
|
||||||
|
<svg view-box="0 0 58 38" {...props}>
|
||||||
|
<title>Memento Dev</title>
|
||||||
|
<LogomarkPaths />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
97
app/components/PrevNextLinks.tsx
Normal file
97
app/components/PrevNextLinks.tsx
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
import type { JSX } from "solid-js";
|
||||||
|
|
||||||
|
import { usePageContext } from "vike-solid/usePageContext";
|
||||||
|
import { cleanProps } from "@/utils/cleanProps";
|
||||||
|
import { navigation } from "@/libs/navigation";
|
||||||
|
import { Link } from "@/components/Link";
|
||||||
|
import clsx from "clsx";
|
||||||
|
|
||||||
|
function ArrowIcon(props: JSX.IntrinsicElements["svg"]) {
|
||||||
|
return (
|
||||||
|
<svg viewBox="0 0 16 16" aria-hidden="true" {...props}>
|
||||||
|
<path d="m9.182 13.423-1.17-1.16 3.505-3.505H3V7.065h8.517l-3.506-3.5L9.181 2.4l5.512 5.511-5.511 5.512Z" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
type PageLinkProps = Omit<JSX.IntrinsicElements["div"], "dir" | "title"> & {
|
||||||
|
title: string;
|
||||||
|
href: string;
|
||||||
|
dir?: "previous" | "next";
|
||||||
|
};
|
||||||
|
|
||||||
|
function PageLink(props: PageLinkProps) {
|
||||||
|
const pageCategory = navigation.find((section) => {
|
||||||
|
return section.links.some(
|
||||||
|
(link) =>
|
||||||
|
link.href === props.href ||
|
||||||
|
link.subitems.some((subitem) => subitem.href === props.href),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div {...cleanProps(props, "dir", "title")}>
|
||||||
|
<dt class="font-display text-sm font-medium text-slate-900 dark:text-white">
|
||||||
|
{props.dir === "next" ? "Suivant" : "Précédent"}
|
||||||
|
</dt>
|
||||||
|
<dd class="mt-1">
|
||||||
|
<Link
|
||||||
|
href={props.href}
|
||||||
|
class={clsx(
|
||||||
|
"flex items-center gap-x-2 text-base font-semibold text-slate-500 hover:text-slate-600 dark:text-slate-400 dark:hover:text-slate-300",
|
||||||
|
props.dir === "previous" && "flex-row-reverse",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<p class="flex flex-col gap-0">
|
||||||
|
{pageCategory && (
|
||||||
|
<span class="text-violet-600 dark:text-violet-400 text-sm mb-1 leading-3">
|
||||||
|
{pageCategory.title}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<span class="leading-4">{props.title}</span>
|
||||||
|
</p>
|
||||||
|
<ArrowIcon
|
||||||
|
class={clsx(
|
||||||
|
"h-6 w-6 flex-none fill-current",
|
||||||
|
props.dir === "previous" && "-scale-x-100",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</Link>
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PrevNextLinks() {
|
||||||
|
const { urlPathname } = usePageContext();
|
||||||
|
|
||||||
|
const allLinks = navigation
|
||||||
|
.flatMap((section) => section.links)
|
||||||
|
.flatMap((link) => {
|
||||||
|
return link.subitems ? [link, ...link.subitems] : link;
|
||||||
|
});
|
||||||
|
|
||||||
|
const getNeighboringLinks = () => {
|
||||||
|
const linkIndex = allLinks.findIndex((link) => link.href === urlPathname);
|
||||||
|
if (linkIndex === -1) return [null, null];
|
||||||
|
|
||||||
|
const previousPage = allLinks[linkIndex - 1] || null;
|
||||||
|
let nextPage = allLinks[linkIndex + 1] || null;
|
||||||
|
|
||||||
|
if (nextPage?.href === urlPathname) {
|
||||||
|
nextPage = allLinks[linkIndex + 2] || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [previousPage, nextPage];
|
||||||
|
};
|
||||||
|
|
||||||
|
const [previousPage, nextPage] = getNeighboringLinks();
|
||||||
|
if (!nextPage && !previousPage) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<dl class="mt-12 flex gap-4 border-t border-slate-200 pt-6 dark:border-slate-800">
|
||||||
|
{previousPage && <PageLink dir="previous" {...previousPage} />}
|
||||||
|
{nextPage && <PageLink class="ml-auto text-right" {...nextPage} />}
|
||||||
|
</dl>
|
||||||
|
);
|
||||||
|
}
|
||||||
36
app/components/Prose.tsx
Normal file
36
app/components/Prose.tsx
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import type { JSX } from "solid-js";
|
||||||
|
|
||||||
|
import { Dynamic } from "solid-js/web";
|
||||||
|
import clsx from "clsx";
|
||||||
|
|
||||||
|
type ProseProps = JSX.IntrinsicElements["div"] & {
|
||||||
|
class?: string;
|
||||||
|
as?: keyof JSX.IntrinsicElements;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function Prose(props: ProseProps) {
|
||||||
|
const Component = props.as ?? "div";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dynamic
|
||||||
|
component={Component}
|
||||||
|
class={clsx(
|
||||||
|
props.class,
|
||||||
|
"prose max-w-none prose-slate dark:text-slate-400 dark:prose-invert",
|
||||||
|
// headings
|
||||||
|
"prose-headings:scroll-mt-28 prose-headings:font-display prose-headings:font-normal lg:prose-headings:scroll-mt-[8.5rem]",
|
||||||
|
// lead
|
||||||
|
"prose-lead:text-slate-500 dark:prose-lead:text-slate-400",
|
||||||
|
// links
|
||||||
|
"prose-a:font-semibold dark:prose-a:text-violet-400",
|
||||||
|
// link underline
|
||||||
|
"dark:[--tw-prose-background:var(--color-slate-900)] prose-a:no-underline prose-a:shadow-[inset_0_-2px_0_0_var(--tw-prose-background,#fff),inset_0_calc(-1*(var(--tw-prose-underline-size,4px)+2px))_0_0_var(--tw-prose-underline,var(--color-violet-300))] prose-a:hover:[--tw-prose-underline-size:6px] dark:prose-a:shadow-[inset_0_calc(-1*var(--tw-prose-underline-size,2px))_0_0_var(--tw-prose-underline,var(--color-violet-800))] dark:prose-a:hover:[--tw-prose-underline-size:6px]",
|
||||||
|
// pre
|
||||||
|
"prose-pre:rounded-xl prose-pre:bg-slate-900 prose-pre:shadow-lg dark:prose-pre:bg-slate-800/60 dark:prose-pre:ring-1 dark:prose-pre:shadow-none dark:prose-pre:ring-slate-300/10",
|
||||||
|
// hr
|
||||||
|
"dark:prose-hr:border-slate-800",
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
46
app/components/QuickLinks.tsx
Normal file
46
app/components/QuickLinks.tsx
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
import type { JSXElement } from "solid-js";
|
||||||
|
import type { IconProps } from "./Icon";
|
||||||
|
|
||||||
|
import { Icon } from "./Icon";
|
||||||
|
import { Link } from "./Link";
|
||||||
|
|
||||||
|
type QuickLinksProps = {
|
||||||
|
children: JSXElement;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function QuickLinks(props: QuickLinksProps) {
|
||||||
|
return (
|
||||||
|
<div class="not-prose my-12 grid grid-cols-1 gap-6 sm:grid-cols-2">
|
||||||
|
{props.children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
type QuickLinkProps = {
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
href: string;
|
||||||
|
icon: IconProps["icon"];
|
||||||
|
};
|
||||||
|
|
||||||
|
export function QuickLink(props: QuickLinkProps) {
|
||||||
|
return (
|
||||||
|
<div class="group relative rounded-xl border border-slate-200 dark:border-slate-800">
|
||||||
|
<div class="absolute -inset-px rounded-xl border-2 border-transparent opacity-0 [background:linear-gradient(var(--quick-links-hover-bg,var(--color-violet-50)),var(--quick-links-hover-bg,var(--color-violet-50)))_padding-box,linear-gradient(to_top,var(--color-indigo-400),var(--color-cyan-400),var(--color-violet-500))_border-box] group-hover:opacity-100 dark:[--quick-links-hover-bg:var(--color-slate-800)]" />
|
||||||
|
<div class="relative overflow-hidden rounded-xl p-6">
|
||||||
|
<Icon icon={props.icon} class="h-8 w-8" />
|
||||||
|
|
||||||
|
<h2 class="mt-4 font-display text-base text-slate-900 dark:text-white">
|
||||||
|
<Link href={props.href}>
|
||||||
|
<span class="absolute -inset-px rounded-xl" />
|
||||||
|
{props.title}
|
||||||
|
</Link>
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<p class="mt-1 text-sm text-slate-700 dark:text-slate-400">
|
||||||
|
{props.description}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
36
app/icons/InstallationIcon.tsx
Normal file
36
app/icons/InstallationIcon.tsx
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import type { IconColor } from "@/components/Icon";
|
||||||
|
|
||||||
|
import { Gradient } from "@/components/Icon";
|
||||||
|
|
||||||
|
type GradientProps = {
|
||||||
|
id: string;
|
||||||
|
color?: IconColor;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function InstallationIcon(props: GradientProps) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<defs>
|
||||||
|
<Gradient
|
||||||
|
id={`${props.id}-gradient`}
|
||||||
|
color={props.color}
|
||||||
|
gradientTransform="matrix(0 21 -21 0 12 3)"
|
||||||
|
/>
|
||||||
|
<Gradient
|
||||||
|
id={`${props.id}-gradient-dark`}
|
||||||
|
color={props.color}
|
||||||
|
gradientTransform="matrix(0 21 -21 0 16 7)"
|
||||||
|
/>
|
||||||
|
</defs>
|
||||||
|
<circle cx={12} cy={12} r={12} fill={`url(#${props.id}-gradient)`} />
|
||||||
|
<path
|
||||||
|
d="m8 8 9 21 2-10 10-2L8 8Z"
|
||||||
|
fill-opacity={0.5}
|
||||||
|
class="fill-[var(--icon-background)] stroke-[color:var(--icon-foreground)]"
|
||||||
|
stroke-width={2}
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
39
app/icons/LightbulbIcon.tsx
Normal file
39
app/icons/LightbulbIcon.tsx
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import { DarkMode, Gradient, LightMode } from "@syntax/Icon";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
export function LightbulbIcon({ id, color }: { id: string; color?: React.ComponentProps<typeof Gradient>["color"] }) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<defs>
|
||||||
|
<Gradient id={`${id}-gradient`} color={color} gradientTransform="matrix(0 21 -21 0 20 11)" />
|
||||||
|
<Gradient id={`${id}-gradient-dark`} color={color} gradientTransform="matrix(0 24.5001 -19.2498 0 16 5.5)" />
|
||||||
|
</defs>
|
||||||
|
<LightMode>
|
||||||
|
<circle cx={20} cy={20} r={12} fill={`url(#${id}-gradient)`} />
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
clipRule="evenodd"
|
||||||
|
d="M20 24.995c0-1.855 1.094-3.501 2.427-4.792C24.61 18.087 26 15.07 26 12.231 26 7.133 21.523 3 16 3S6 7.133 6 12.23c0 2.84 1.389 5.857 3.573 7.973C10.906 21.494 12 23.14 12 24.995V27a2 2 0 0 0 2 2h4a2 2 0 0 0 2-2v-2.005Z"
|
||||||
|
className="fill-[var(--icon-background)]"
|
||||||
|
fillOpacity={0.5}
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M25 12.23c0 2.536-1.254 5.303-3.269 7.255l1.391 1.436c2.354-2.28 3.878-5.547 3.878-8.69h-2ZM16 4c5.047 0 9 3.759 9 8.23h2C27 6.508 21.998 2 16 2v2Zm-9 8.23C7 7.76 10.953 4 16 4V2C10.002 2 5 6.507 5 12.23h2Zm3.269 7.255C8.254 17.533 7 14.766 7 12.23H5c0 3.143 1.523 6.41 3.877 8.69l1.392-1.436ZM13 27v-2.005h-2V27h2Zm1 1a1 1 0 0 1-1-1h-2a3 3 0 0 0 3 3v-2Zm4 0h-4v2h4v-2Zm1-1a1 1 0 0 1-1 1v2a3 3 0 0 0 3-3h-2Zm0-2.005V27h2v-2.005h-2ZM8.877 20.921C10.132 22.136 11 23.538 11 24.995h2c0-2.253-1.32-4.143-2.731-5.51L8.877 20.92Zm12.854-1.436C20.32 20.852 19 22.742 19 24.995h2c0-1.457.869-2.859 2.122-4.074l-1.391-1.436Z"
|
||||||
|
className="fill-[var(--icon-foreground)]"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M20 26a1 1 0 1 0 0-2v2Zm-8-2a1 1 0 1 0 0 2v-2Zm2 0h-2v2h2v-2Zm1 1V13.5h-2V25h2Zm-5-11.5v1h2v-1h-2Zm3.5 4.5h5v-2h-5v2Zm8.5-3.5v-1h-2v1h2ZM20 24h-2v2h2v-2Zm-2 0h-4v2h4v-2Zm-1-10.5V25h2V13.5h-2Zm2.5-2.5a2.5 2.5 0 0 0-2.5 2.5h2a.5.5 0 0 1 .5-.5v-2Zm2.5 2.5a2.5 2.5 0 0 0-2.5-2.5v2a.5.5 0 0 1 .5.5h2ZM18.5 18a3.5 3.5 0 0 0 3.5-3.5h-2a1.5 1.5 0 0 1-1.5 1.5v2ZM10 14.5a3.5 3.5 0 0 0 3.5 3.5v-2a1.5 1.5 0 0 1-1.5-1.5h-2Zm2.5-3.5a2.5 2.5 0 0 0-2.5 2.5h2a.5.5 0 0 1 .5-.5v-2Zm2.5 2.5a2.5 2.5 0 0 0-2.5-2.5v2a.5.5 0 0 1 .5.5h2Z"
|
||||||
|
className="fill-[var(--icon-foreground)]"
|
||||||
|
/>
|
||||||
|
</LightMode>
|
||||||
|
<DarkMode>
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
clipRule="evenodd"
|
||||||
|
d="M16 2C10.002 2 5 6.507 5 12.23c0 3.144 1.523 6.411 3.877 8.691.75.727 1.363 1.52 1.734 2.353.185.415.574.726 1.028.726H12a1 1 0 0 0 1-1v-4.5a.5.5 0 0 0-.5-.5A3.5 3.5 0 0 1 9 14.5V14a3 3 0 1 1 6 0v9a1 1 0 1 0 2 0v-9a3 3 0 1 1 6 0v.5a3.5 3.5 0 0 1-3.5 3.5.5.5 0 0 0-.5.5V23a1 1 0 0 0 1 1h.36c.455 0 .844-.311 1.03-.726.37-.833.982-1.626 1.732-2.353 2.354-2.28 3.878-5.547 3.878-8.69C27 6.507 21.998 2 16 2Zm5 25a1 1 0 0 0-1-1h-8a1 1 0 0 0-1 1 3 3 0 0 0 3 3h4a3 3 0 0 0 3-3Zm-8-13v1.5a.5.5 0 0 1-.5.5 1.5 1.5 0 0 1-1.5-1.5V14a1 1 0 1 1 2 0Zm6.5 2a.5.5 0 0 1-.5-.5V14a1 1 0 1 1 2 0v.5a1.5 1.5 0 0 1-1.5 1.5Z"
|
||||||
|
fill={`url(#${id}-gradient-dark)`}
|
||||||
|
/>
|
||||||
|
</DarkMode>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
48
app/icons/PluginsIcon.tsx
Normal file
48
app/icons/PluginsIcon.tsx
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
import { DarkMode, Gradient, LightMode } from "@syntax/Icon";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
export function PluginsIcon({ id, color }: { id: string; color?: React.ComponentProps<typeof Gradient>["color"] }) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<defs>
|
||||||
|
<Gradient id={`${id}-gradient`} color={color} gradientTransform="matrix(0 21 -21 0 20 11)" />
|
||||||
|
<Gradient id={`${id}-gradient-dark-1`} color={color} gradientTransform="matrix(0 22.75 -22.75 0 16 6.25)" />
|
||||||
|
<Gradient id={`${id}-gradient-dark-2`} color={color} gradientTransform="matrix(0 14 -14 0 16 10)" />
|
||||||
|
</defs>
|
||||||
|
<LightMode>
|
||||||
|
<circle cx={20} cy={20} r={12} fill={`url(#${id}-gradient)`} />
|
||||||
|
<g
|
||||||
|
fillOpacity={0.5}
|
||||||
|
className="fill-[var(--icon-background)] stroke-[color:var(--icon-foreground)]"
|
||||||
|
strokeWidth={2}
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
>
|
||||||
|
<path d="M3 9v14l12 6V15L3 9Z" />
|
||||||
|
<path d="M27 9v14l-12 6V15l12-6Z" />
|
||||||
|
</g>
|
||||||
|
<path d="M11 4h8v2l6 3-10 6L5 9l6-3V4Z" fillOpacity={0.5} className="fill-[var(--icon-background)]" />
|
||||||
|
<g
|
||||||
|
className="stroke-[color:var(--icon-foreground)]"
|
||||||
|
strokeWidth={2}
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
>
|
||||||
|
<path d="M20 5.5 27 9l-12 6L3 9l7-3.5" />
|
||||||
|
<path d="M20 5c0 1.105-2.239 2-5 2s-5-.895-5-2m10 0c0-1.105-2.239-2-5-2s-5 .895-5 2m10 0v3c0 1.105-2.239 2-5 2s-5-.895-5-2V5" />
|
||||||
|
</g>
|
||||||
|
</LightMode>
|
||||||
|
<DarkMode strokeWidth={2} strokeLinecap="round" strokeLinejoin="round">
|
||||||
|
<path
|
||||||
|
d="M17.676 3.38a3.887 3.887 0 0 0-3.352 0l-9 4.288C3.907 8.342 3 9.806 3 11.416v9.168c0 1.61.907 3.073 2.324 3.748l9 4.288a3.887 3.887 0 0 0 3.352 0l9-4.288C28.093 23.657 29 22.194 29 20.584v-9.168c0-1.61-.907-3.074-2.324-3.748l-9-4.288Z"
|
||||||
|
stroke={`url(#${id}-gradient-dark-1)`}
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M16.406 8.087a.989.989 0 0 0-.812 0l-7 3.598A1.012 1.012 0 0 0 8 12.61v6.78c0 .4.233.762.594.925l7 3.598a.989.989 0 0 0 .812 0l7-3.598c.361-.163.594-.525.594-.925v-6.78c0-.4-.233-.762-.594-.925l-7-3.598Z"
|
||||||
|
fill={`url(#${id}-gradient-dark-2)`}
|
||||||
|
stroke={`url(#${id}-gradient-dark-2)`}
|
||||||
|
/>
|
||||||
|
</DarkMode>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
36
app/icons/PresetsIcon.tsx
Normal file
36
app/icons/PresetsIcon.tsx
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import { DarkMode, Gradient, LightMode } from "@syntax/Icon";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
export function PresetsIcon({ id, color }: { id: string; color?: React.ComponentProps<typeof Gradient>["color"] }) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<defs>
|
||||||
|
<Gradient id={`${id}-gradient`} color={color} gradientTransform="matrix(0 21 -21 0 20 3)" />
|
||||||
|
<Gradient id={`${id}-gradient-dark`} color={color} gradientTransform="matrix(0 22.75 -22.75 0 16 6.25)" />
|
||||||
|
</defs>
|
||||||
|
<LightMode>
|
||||||
|
<circle cx={20} cy={12} r={12} fill={`url(#${id}-gradient)`} />
|
||||||
|
<g
|
||||||
|
className="fill-[var(--icon-background)] stroke-[color:var(--icon-foreground)]"
|
||||||
|
fillOpacity={0.5}
|
||||||
|
strokeWidth={2}
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
>
|
||||||
|
<path d="M3 5v12a2 2 0 0 0 2 2h7a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2Z" />
|
||||||
|
<path d="M18 17v10a2 2 0 0 0 2 2h7a2 2 0 0 0 2-2V17a2 2 0 0 0-2-2h-7a2 2 0 0 0-2 2Z" />
|
||||||
|
<path d="M18 5v4a2 2 0 0 0 2 2h7a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2h-7a2 2 0 0 0-2 2Z" />
|
||||||
|
<path d="M3 25v2a2 2 0 0 0 2 2h7a2 2 0 0 0 2-2v-2a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2Z" />
|
||||||
|
</g>
|
||||||
|
</LightMode>
|
||||||
|
<DarkMode fill={`url(#${id}-gradient-dark)`}>
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
clipRule="evenodd"
|
||||||
|
d="M3 17V4a1 1 0 0 1 1-1h8a1 1 0 0 1 1 1v13a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1Zm16 10v-9a2 2 0 0 1 2-2h6a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2h-6a2 2 0 0 1-2-2Zm0-23v5a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V4a1 1 0 0 0-1-1h-8a1 1 0 0 0-1 1ZM3 28v-3a1 1 0 0 1 1-1h9a1 1 0 0 1 1 1v3a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1Z"
|
||||||
|
/>
|
||||||
|
<path d="M2 4v13h2V4H2Zm2-2a2 2 0 0 0-2 2h2V2Zm8 0H4v2h8V2Zm2 2a2 2 0 0 0-2-2v2h2Zm0 13V4h-2v13h2Zm-2 2a2 2 0 0 0 2-2h-2v2Zm-8 0h8v-2H4v2Zm-2-2a2 2 0 0 0 2 2v-2H2Zm16 1v9h2v-9h-2Zm3-3a3 3 0 0 0-3 3h2a1 1 0 0 1 1-1v-2Zm6 0h-6v2h6v-2Zm3 3a3 3 0 0 0-3-3v2a1 1 0 0 1 1 1h2Zm0 9v-9h-2v9h2Zm-3 3a3 3 0 0 0 3-3h-2a1 1 0 0 1-1 1v2Zm-6 0h6v-2h-6v2Zm-3-3a3 3 0 0 0 3 3v-2a1 1 0 0 1-1-1h-2Zm2-18V4h-2v5h2Zm0 0h-2a2 2 0 0 0 2 2V9Zm8 0h-8v2h8V9Zm0 0v2a2 2 0 0 0 2-2h-2Zm0-5v5h2V4h-2Zm0 0h2a2 2 0 0 0-2-2v2Zm-8 0h8V2h-8v2Zm0 0V2a2 2 0 0 0-2 2h2ZM2 25v3h2v-3H2Zm2-2a2 2 0 0 0-2 2h2v-2Zm9 0H4v2h9v-2Zm2 2a2 2 0 0 0-2-2v2h2Zm0 3v-3h-2v3h2Zm-2 2a2 2 0 0 0 2-2h-2v2Zm-9 0h9v-2H4v2Zm-2-2a2 2 0 0 0 2 2v-2H2Z" />
|
||||||
|
</DarkMode>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
48
app/icons/QuestionIcon.tsx
Normal file
48
app/icons/QuestionIcon.tsx
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
import { DarkMode, Gradient, LightMode } from "@syntax/Icon";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
export function QuestionIcon({ id, color }: { id: string; color?: React.ComponentProps<typeof Gradient>["color"] }) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<defs>
|
||||||
|
<Gradient id={`${id}-gradient`} color={color} gradientTransform="rotate(65.924 1.519 20.92) scale(25.7391)" />
|
||||||
|
<Gradient id={`${id}-gradient-dark`} color={color} gradientTransform="matrix(0 24.5 -24.5 0 16 5.5)" />
|
||||||
|
</defs>
|
||||||
|
<LightMode>
|
||||||
|
<circle cx={20} cy={20} r={12} fill={`url(#${id}-gradient)`} />
|
||||||
|
<path
|
||||||
|
d="M3 16c0 7.18 5.82 13 13 13s13-5.82 13-13S23.18 3 16 3 3 8.82 3 16Z"
|
||||||
|
fillOpacity={0.5}
|
||||||
|
className="fill-[var(--icon-background)] stroke-[color:var(--icon-foreground)]"
|
||||||
|
strokeWidth={2}
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="m 16.39 14.617 l 1.179 -3.999 C 17.38 9.304 16.133 9.127 15.469 10.645 C 15.306 11.269 14.71 11.12 14.71 10.537 a 1.66 1.66 5 1 1 3.808 0.217 l -1.5182 5.4314 a 0.602 0.602 5 0 1 -1.1795 -0.1032 Z"
|
||||||
|
className="fill-[var(--icon-foreground)] stroke-[color:var(--icon-foreground)]"
|
||||||
|
strokeWidth={2}
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M16 23a1 1 0 1 0 0-2 1 1 0 0 0 0 2Z"
|
||||||
|
fillOpacity={0.5}
|
||||||
|
stroke="currentColor"
|
||||||
|
className="fill-[var(--icon-background)] stroke-[color:var(--icon-foreground)]"
|
||||||
|
strokeWidth={2}
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
/>
|
||||||
|
</LightMode>
|
||||||
|
<DarkMode>
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
clipRule="evenodd"
|
||||||
|
d="M2 16C2 8.268 8.268 2 16 2s14 6.268 14 14-6.268 14-14 14S2 23.732 2 16Zm11.386-4.85a2.66 2.66 0 1 1 5.228 0l-1.039 5.543a1.602 1.602 0 0 1-3.15 0l-1.04-5.543ZM16 20a2 2 0 1 0 0 4 2 2 0 0 0 0-4Z"
|
||||||
|
fill={`url(#${id}-gradient-dark)`}
|
||||||
|
/>
|
||||||
|
</DarkMode>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
52
app/icons/ThemingIcon.tsx
Normal file
52
app/icons/ThemingIcon.tsx
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
import { DarkMode, Gradient, LightMode } from "@syntax/Icon";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
export function ThemingIcon({ id, color }: { id: string; color?: React.ComponentProps<typeof Gradient>["color"] }) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<defs>
|
||||||
|
<Gradient id={`${id}-gradient`} color={color} gradientTransform="matrix(0 21 -21 0 12 11)" />
|
||||||
|
<Gradient id={`${id}-gradient-dark`} color={color} gradientTransform="matrix(0 24.5 -24.5 0 16 5.5)" />
|
||||||
|
</defs>
|
||||||
|
<LightMode>
|
||||||
|
<circle cx={12} cy={20} r={12} fill={`url(#${id}-gradient)`} />
|
||||||
|
<path
|
||||||
|
d="M27 12.13 19.87 5 13 11.87v14.26l14-14Z"
|
||||||
|
className="fill-[var(--icon-background)] stroke-[color:var(--icon-foreground)]"
|
||||||
|
fillOpacity={0.5}
|
||||||
|
strokeWidth={2}
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M3 3h10v22a4 4 0 0 1-4 4H7a4 4 0 0 1-4-4V3Z"
|
||||||
|
className="fill-[var(--icon-background)]"
|
||||||
|
fillOpacity={0.5}
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M3 9v16a4 4 0 0 0 4 4h2a4 4 0 0 0 4-4V9M3 9V3h10v6M3 9h10M3 15h10M3 21h10"
|
||||||
|
className="stroke-[color:var(--icon-foreground)]"
|
||||||
|
strokeWidth={2}
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M29 29V19h-8.5L13 26c0 1.5-2.5 3-5 3h21Z"
|
||||||
|
fillOpacity={0.5}
|
||||||
|
className="fill-[var(--icon-background)] stroke-[color:var(--icon-foreground)]"
|
||||||
|
strokeWidth={2}
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
/>
|
||||||
|
</LightMode>
|
||||||
|
<DarkMode>
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
clipRule="evenodd"
|
||||||
|
d="M3 2a1 1 0 0 0-1 1v21a6 6 0 0 0 12 0V3a1 1 0 0 0-1-1H3Zm16.752 3.293a1 1 0 0 0-1.593.244l-1.045 2A1 1 0 0 0 17 8v13a1 1 0 0 0 1.71.705l7.999-8.045a1 1 0 0 0-.002-1.412l-6.955-6.955ZM26 18a1 1 0 0 0-.707.293l-10 10A1 1 0 0 0 16 30h13a1 1 0 0 0 1-1V19a1 1 0 0 0-1-1h-3ZM5 18a1 1 0 1 0 0 2h6a1 1 0 1 0 0-2H5Zm-1-5a1 1 0 0 1 1-1h6a1 1 0 1 1 0 2H5a1 1 0 0 1-1-1Zm1-7a1 1 0 0 0 0 2h6a1 1 0 1 0 0-2H5Z"
|
||||||
|
fill={`url(#${id}-gradient-dark)`}
|
||||||
|
/>
|
||||||
|
</DarkMode>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
48
app/icons/WarningIcon.tsx
Normal file
48
app/icons/WarningIcon.tsx
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
import { DarkMode, Gradient, LightMode } from "@syntax/Icon";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
export function WarningIcon({ id, color }: { id: string; color?: React.ComponentProps<typeof Gradient>["color"] }) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<defs>
|
||||||
|
<Gradient id={`${id}-gradient`} color={color} gradientTransform="rotate(65.924 1.519 20.92) scale(25.7391)" />
|
||||||
|
<Gradient id={`${id}-gradient-dark`} color={color} gradientTransform="matrix(0 24.5 -24.5 0 16 5.5)" />
|
||||||
|
</defs>
|
||||||
|
<LightMode>
|
||||||
|
<circle cx={20} cy={20} r={12} fill={`url(#${id}-gradient)`} />
|
||||||
|
<path
|
||||||
|
d="M3 16c0 7.18 5.82 13 13 13s13-5.82 13-13S23.18 3 16 3 3 8.82 3 16Z"
|
||||||
|
fillOpacity={0.5}
|
||||||
|
className="fill-[var(--icon-background)] stroke-[color:var(--icon-foreground)]"
|
||||||
|
strokeWidth={2}
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="m15.408 16.509-1.04-5.543a1.66 1.66 0 1 1 3.263 0l-1.039 5.543a.602.602 0 0 1-1.184 0Z"
|
||||||
|
className="fill-[var(--icon-foreground)] stroke-[color:var(--icon-foreground)]"
|
||||||
|
strokeWidth={2}
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M16 23a1 1 0 1 0 0-2 1 1 0 0 0 0 2Z"
|
||||||
|
fillOpacity={0.5}
|
||||||
|
stroke="currentColor"
|
||||||
|
className="fill-[var(--icon-background)] stroke-[color:var(--icon-foreground)]"
|
||||||
|
strokeWidth={2}
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
/>
|
||||||
|
</LightMode>
|
||||||
|
<DarkMode>
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
clipRule="evenodd"
|
||||||
|
d="M2 16C2 8.268 8.268 2 16 2s14 6.268 14 14-6.268 14-14 14S2 23.732 2 16Zm11.386-4.85a2.66 2.66 0 1 1 5.228 0l-1.039 5.543a1.602 1.602 0 0 1-3.15 0l-1.04-5.543ZM16 20a2 2 0 1 0 0 4 2 2 0 0 0 0-4Z"
|
||||||
|
fill={`url(#${id}-gradient-dark)`}
|
||||||
|
/>
|
||||||
|
</DarkMode>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
38
app/layouts/DocsLayout.tsx
Normal file
38
app/layouts/DocsLayout.tsx
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
import { type Node } from "@markdoc/markdoc";
|
||||||
|
|
||||||
|
import { TableOfContents } from "@syntax/TableOfContents";
|
||||||
|
import { PrevNextLinks } from "@/components/PrevNextLinks";
|
||||||
|
import { collectSections } from "@/lib/sections";
|
||||||
|
import { DocsHeader } from "@syntax/DocsHeader";
|
||||||
|
import { Prose } from "@/components/Prose";
|
||||||
|
|
||||||
|
export function DocsLayout({
|
||||||
|
children,
|
||||||
|
frontmatter: { title },
|
||||||
|
estimatedReadingTime,
|
||||||
|
nodes,
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode;
|
||||||
|
frontmatter: { title?: string };
|
||||||
|
estimatedReadingTime?: string;
|
||||||
|
nodes: Array<Node>;
|
||||||
|
}) {
|
||||||
|
const tableOfContents = collectSections(nodes);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div class="max-w-2xl min-w-0 flex-auto px-4 py-16 lg:max-w-none lg:pr-0 lg:pl-8 xl:px-16 grow">
|
||||||
|
<article>
|
||||||
|
<DocsHeader
|
||||||
|
title={title}
|
||||||
|
estimatedReadingTime={estimatedReadingTime}
|
||||||
|
/>
|
||||||
|
<Prose>{children}</Prose>
|
||||||
|
</article>
|
||||||
|
<PrevNextLinks />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<TableOfContents tableOfContents={tableOfContents} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
172
app/libs/navigation.ts
Normal file
172
app/libs/navigation.ts
Normal file
@ -0,0 +1,172 @@
|
|||||||
|
const navigationsTypes = {
|
||||||
|
GLOBAL: "👋 Général",
|
||||||
|
CERTIFICATIONS: "🎓 Certifications",
|
||||||
|
DOCUMENTATIONS: "📚 Documentations",
|
||||||
|
OTHER: "🔗 Autres",
|
||||||
|
};
|
||||||
|
|
||||||
|
export type NavigationSection = {
|
||||||
|
title: string;
|
||||||
|
type: (typeof navigationsTypes)[keyof typeof navigationsTypes];
|
||||||
|
position: "start" | "end" | "auto";
|
||||||
|
links: NavigationLink[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type NavigationOG = Partial<{
|
||||||
|
image: string;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
export type NavigationLink = {
|
||||||
|
title: string;
|
||||||
|
href: string;
|
||||||
|
og?: NavigationOG;
|
||||||
|
subitems: NavigationSubItem[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type NavigationSubItem = {
|
||||||
|
title: string;
|
||||||
|
href: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const navigation: NavigationSection[] = [
|
||||||
|
{
|
||||||
|
title: "Préambule",
|
||||||
|
type: navigationsTypes.GLOBAL,
|
||||||
|
position: "start",
|
||||||
|
links: [
|
||||||
|
{ title: "Memento Dev", href: "/", subitems: [] },
|
||||||
|
{ title: "Certifications", href: "/certifications", subitems: [] },
|
||||||
|
{ title: "Documentations", href: "/docs", subitems: [] },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Communauté",
|
||||||
|
type: navigationsTypes.GLOBAL,
|
||||||
|
position: "start",
|
||||||
|
links: [
|
||||||
|
{
|
||||||
|
title: "Influenceurs",
|
||||||
|
href: "/docs/communaute/influenceurs",
|
||||||
|
subitems: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Partages et réutilisations",
|
||||||
|
href: "/docs/communaute/partages",
|
||||||
|
subitems: [],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Légal",
|
||||||
|
type: navigationsTypes.OTHER,
|
||||||
|
position: "end",
|
||||||
|
links: [
|
||||||
|
{ title: "Mentions légales", href: "/mentions-legales", subitems: [] },
|
||||||
|
{
|
||||||
|
title: "Politique de confidentialité",
|
||||||
|
href: "/politique-de-confidentialite",
|
||||||
|
subitems: [],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Développeur Web et Web Mobile",
|
||||||
|
type: navigationsTypes.CERTIFICATIONS,
|
||||||
|
position: "auto",
|
||||||
|
links: [
|
||||||
|
{ title: "Résumé du titre", href: "/certifications/dwwm", subitems: [] },
|
||||||
|
{
|
||||||
|
title: "Activité Type 1",
|
||||||
|
href: "/certifications/dwwm/at1",
|
||||||
|
subitems: [
|
||||||
|
{ title: "Résumé de l'AT", href: "/certifications/dwwm/at1" },
|
||||||
|
{ title: "CP 1", href: "/certifications/dwwm/at1/cp1" },
|
||||||
|
{ title: "CP 2", href: "/certifications/dwwm/at1/cp2" },
|
||||||
|
{ title: "CP 3", href: "/certifications/dwwm/at1/cp3" },
|
||||||
|
{ title: "CP 4", href: "/certifications/dwwm/at1/cp4" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Activité Type 2",
|
||||||
|
href: "/certifications/dwwm/at2",
|
||||||
|
subitems: [
|
||||||
|
{ title: "Résumé de l'AT", href: "/certifications/dwwm/at2" },
|
||||||
|
{ title: "CP 5", href: "/certifications/dwwm/at2/cp5" },
|
||||||
|
{ title: "CP 6", href: "/certifications/dwwm/at2/cp6" },
|
||||||
|
{ title: "CP 7", href: "/certifications/dwwm/at2/cp7" },
|
||||||
|
{ title: "CP 8", href: "/certifications/dwwm/at2/cp8" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Front-end",
|
||||||
|
type: navigationsTypes.DOCUMENTATIONS,
|
||||||
|
position: "auto",
|
||||||
|
links: [
|
||||||
|
{
|
||||||
|
title: "React",
|
||||||
|
href: "/docs/react",
|
||||||
|
subitems: [
|
||||||
|
{ title: "Introduction", href: "/docs/react" },
|
||||||
|
{ title: "Initialisation", href: "/docs/react/initialisation" },
|
||||||
|
{ title: "Syntaxe JSX", href: "/docs/react/jsx" },
|
||||||
|
{ title: "Premier composant", href: "/docs/react/premier-composant" },
|
||||||
|
{
|
||||||
|
title: "State et cycle de vie",
|
||||||
|
href: "/docs/react/state-et-cycle-de-vie",
|
||||||
|
},
|
||||||
|
{ title: "Hooks", href: "/docs/react/hooks" },
|
||||||
|
{ title: "Le hook useContext", href: "/docs/react/use-context" },
|
||||||
|
{ title: "Le hook useReducer", href: "/docs/react/use-reducer" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Base de données",
|
||||||
|
type: navigationsTypes.DOCUMENTATIONS,
|
||||||
|
position: "auto",
|
||||||
|
links: [
|
||||||
|
{
|
||||||
|
title: "Merise",
|
||||||
|
href: "/docs/merise",
|
||||||
|
og: { image: "/merise/og.webp" },
|
||||||
|
subitems: [
|
||||||
|
{ title: "Introduction", href: "/docs/merise" },
|
||||||
|
{
|
||||||
|
title: "Dictionnaire de données",
|
||||||
|
href: "/docs/merise/dictionnaire-de-donnees",
|
||||||
|
},
|
||||||
|
{ title: "Modèle Conceptuel de Données", href: "/docs/merise/mcd" },
|
||||||
|
{ title: "Modèle Logique de Données", href: "/docs/merise/mld" },
|
||||||
|
{ title: "Modèle Physique de Données", href: "/docs/merise/mpd" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export function doesLinkSubitemExist(
|
||||||
|
link: NavigationLink,
|
||||||
|
subitemHref: string,
|
||||||
|
): boolean {
|
||||||
|
return link.subitems.some((subitem) => subitem.href === subitemHref);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function findNavigationLink(
|
||||||
|
namespace: string,
|
||||||
|
href?: string,
|
||||||
|
): NavigationLink | undefined {
|
||||||
|
const currentUrl = `/${namespace}/${href}`
|
||||||
|
.replace(/\/+/g, "/")
|
||||||
|
.replace(/\/$/, "");
|
||||||
|
|
||||||
|
const foundLink = navigation
|
||||||
|
.flatMap((section) => section.links)
|
||||||
|
.find((link) => {
|
||||||
|
return link.href === currentUrl || doesLinkSubitemExist(link, currentUrl);
|
||||||
|
});
|
||||||
|
|
||||||
|
return foundLink;
|
||||||
|
}
|
||||||
100
app/libs/sections.ts
Normal file
100
app/libs/sections.ts
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
import type { Node } from "@markdoc/markdoc";
|
||||||
|
|
||||||
|
import { slugifyWithCounter } from "@sindresorhus/slugify";
|
||||||
|
|
||||||
|
interface HeadingNode extends Node {
|
||||||
|
type: "heading";
|
||||||
|
attributes: {
|
||||||
|
level: 1 | 2 | 3 | 4 | 5 | 6;
|
||||||
|
id?: string;
|
||||||
|
[key: string]: unknown;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
type H2Node = HeadingNode & {
|
||||||
|
attributes: {
|
||||||
|
level: 2;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
type H3Node = HeadingNode & {
|
||||||
|
attributes: {
|
||||||
|
level: 3;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
function isHeadingNode(node: Node): node is HeadingNode {
|
||||||
|
return (
|
||||||
|
node.type === "heading" &&
|
||||||
|
[1, 2, 3, 4, 5, 6].includes(node.attributes.level) &&
|
||||||
|
(typeof node.attributes.id === "string" ||
|
||||||
|
typeof node.attributes.id === "undefined")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isH2Node(node: Node): node is H2Node {
|
||||||
|
return isHeadingNode(node) && node.attributes.level === 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isH3Node(node: Node): node is H3Node {
|
||||||
|
return isHeadingNode(node) && node.attributes.level === 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getNodeText(node: Node) {
|
||||||
|
let text = "";
|
||||||
|
|
||||||
|
for (const child of node.children ?? []) {
|
||||||
|
if (child.type === "text") {
|
||||||
|
text += child.attributes.content;
|
||||||
|
}
|
||||||
|
text += getNodeText(child);
|
||||||
|
}
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Subsection = H3Node["attributes"] & {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
children?: undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Section = H2Node["attributes"] & {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
children: Array<Subsection>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function collectSections(
|
||||||
|
nodes: Array<Node>,
|
||||||
|
slugify = slugifyWithCounter(),
|
||||||
|
) {
|
||||||
|
const sections: Array<Section> = [];
|
||||||
|
|
||||||
|
for (const node of nodes) {
|
||||||
|
if (isH2Node(node) || isH3Node(node)) {
|
||||||
|
const title = getNodeText(node);
|
||||||
|
if (title) {
|
||||||
|
const id = slugify(title);
|
||||||
|
|
||||||
|
if (isH3Node(node)) {
|
||||||
|
if (!sections[sections.length - 1]) {
|
||||||
|
throw new Error(
|
||||||
|
"Cannot add `h3` to table of contents without a preceding `h2`",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
sections[sections.length - 1].children.push({
|
||||||
|
...node.attributes,
|
||||||
|
id,
|
||||||
|
title,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
sections.push({ ...node.attributes, id, title, children: [] });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sections.push(...collectSections(node.children ?? [], slugify));
|
||||||
|
}
|
||||||
|
|
||||||
|
return sections;
|
||||||
|
}
|
||||||
@ -8,15 +8,20 @@
|
|||||||
"format": "biome format --write ."
|
"format": "biome format --write ."
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"vike": "^0.4.228",
|
|
||||||
"@fastify/middie": "^9.0.3",
|
"@fastify/middie": "^9.0.3",
|
||||||
"@fastify/static": "^8.1.1",
|
"@fastify/static": "^8.1.1",
|
||||||
"@universal-middleware/fastify": "^0.5.16",
|
"@sindresorhus/slugify": "^2.2.1",
|
||||||
"fastify": "^5.3.0",
|
|
||||||
"@universal-middleware/core": "^0.4.7",
|
"@universal-middleware/core": "^0.4.7",
|
||||||
|
"@universal-middleware/fastify": "^0.5.16",
|
||||||
|
"clsx": "^2.1.1",
|
||||||
|
"fastify": "^5.3.0",
|
||||||
|
"solid-heroicons": "^3.2.4",
|
||||||
|
"solid-highlight": "^0.1.26",
|
||||||
"solid-js": "^1.9.5",
|
"solid-js": "^1.9.5",
|
||||||
"vike-solid": "^0.7.9",
|
"telefunc": "^0.2.3",
|
||||||
"telefunc": "^0.2.3"
|
"terracotta": "^1.0.6",
|
||||||
|
"vike": "^0.4.228",
|
||||||
|
"vike-solid": "^0.7.9"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@biomejs/biome": "1.9.4",
|
"@biomejs/biome": "1.9.4",
|
||||||
|
|||||||
190
app/partials/HeroBackground.tsx
Normal file
190
app/partials/HeroBackground.tsx
Normal file
@ -0,0 +1,190 @@
|
|||||||
|
import type { JSX } from "solid-js";
|
||||||
|
|
||||||
|
import { createUniqueId } from "solid-js";
|
||||||
|
|
||||||
|
export function HeroBackground(props: JSX.IntrinsicElements["svg"]) {
|
||||||
|
const id = createUniqueId();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
viewBox="0 0 668 1069"
|
||||||
|
width={668}
|
||||||
|
height={1069}
|
||||||
|
fill="none"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<defs>
|
||||||
|
<clipPath id={`${id}-clip-path`}>
|
||||||
|
<path
|
||||||
|
fill="#fff"
|
||||||
|
transform="rotate(-180 334 534.4)"
|
||||||
|
d="M0 0h668v1068.8H0z"
|
||||||
|
/>
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
<g opacity=".4" clip-path={`url(#${id}-clip-path)`} stroke-width={4}>
|
||||||
|
<path
|
||||||
|
opacity=".3"
|
||||||
|
d="M584.5 770.4v-474M484.5 770.4v-474M384.5 770.4v-474M283.5 769.4v-474M183.5 768.4v-474M83.5 767.4v-474"
|
||||||
|
stroke="#334155"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M83.5 221.275v6.587a50.1 50.1 0 0 0 22.309 41.686l55.581 37.054a50.102 50.102 0 0 1 22.309 41.686v6.587M83.5 716.012v6.588a50.099 50.099 0 0 0 22.309 41.685l55.581 37.054a50.102 50.102 0 0 1 22.309 41.686v6.587M183.7 584.5v6.587a50.1 50.1 0 0 0 22.31 41.686l55.581 37.054a50.097 50.097 0 0 1 22.309 41.685v6.588M384.101 277.637v6.588a50.1 50.1 0 0 0 22.309 41.685l55.581 37.054a50.1 50.1 0 0 1 22.31 41.686v6.587M384.1 770.288v6.587a50.1 50.1 0 0 1-22.309 41.686l-55.581 37.054A50.099 50.099 0 0 0 283.9 897.3v6.588"
|
||||||
|
stroke="#334155"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M384.1 770.288v6.587a50.1 50.1 0 0 1-22.309 41.686l-55.581 37.054A50.099 50.099 0 0 0 283.9 897.3v6.588M484.3 594.937v6.587a50.1 50.1 0 0 1-22.31 41.686l-55.581 37.054A50.1 50.1 0 0 0 384.1 721.95v6.587M484.3 872.575v6.587a50.1 50.1 0 0 1-22.31 41.686l-55.581 37.054a50.098 50.098 0 0 0-22.309 41.686v6.582M584.501 663.824v39.988a50.099 50.099 0 0 1-22.31 41.685l-55.581 37.054a50.102 50.102 0 0 0-22.309 41.686v6.587M283.899 945.637v6.588a50.1 50.1 0 0 1-22.309 41.685l-55.581 37.05a50.12 50.12 0 0 0-22.31 41.69v6.59M384.1 277.637c0 19.946 12.763 37.655 31.686 43.962l137.028 45.676c18.923 6.308 31.686 24.016 31.686 43.962M183.7 463.425v30.69c0 21.564 13.799 40.709 34.257 47.529l134.457 44.819c18.922 6.307 31.686 24.016 31.686 43.962M83.5 102.288c0 19.515 13.554 36.412 32.604 40.645l235.391 52.309c19.05 4.234 32.605 21.13 32.605 40.646M83.5 463.425v-58.45M183.699 542.75V396.625M283.9 1068.8V945.637M83.5 363.225v-141.95M83.5 179.524v-77.237M83.5 60.537V0M384.1 630.425V277.637M484.301 830.824V594.937M584.5 1068.8V663.825M484.301 555.275V452.988M584.5 622.075V452.988M384.1 728.537v-56.362M384.1 1068.8v-20.88M384.1 1006.17V770.287M283.9 903.888V759.85M183.699 1066.71V891.362M83.5 1068.8V716.012M83.5 674.263V505.175"
|
||||||
|
stroke="#334155"
|
||||||
|
/>
|
||||||
|
<circle
|
||||||
|
cx="83.5"
|
||||||
|
cy="384.1"
|
||||||
|
r="10.438"
|
||||||
|
transform="rotate(-180 83.5 384.1)"
|
||||||
|
fill="#1E293B"
|
||||||
|
stroke="#334155"
|
||||||
|
/>
|
||||||
|
<circle
|
||||||
|
cx="83.5"
|
||||||
|
cy="200.399"
|
||||||
|
r="10.438"
|
||||||
|
transform="rotate(-180 83.5 200.399)"
|
||||||
|
stroke="#334155"
|
||||||
|
/>
|
||||||
|
<circle
|
||||||
|
cx="83.5"
|
||||||
|
cy="81.412"
|
||||||
|
r="10.438"
|
||||||
|
transform="rotate(-180 83.5 81.412)"
|
||||||
|
stroke="#334155"
|
||||||
|
/>
|
||||||
|
<circle
|
||||||
|
cx="183.699"
|
||||||
|
cy="375.75"
|
||||||
|
r="10.438"
|
||||||
|
transform="rotate(-180 183.699 375.75)"
|
||||||
|
fill="#1E293B"
|
||||||
|
stroke="#334155"
|
||||||
|
/>
|
||||||
|
<circle
|
||||||
|
cx="183.699"
|
||||||
|
cy="563.625"
|
||||||
|
r="10.438"
|
||||||
|
transform="rotate(-180 183.699 563.625)"
|
||||||
|
fill="#1E293B"
|
||||||
|
stroke="#334155"
|
||||||
|
/>
|
||||||
|
<circle
|
||||||
|
cx="384.1"
|
||||||
|
cy="651.3"
|
||||||
|
r="10.438"
|
||||||
|
transform="rotate(-180 384.1 651.3)"
|
||||||
|
fill="#1E293B"
|
||||||
|
stroke="#334155"
|
||||||
|
/>
|
||||||
|
<circle
|
||||||
|
cx="484.301"
|
||||||
|
cy="574.062"
|
||||||
|
r="10.438"
|
||||||
|
transform="rotate(-180 484.301 574.062)"
|
||||||
|
fill="#0EA5E9"
|
||||||
|
fill-opacity=".42"
|
||||||
|
stroke="#0EA5E9"
|
||||||
|
/>
|
||||||
|
<circle
|
||||||
|
cx="384.1"
|
||||||
|
cy="749.412"
|
||||||
|
r="10.438"
|
||||||
|
transform="rotate(-180 384.1 749.412)"
|
||||||
|
fill="#1E293B"
|
||||||
|
stroke="#334155"
|
||||||
|
/>
|
||||||
|
<circle
|
||||||
|
cx="384.1"
|
||||||
|
cy="1027.05"
|
||||||
|
r="10.438"
|
||||||
|
transform="rotate(-180 384.1 1027.05)"
|
||||||
|
stroke="#334155"
|
||||||
|
/>
|
||||||
|
<circle
|
||||||
|
cx="283.9"
|
||||||
|
cy="924.763"
|
||||||
|
r="10.438"
|
||||||
|
transform="rotate(-180 283.9 924.763)"
|
||||||
|
stroke="#334155"
|
||||||
|
/>
|
||||||
|
<circle
|
||||||
|
cx="183.699"
|
||||||
|
cy="870.487"
|
||||||
|
r="10.438"
|
||||||
|
transform="rotate(-180 183.699 870.487)"
|
||||||
|
stroke="#334155"
|
||||||
|
/>
|
||||||
|
<circle
|
||||||
|
cx="283.9"
|
||||||
|
cy="738.975"
|
||||||
|
r="10.438"
|
||||||
|
transform="rotate(-180 283.9 738.975)"
|
||||||
|
fill="#1E293B"
|
||||||
|
stroke="#334155"
|
||||||
|
/>
|
||||||
|
<circle
|
||||||
|
cx="83.5"
|
||||||
|
cy="695.138"
|
||||||
|
r="10.438"
|
||||||
|
transform="rotate(-180 83.5 695.138)"
|
||||||
|
fill="#1E293B"
|
||||||
|
stroke="#334155"
|
||||||
|
/>
|
||||||
|
<circle
|
||||||
|
cx="83.5"
|
||||||
|
cy="484.3"
|
||||||
|
r="10.438"
|
||||||
|
transform="rotate(-180 83.5 484.3)"
|
||||||
|
fill="#0EA5E9"
|
||||||
|
fill-opacity=".42"
|
||||||
|
stroke="#0EA5E9"
|
||||||
|
/>
|
||||||
|
<circle
|
||||||
|
cx="484.301"
|
||||||
|
cy="432.112"
|
||||||
|
r="10.438"
|
||||||
|
transform="rotate(-180 484.301 432.112)"
|
||||||
|
fill="#1E293B"
|
||||||
|
stroke="#334155"
|
||||||
|
/>
|
||||||
|
<circle
|
||||||
|
cx="584.5"
|
||||||
|
cy="432.112"
|
||||||
|
r="10.438"
|
||||||
|
transform="rotate(-180 584.5 432.112)"
|
||||||
|
fill="#1E293B"
|
||||||
|
stroke="#334155"
|
||||||
|
/>
|
||||||
|
<circle
|
||||||
|
cx="584.5"
|
||||||
|
cy="642.95"
|
||||||
|
r="10.438"
|
||||||
|
transform="rotate(-180 584.5 642.95)"
|
||||||
|
fill="#1E293B"
|
||||||
|
stroke="#334155"
|
||||||
|
/>
|
||||||
|
<circle
|
||||||
|
cx="484.301"
|
||||||
|
cy="851.699"
|
||||||
|
r="10.438"
|
||||||
|
transform="rotate(-180 484.301 851.699)"
|
||||||
|
stroke="#334155"
|
||||||
|
/>
|
||||||
|
<circle
|
||||||
|
cx="384.1"
|
||||||
|
cy="256.763"
|
||||||
|
r="10.438"
|
||||||
|
transform="rotate(-180 384.1 256.763)"
|
||||||
|
stroke="#334155"
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
161
app/partials/HeroSection.tsx
Normal file
161
app/partials/HeroSection.tsx
Normal file
@ -0,0 +1,161 @@
|
|||||||
|
import type { JSX } from "solid-js";
|
||||||
|
|
||||||
|
import blurIndigoImage from "@/images/blur-indigo.webp";
|
||||||
|
import blurCyanImage from "@/images/blur-cyan.webp";
|
||||||
|
import { HeroBackground } from "./HeroBackground";
|
||||||
|
import { Button } from "@/components/Button";
|
||||||
|
import { Highlight } from "solid-highlight";
|
||||||
|
import { Image } from "@/components/Image";
|
||||||
|
import { For } from "solid-js";
|
||||||
|
import clsx from "clsx";
|
||||||
|
|
||||||
|
const codeLanguage = "javascript";
|
||||||
|
const code = `export default {
|
||||||
|
role: 'developer',
|
||||||
|
qualifications: [
|
||||||
|
'DWWM',
|
||||||
|
'CDA',
|
||||||
|
'CDUI',
|
||||||
|
]
|
||||||
|
}`;
|
||||||
|
|
||||||
|
const tabs = [
|
||||||
|
{ name: "memento-dev.config.js", isActive: true },
|
||||||
|
{ name: "package.json", isActive: false },
|
||||||
|
];
|
||||||
|
|
||||||
|
function TrafficLightsIcon(props: JSX.IntrinsicElements["svg"]) {
|
||||||
|
return (
|
||||||
|
<svg aria-hidden="true" viewBox="0 0 42 10" fill="none" {...props}>
|
||||||
|
<circle cx="5" cy="5" r="4.5" />
|
||||||
|
<circle cx="21" cy="5" r="4.5" />
|
||||||
|
<circle cx="37" cy="5" r="4.5" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Hero() {
|
||||||
|
return (
|
||||||
|
<div class="overflow-hidden bg-slate-900 dark:mt-[-4.75rem] dark:-mb-32 dark:pt-[4.75rem] dark:pb-32">
|
||||||
|
<div class="py-16 sm:px-2 lg:relative lg:px-0 lg:py-20">
|
||||||
|
<div class="mx-auto grid max-w-2xl w-full grid-cols-1 items-center gap-x-8 gap-y-16 px-4 lg:max-w-8xl lg:grid-cols-2 lg:px-8 xl:gap-x-16 xl:px-12">
|
||||||
|
<div class="relative z-10 md:text-center lg:text-left">
|
||||||
|
<Image
|
||||||
|
class="absolute right-full bottom-full -mr-72 -mb-56 opacity-50"
|
||||||
|
src={blurCyanImage}
|
||||||
|
alt=""
|
||||||
|
width={530}
|
||||||
|
height={530}
|
||||||
|
/>
|
||||||
|
<div class="relative">
|
||||||
|
<p class="inline bg-linear-to-r from-indigo-200 via-violet-400 to-indigo-200 bg-clip-text font-display text-5xl tracking-tight text-transparent">
|
||||||
|
Souviens-toi que tu développeras.
|
||||||
|
</p>
|
||||||
|
<p class="mt-3 text-2xl tracking-tight text-slate-400">
|
||||||
|
Découvrez des ressources essentielles pour améliorer tes
|
||||||
|
compétences en développement.
|
||||||
|
</p>
|
||||||
|
<div class="mt-8 flex gap-4 md:justify-center lg:justify-start">
|
||||||
|
<Button href="/docs">Accédez aux ressources</Button>
|
||||||
|
<Button
|
||||||
|
href="https://github.com/GauthierWebDev/memento-dev"
|
||||||
|
variant="secondary"
|
||||||
|
>
|
||||||
|
Voir sur Github
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="relative lg:static xl:pl-10">
|
||||||
|
<div class="absolute inset-x-[-50vw] -top-32 -bottom-48 [mask-image:linear-gradient(transparent,white,white)] lg:-top-32 lg:right-0 lg:-bottom-32 lg:left-[calc(50%+14rem)] lg:[mask-image:none] dark:[mask-image:linear-gradient(transparent,white,transparent)] lg:dark:[mask-image:linear-gradient(white,white,transparent)]">
|
||||||
|
<HeroBackground class="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 lg:left-0 lg:translate-x-0 lg:translate-y-[-60%]" />
|
||||||
|
</div>
|
||||||
|
<div class="relative">
|
||||||
|
<Image
|
||||||
|
class="absolute -top-64 -right-64"
|
||||||
|
src={blurCyanImage}
|
||||||
|
alt=""
|
||||||
|
width={530}
|
||||||
|
height={530}
|
||||||
|
/>
|
||||||
|
<Image
|
||||||
|
class="absolute -right-44 -bottom-40"
|
||||||
|
src={blurIndigoImage}
|
||||||
|
alt=""
|
||||||
|
width={567}
|
||||||
|
height={567}
|
||||||
|
/>
|
||||||
|
<div class="absolute inset-0 rounded-2xl bg-linear-to-tr from-violet-300 via-violet-300/70 to-purple-300 opacity-10 blur-lg" />
|
||||||
|
<div class="absolute inset-0 rounded-2xl bg-linear-to-tr from-violet-300 via-violet-300/70 to-purple-300 opacity-10" />
|
||||||
|
<div class="relative rounded-2xl bg-[#0A101F]/80 ring-1 ring-white/10 backdrop-blur-sm">
|
||||||
|
<div class="absolute -top-px right-11 left-20 h-px bg-linear-to-r from-violet-300/0 via-violet-300/70 to-violet-300/0" />
|
||||||
|
<div class="absolute right-20 -bottom-px left-11 h-px bg-linear-to-r from-purple-400/0 via-purple-400 to-purple-400/0" />
|
||||||
|
<div class="pt-4 pl-4">
|
||||||
|
<TrafficLightsIcon class="h-2.5 w-auto stroke-slate-500/30" />
|
||||||
|
<div class="mt-4 flex space-x-2 text-xs">
|
||||||
|
<For each={tabs}>
|
||||||
|
{(tab) => (
|
||||||
|
<div
|
||||||
|
class={clsx(
|
||||||
|
"flex h-6 rounded-full",
|
||||||
|
tab.isActive
|
||||||
|
? "bg-linear-to-r from-violet-400/30 via-violet-400 to-violet-400/30 p-px font-medium text-violet-300"
|
||||||
|
: "text-slate-500",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class={clsx(
|
||||||
|
"flex items-center rounded-full px-2.5",
|
||||||
|
tab.isActive && "bg-slate-800",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{tab.name}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
<div class="mt-6 flex items-start px-1 text-sm">
|
||||||
|
<div
|
||||||
|
aria-hidden="true"
|
||||||
|
class="border-r border-slate-300/5 pr-4 font-mono text-slate-600 select-none"
|
||||||
|
>
|
||||||
|
<For
|
||||||
|
each={Array.from({ length: code.split("\n").length })}
|
||||||
|
>
|
||||||
|
{(_, index) => (
|
||||||
|
<>
|
||||||
|
{(index() + 1).toString().padStart(2, "0")}
|
||||||
|
<br />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Highlight language={codeLanguage}>{code}</Highlight>
|
||||||
|
|
||||||
|
{/* <Highlight code={code} language={codeLanguage} theme={{ plain: {}, styles: [] }}>
|
||||||
|
{({ class, style, tokens, getLineProps, getTokenProps }) => (
|
||||||
|
<pre class={clsx(class, "flex overflow-x-auto pb-6")} style={style}>
|
||||||
|
<code class="px-4">
|
||||||
|
{tokens.map((line, lineIndex) => (
|
||||||
|
<div key={lineIndex} {...getLineProps({ line })}>
|
||||||
|
{line.map((token, tokenIndex) => (
|
||||||
|
<span key={tokenIndex} {...getTokenProps({ token })} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</code>
|
||||||
|
</pre>
|
||||||
|
)}
|
||||||
|
</Highlight> */}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
92
app/partials/MobileNavigation.tsx
Normal file
92
app/partials/MobileNavigation.tsx
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
import type { JSX } from "solid-js";
|
||||||
|
|
||||||
|
import { usePageContext } from "vike-solid/usePageContext";
|
||||||
|
import { createEffect, createSignal } from "solid-js";
|
||||||
|
import { Dialog, DialogPanel } from "terracotta";
|
||||||
|
import { Navigation } from "./Navigation";
|
||||||
|
import { Link } from "@/components/Link";
|
||||||
|
import { Logo } from "@/components/Logo";
|
||||||
|
|
||||||
|
function MenuIcon(props: JSX.IntrinsicElements["svg"]) {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<path d="M4 7h16M4 12h16M4 17h16" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function CloseIcon(props: JSX.IntrinsicElements["svg"]) {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<path d="M5 5l14 14M19 5l-14 14" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function CloseOnNavigation({ close }: { close: () => void }) {
|
||||||
|
const { urlPathname } = usePageContext();
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
close();
|
||||||
|
}, [urlPathname, close]);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function MobileNavigation() {
|
||||||
|
const [isOpen, setIsOpen] = createSignal(false);
|
||||||
|
const close = () => setIsOpen(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setIsOpen(true)}
|
||||||
|
class="relative"
|
||||||
|
aria-label="Ouvrir le menu de navigation"
|
||||||
|
>
|
||||||
|
<MenuIcon class="h-6 w-6 stroke-slate-500" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<CloseOnNavigation close={close} />
|
||||||
|
|
||||||
|
<Dialog
|
||||||
|
isOpen={isOpen()}
|
||||||
|
onClose={close}
|
||||||
|
class="fixed inset-0 z-50 flex items-start overflow-y-auto bg-slate-900/50 pr-10 backdrop-blur-sm lg:hidden"
|
||||||
|
aria-label="Navigation"
|
||||||
|
>
|
||||||
|
<DialogPanel class="min-h-full w-full max-w-xs bg-white px-4 pt-5 pb-12 sm:px-6 dark:bg-slate-900">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={close}
|
||||||
|
aria-label="Fermer le menu de navigation"
|
||||||
|
>
|
||||||
|
<CloseIcon class="h-6 w-6 stroke-slate-500" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<Link href="/" class="ml-6" aria-label="Page d'accueil">
|
||||||
|
<Logo class="h-6 w-auto shrink-0" />
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
<Navigation class="mt-5 px-1" onLinkClick={close} />
|
||||||
|
</DialogPanel>
|
||||||
|
</Dialog>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
259
app/partials/Navigation.tsx
Normal file
259
app/partials/Navigation.tsx
Normal file
@ -0,0 +1,259 @@
|
|||||||
|
import { chevronDown, chevronUp } from "solid-heroicons/solid";
|
||||||
|
import { createEffect, createSignal, For } from "solid-js";
|
||||||
|
import { usePageContext } from "vike-solid/usePageContext";
|
||||||
|
import { navigation } from "@/libs/navigation";
|
||||||
|
import { Link } from "@/components/Link";
|
||||||
|
import { Icon } from "solid-heroicons";
|
||||||
|
import clsx from "clsx";
|
||||||
|
|
||||||
|
type NavigationItemProps = {
|
||||||
|
section: (typeof navigation)[number];
|
||||||
|
onLinkClick?: (event: MouseEvent) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
function NavigationItem(props: NavigationItemProps) {
|
||||||
|
const { urlPathname } = usePageContext();
|
||||||
|
|
||||||
|
const [isOpened, setIsOpened] = createSignal(
|
||||||
|
props.section.links.some(
|
||||||
|
(link) =>
|
||||||
|
link.href === urlPathname ||
|
||||||
|
link.subitems?.some((subitem) => subitem.href === urlPathname),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<h2
|
||||||
|
class={clsx(
|
||||||
|
"font-display font-medium cursor-pointer",
|
||||||
|
isOpened()
|
||||||
|
? "text-violet-600 dark:text-violet-200"
|
||||||
|
: "text-slate-900 dark:text-white ",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
onClick={() => setIsOpened((prev) => !prev)}
|
||||||
|
type="button"
|
||||||
|
aria-label={`${isOpened() ? "Masquer" : "Afficher"} les sous-sections de ${props.section.title}`}
|
||||||
|
>
|
||||||
|
<span class="sr-only">{isOpened() ? "Masquer" : "Afficher"}</span>
|
||||||
|
|
||||||
|
{isOpened() ? (
|
||||||
|
<Icon
|
||||||
|
path={chevronUp}
|
||||||
|
class="inline-block mr-2 h-5 w-5 text-slate-400"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Icon
|
||||||
|
path={chevronDown}
|
||||||
|
class="inline-block mr-2 h-5 w-5 text-slate-400"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<span class="sr-only">{isOpened() ? "Masquer" : "Afficher"}</span>
|
||||||
|
|
||||||
|
{props.section.title}
|
||||||
|
|
||||||
|
<span class="text-slate-400 dark:text-slate-500">
|
||||||
|
{" "}
|
||||||
|
({props.section.links.length})
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</h2>
|
||||||
|
{isOpened() && (
|
||||||
|
<ul class="!mt-0 ml-2 space-y-1 border-l-2 border-slate-100 lg:mt-4 lg:space-y-2 lg:border-slate-200 dark:border-slate-800 mb-4">
|
||||||
|
<For each={props.section.links}>
|
||||||
|
{(link) => (
|
||||||
|
<li class="relative">
|
||||||
|
<NavigationSubItem
|
||||||
|
link={link}
|
||||||
|
onLinkClick={props.onLinkClick}
|
||||||
|
isOpened={
|
||||||
|
link.href === urlPathname ||
|
||||||
|
link.subitems?.some(
|
||||||
|
(subitem) => subitem.href === urlPathname,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
type NavigationSubItemProps = {
|
||||||
|
link: (typeof navigation)[number]["links"][number];
|
||||||
|
onLinkClick?: (event: MouseEvent) => void;
|
||||||
|
isOpened?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
function NavigationSubItem(props: NavigationSubItemProps) {
|
||||||
|
const [isOpened, setIsOpened] = createSignal(props.isOpened);
|
||||||
|
const { urlPathname } = usePageContext();
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
setIsOpened(
|
||||||
|
props.link.href === urlPathname ||
|
||||||
|
props.link.subitems?.some((subitem) => subitem.href === urlPathname),
|
||||||
|
);
|
||||||
|
}, [urlPathname, props.link]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<span class="pl-2 flex cursor-pointer">
|
||||||
|
{props.link.subitems.length > 0 && (
|
||||||
|
<button
|
||||||
|
onClick={() => setIsOpened((prev) => !prev)}
|
||||||
|
type="button"
|
||||||
|
aria-label={`${isOpened() ? "Masquer" : "Afficher"} les sous-sections de ${props.link.title}`}
|
||||||
|
>
|
||||||
|
<span class="sr-only">{isOpened() ? "Masquer" : "Afficher"}</span>
|
||||||
|
|
||||||
|
{isOpened() ? (
|
||||||
|
<Icon
|
||||||
|
path={chevronUp}
|
||||||
|
class="inline-block h-5 w-5 text-slate-400"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Icon
|
||||||
|
path={chevronDown}
|
||||||
|
class="inline-block h-5 w-5 text-slate-400"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Link
|
||||||
|
href={props.link.href}
|
||||||
|
onClick={props.onLinkClick ?? undefined}
|
||||||
|
class={clsx(
|
||||||
|
"block pl-2 w-full before:pointer-events-none before:absolute before:-left-1 before:h-1.5 before:w-1.5 before:rounded-full",
|
||||||
|
{ "before:top-1/2 before:-translate-y-1/2": !props.link.subitems },
|
||||||
|
{
|
||||||
|
"before:top-3 before:-translate-y-1/2 font-semibold":
|
||||||
|
props.link.subitems,
|
||||||
|
},
|
||||||
|
props.link.href !== urlPathname && "before:hidden",
|
||||||
|
isOpened()
|
||||||
|
? "text-violet-500 before:bg-violet-500"
|
||||||
|
: "text-slate-500 before:bg-slate-300 hover:text-slate-600 hover:before:block dark:text-slate-400 dark:before:bg-slate-700 dark:hover:text-slate-300",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{props.link.title}
|
||||||
|
{props.link.subitems.length > 0 && (
|
||||||
|
<span class="text-slate-400 dark:text-slate-500">
|
||||||
|
{" "}
|
||||||
|
({props.link.subitems.length})
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</Link>
|
||||||
|
</span>
|
||||||
|
{props.link.subitems.length > 0 && isOpened() && (
|
||||||
|
<ul class="ml-4 border-l-2 border-slate-100 space-y-1 lg:space-y-2 lg:border-slate-200 dark:border-slate-800 mb-4">
|
||||||
|
<For each={props.link.subitems}>
|
||||||
|
{(subitem) => (
|
||||||
|
<li class="relative">
|
||||||
|
<Link
|
||||||
|
href={subitem.href}
|
||||||
|
onClick={props.onLinkClick}
|
||||||
|
class={clsx(
|
||||||
|
"block w-full pl-3.5 before:pointer-events-none before:absolute before:top-1/2 before:-left-1 before:h-1.5 before:w-1.5 before:-translate-y-1/2 before:rounded-full",
|
||||||
|
subitem.href === urlPathname
|
||||||
|
? "font-semibold text-violet-500 before:bg-violet-500"
|
||||||
|
: "text-slate-500 before:hidden before:bg-slate-300 hover:text-slate-600 hover:before:block dark:text-slate-400 dark:before:bg-slate-700 dark:hover:text-slate-300",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{subitem.title}
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Navigation(props: {
|
||||||
|
class?: string;
|
||||||
|
onLinkClick?: (event: MouseEvent) => void;
|
||||||
|
}) {
|
||||||
|
const firstSections = navigation.filter(
|
||||||
|
(section) => section.position === "start",
|
||||||
|
);
|
||||||
|
const lastSections = navigation.filter(
|
||||||
|
(section) => section.position === "end",
|
||||||
|
);
|
||||||
|
|
||||||
|
const filteredSections = navigation
|
||||||
|
.filter(
|
||||||
|
(section) =>
|
||||||
|
section.position === "auto" || section.position === undefined,
|
||||||
|
)
|
||||||
|
.reduce(
|
||||||
|
(acc, section) => {
|
||||||
|
if (!acc[section.type]) {
|
||||||
|
acc[section.type] = [];
|
||||||
|
}
|
||||||
|
acc[section.type].push(section);
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{} as Record<string, typeof navigation>,
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<nav class={clsx("text-base lg:text-sm", props.class)}>
|
||||||
|
<ul class="space-y-4">
|
||||||
|
<li>
|
||||||
|
<h2 class="font-display font-bold text-base text-slate-900 dark:text-white">
|
||||||
|
{firstSections[0]?.type}
|
||||||
|
</h2>
|
||||||
|
<For each={firstSections}>
|
||||||
|
{(section) => (
|
||||||
|
<NavigationItem
|
||||||
|
section={section}
|
||||||
|
onLinkClick={props.onLinkClick}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<For each={Object.entries(filteredSections)}>
|
||||||
|
{([type, sections]) => (
|
||||||
|
<li>
|
||||||
|
<h2 class="font-display font-bold text-base text-slate-900 dark:text-white">
|
||||||
|
{type}
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<For each={sections}>
|
||||||
|
{(section) => (
|
||||||
|
<NavigationItem
|
||||||
|
section={section}
|
||||||
|
onLinkClick={props.onLinkClick}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
|
||||||
|
<li>
|
||||||
|
<h2 class="font-display font-bold text-base text-slate-900 dark:text-white">
|
||||||
|
{lastSections[0]?.type}
|
||||||
|
</h2>
|
||||||
|
<For each={lastSections}>
|
||||||
|
{(section) => (
|
||||||
|
<NavigationItem
|
||||||
|
section={section}
|
||||||
|
onLinkClick={props.onLinkClick}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
);
|
||||||
|
}
|
||||||
118
app/partials/TableOfContents.tsx
Normal file
118
app/partials/TableOfContents.tsx
Normal file
@ -0,0 +1,118 @@
|
|||||||
|
import type { Section, Subsection } from "@/libs/sections";
|
||||||
|
|
||||||
|
import { createSignal, createEffect, For } from "solid-js";
|
||||||
|
import { Link } from "@/components/Link";
|
||||||
|
import clsx from "clsx";
|
||||||
|
|
||||||
|
type TableOfContentsProps = {
|
||||||
|
tableOfContents: Array<Section>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function TableOfContents(props: TableOfContentsProps) {
|
||||||
|
const [currentSection, setCurrentSection] = createSignal(
|
||||||
|
props.tableOfContents[0]?.id,
|
||||||
|
);
|
||||||
|
|
||||||
|
const getHeadings = (tableOfContents: Array<Section>) => {
|
||||||
|
return tableOfContents
|
||||||
|
.flatMap((node) => [node.id, ...node.children.map((child) => child.id)])
|
||||||
|
.map((id) => {
|
||||||
|
const el = document.getElementById(id);
|
||||||
|
if (!el) return null;
|
||||||
|
|
||||||
|
const style = window.getComputedStyle(el);
|
||||||
|
const scrollMt = Number.parseFloat(style.scrollMarginTop);
|
||||||
|
|
||||||
|
const top = window.scrollY + el.getBoundingClientRect().top - scrollMt;
|
||||||
|
return { id, top };
|
||||||
|
})
|
||||||
|
.filter((x): x is { id: string; top: number } => x !== null);
|
||||||
|
};
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
if (props.tableOfContents.length === 0) return;
|
||||||
|
const headings = getHeadings(props.tableOfContents);
|
||||||
|
|
||||||
|
function onScroll() {
|
||||||
|
const top = window.scrollY;
|
||||||
|
|
||||||
|
let current = headings[0]?.id;
|
||||||
|
|
||||||
|
for (const heading of headings) {
|
||||||
|
if (top < heading.top - 10) break;
|
||||||
|
current = heading.id;
|
||||||
|
}
|
||||||
|
setCurrentSection(current);
|
||||||
|
}
|
||||||
|
window.addEventListener("scroll", onScroll, { passive: true });
|
||||||
|
onScroll();
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener("scroll", onScroll);
|
||||||
|
};
|
||||||
|
}, [getHeadings, props.tableOfContents]);
|
||||||
|
|
||||||
|
function isActive(section: Section | Subsection) {
|
||||||
|
if (section.id === currentSection()) return true;
|
||||||
|
if (!section.children) return false;
|
||||||
|
|
||||||
|
return section.children.findIndex(isActive) > -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div class="hidden xl:sticky xl:top-[4.75rem] xl:-mr-6 xl:block xl:h-[calc(100vh-4.75rem)] xl:flex-none xl:overflow-y-auto xl:py-16 xl:pr-6">
|
||||||
|
<nav aria-labelledby="on-this-page-title" class="w-56">
|
||||||
|
{props.tableOfContents.length > 0 && (
|
||||||
|
<>
|
||||||
|
<h2
|
||||||
|
id="on-this-page-title"
|
||||||
|
class="font-display text-sm font-medium text-slate-900 dark:text-white"
|
||||||
|
>
|
||||||
|
Table des matières
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<ol class="mt-4 space-y-3 text-sm">
|
||||||
|
<For each={props.tableOfContents}>
|
||||||
|
{(section) => (
|
||||||
|
<li>
|
||||||
|
<h3>
|
||||||
|
<Link
|
||||||
|
href={`#${section.id}`}
|
||||||
|
class={clsx(
|
||||||
|
isActive(section)
|
||||||
|
? "text-violet-500"
|
||||||
|
: "font-normal text-slate-500 hover:text-slate-700 dark:text-slate-400 dark:hover:text-slate-300",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{section.title}
|
||||||
|
</Link>
|
||||||
|
</h3>
|
||||||
|
{section.children.length > 0 && (
|
||||||
|
<ol class="mt-2 space-y-3 pl-5 text-slate-500 dark:text-slate-400">
|
||||||
|
<For each={section.children}>
|
||||||
|
{(subSection) => (
|
||||||
|
<li>
|
||||||
|
<Link
|
||||||
|
href={`#${subSection.id}`}
|
||||||
|
class={
|
||||||
|
isActive(subSection)
|
||||||
|
? "text-violet-500"
|
||||||
|
: "hover:text-slate-600 dark:hover:text-slate-300"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{subSection.title}
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</ol>
|
||||||
|
)}
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</ol>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -1,28 +1,23 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"forceConsistentCasingInFileNames": true,
|
"forceConsistentCasingInFileNames": true,
|
||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"sourceMap": true,
|
"sourceMap": true,
|
||||||
"module": "ESNext",
|
"module": "ESNext",
|
||||||
"noEmit": true,
|
"noEmit": true,
|
||||||
"moduleResolution": "Bundler",
|
"moduleResolution": "Bundler",
|
||||||
"target": "ES2022",
|
"target": "ES2022",
|
||||||
"lib": [
|
"lib": ["DOM", "DOM.Iterable", "ESNext"],
|
||||||
"DOM",
|
"types": ["vite/client", "vike-solid/client"],
|
||||||
"DOM.Iterable",
|
"jsx": "react-jsx",
|
||||||
"ESNext"
|
"jsxImportSource": "solid-js",
|
||||||
],
|
"baseUrl": ".",
|
||||||
"types": [
|
"paths": {
|
||||||
"vite/client",
|
"@/*": ["./*"]
|
||||||
"vike-solid/client"
|
}
|
||||||
],
|
},
|
||||||
"jsx": "react-jsx",
|
"exclude": ["dist"]
|
||||||
"jsxImportSource": "solid-js"
|
}
|
||||||
},
|
|
||||||
"exclude": [
|
|
||||||
"dist"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|||||||
10
app/utils/cleanProps.ts
Normal file
10
app/utils/cleanProps.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
export function cleanProps(
|
||||||
|
props: Record<string, unknown>,
|
||||||
|
...propsToRemove: string[]
|
||||||
|
): Record<string, unknown> {
|
||||||
|
const newProps = { ...props };
|
||||||
|
for (const prop of propsToRemove) {
|
||||||
|
delete newProps[prop];
|
||||||
|
}
|
||||||
|
return newProps;
|
||||||
|
}
|
||||||
@ -1,12 +1,20 @@
|
|||||||
import { telefunc } from "telefunc/vite";
|
|
||||||
import tailwindcss from "@tailwindcss/vite";
|
import tailwindcss from "@tailwindcss/vite";
|
||||||
|
import { telefunc } from "telefunc/vite";
|
||||||
import vikeSolid from "vike-solid/vite";
|
import vikeSolid from "vike-solid/vite";
|
||||||
import { defineConfig } from "vite";
|
import { defineConfig } from "vite";
|
||||||
import vike from "vike/plugin";
|
import vike from "vike/plugin";
|
||||||
|
import path from "node:path";
|
||||||
|
|
||||||
|
const __dirname = path.resolve();
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [vike(), vikeSolid(), tailwindcss(), telefunc()],
|
plugins: [vike(), vikeSolid(), tailwindcss(), telefunc()],
|
||||||
build: {
|
build: {
|
||||||
target: "es2022",
|
target: "es2022",
|
||||||
},
|
},
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
"@": __dirname,
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user