From aeaec6fc06706f8a8d16a0d6506d53417316feb1 Mon Sep 17 00:00:00 2001 From: Jules Neny Date: Fri, 8 May 2026 19:44:22 +0200 Subject: [PATCH 01/36] feat: PC1 scaffolding Astro 6 + Vue islands + Tailwind 4 + Embla swipe Initial structure for page-cerveau: - Astro 6.3.1 + @astrojs/vue 6.0.1 + Vue 3.5 - Tailwind 4 via @tailwindcss/vite (vs Tailwind 3.4 in prompt; @astrojs/tailwind incompatible with Astro 6 peer deps) - Embla Carousel Vue for mobile swipe (3 strict positions) - src/components/astro/ : 5 placeholder components (Col*, HamburgerMenu, PopupOnboarding) - src/components/vue/ : SwipeContainer + 3 placeholder islands - src/layouts/BaseLayout.astro - src/pages/index.astro (3 cols desktop ; SwipeContainer mobile) + manifeste.astro placeholder - public/data/ ready for PC3 (carte-o.json) + PC6 (journal.json) Build OK (0 errors, 0 warnings); dev server tested localhost:4321 with all components rendering. Note: Astro version is 6.3.1 (latest stable) instead of 5.x specified in prompt; 6.x is current LTS. --- .gitignore | 4 + astro.config.mjs | 12 + package-lock.json | 6589 ++++++++++++++++++++ package.json | 22 + public/favicon.ico | Bin 0 -> 655 bytes public/favicon.svg | 9 + src/components/astro/ColCentre.astro | 11 + src/components/astro/ColInsta.astro | 12 + src/components/astro/ColJournal.astro | 14 + src/components/astro/HamburgerMenu.astro | 12 + src/components/astro/PopupOnboarding.astro | 11 + src/components/vue/CarteO.vue | 9 + src/components/vue/ChatbotPlaceholder.vue | 9 + src/components/vue/JournalList.vue | 9 + src/components/vue/SwipeContainer.vue | 102 + src/layouts/BaseLayout.astro | 33 + src/pages/index.astro | 29 + src/pages/manifeste.astro | 9 + src/styles/global.css | 1 + tsconfig.json | 13 + 20 files changed, 6910 insertions(+) create mode 100644 astro.config.mjs create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 public/favicon.ico create mode 100644 public/favicon.svg create mode 100644 src/components/astro/ColCentre.astro create mode 100644 src/components/astro/ColInsta.astro create mode 100644 src/components/astro/ColJournal.astro create mode 100644 src/components/astro/HamburgerMenu.astro create mode 100644 src/components/astro/PopupOnboarding.astro create mode 100644 src/components/vue/CarteO.vue create mode 100644 src/components/vue/ChatbotPlaceholder.vue create mode 100644 src/components/vue/JournalList.vue create mode 100644 src/components/vue/SwipeContainer.vue create mode 100644 src/layouts/BaseLayout.astro create mode 100644 src/pages/index.astro create mode 100644 src/pages/manifeste.astro create mode 100644 src/styles/global.css create mode 100644 tsconfig.json diff --git a/.gitignore b/.gitignore index 2309cc8..14346a7 100644 --- a/.gitignore +++ b/.gitignore @@ -136,3 +136,7 @@ dist .yarn/install-state.gz .pnp.* +# Astro +.astro/ +dist/ +.DS_Store diff --git a/astro.config.mjs b/astro.config.mjs new file mode 100644 index 0000000..dac3789 --- /dev/null +++ b/astro.config.mjs @@ -0,0 +1,12 @@ +// @ts-check +import { defineConfig } from 'astro/config'; +import vue from '@astrojs/vue'; +import tailwindcss from '@tailwindcss/vite'; + +// https://astro.build/config +export default defineConfig({ + integrations: [vue()], + vite: { + plugins: [tailwindcss()], + }, +}); diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..0328247 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,6589 @@ +{ + "name": "astro-site-cerveau", + "version": "0.0.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "astro-site-cerveau", + "version": "0.0.1", + "dependencies": { + "@astrojs/vue": "^6.0.1", + "@tailwindcss/vite": "^4.2.4", + "astro": "^6.3.1", + "embla-carousel-vue": "^8.6.0", + "tailwindcss": "^4.2.4", + "vue": "^3.5.34" + }, + "engines": { + "node": ">=22.12.0" + } + }, + "node_modules/@astrojs/compiler": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@astrojs/compiler/-/compiler-4.0.0.tgz", + "integrity": "sha512-eouss7G8ygdZqHuke033VMcVw5HTZUu+PXd/h06DGDUg/jt5btPYPqh66ENWw/mU78rBrf/oeC4oqoBwMtDMNA==", + "license": "MIT" + }, + "node_modules/@astrojs/internal-helpers": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@astrojs/internal-helpers/-/internal-helpers-0.9.0.tgz", + "integrity": "sha512-GdYkzR26re8izmyYlBqf4z2s7zNngmWLFuxw0UKiPNqHraZGS6GKWIwSHgS22RDlu2ePFJ8bzmpBcUszut/SDg==", + "license": "MIT", + "dependencies": { + "picomatch": "^4.0.4" + } + }, + "node_modules/@astrojs/markdown-remark": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/@astrojs/markdown-remark/-/markdown-remark-7.1.1.tgz", + "integrity": "sha512-C6e9BnLGlbdv6bV8MYGeHpHxsUHrCrB4OuRLqi5LI7oiBVcBcqfUN06zpwFQdHgV48QCCrMmLpyqBr7VqC+swA==", + "license": "MIT", + "dependencies": { + "@astrojs/internal-helpers": "0.9.0", + "@astrojs/prism": "4.0.1", + "github-slugger": "^2.0.0", + "hast-util-from-html": "^2.0.3", + "hast-util-to-text": "^4.0.2", + "js-yaml": "^4.1.1", + "mdast-util-definitions": "^6.0.0", + "rehype-raw": "^7.0.0", + "rehype-stringify": "^10.0.1", + "remark-gfm": "^4.0.1", + "remark-parse": "^11.0.0", + "remark-rehype": "^11.1.2", + "remark-smartypants": "^3.0.2", + "retext-smartypants": "^6.2.0", + "shiki": "^4.0.0", + "smol-toml": "^1.6.0", + "unified": "^11.0.5", + "unist-util-remove-position": "^5.0.0", + "unist-util-visit": "^5.1.0", + "unist-util-visit-parents": "^6.0.2", + "vfile": "^6.0.3" + } + }, + "node_modules/@astrojs/prism": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@astrojs/prism/-/prism-4.0.1.tgz", + "integrity": "sha512-nksZQVjlferuWzhPsBpQ1JE5XuKAf1id1/9Hj4a9KG4+ofrlzxUUwX4YGQF/SuDiuiGKEnzopGOt38F3AnVWsQ==", + "license": "MIT", + "dependencies": { + "prismjs": "^1.30.0" + }, + "engines": { + "node": ">=22.12.0" + } + }, + "node_modules/@astrojs/telemetry": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/@astrojs/telemetry/-/telemetry-3.3.2.tgz", + "integrity": "sha512-j8DNruA8ors99Al39RYZPJK4DC1bKkoNm93mAMuBhY9TCNC4R8n1q7ovFnJ5qhGh5Lsh7pa1gpQVpYpsJPeTHQ==", + "license": "MIT", + "dependencies": { + "ci-info": "^4.4.0", + "dset": "^3.1.4", + "is-docker": "^4.0.0", + "is-wsl": "^3.1.1", + "which-pm-runs": "^1.1.0" + }, + "engines": { + "node": "18.20.8 || ^20.3.0 || >=22.0.0" + } + }, + "node_modules/@astrojs/vue": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@astrojs/vue/-/vue-6.0.1.tgz", + "integrity": "sha512-YeVDmcGkzjhc/LToHXPwxsrH5OaHqOaMDColLvKbGvzn/Y/vsQnNh8pyZZGe76SCKrbMDH6cq8kaVn8upITg7A==", + "license": "MIT", + "dependencies": { + "@vitejs/plugin-vue": "^6.0.4", + "@vitejs/plugin-vue-jsx": "^5.1.4", + "@vue/compiler-sfc": "^3.5.29", + "vite": "^7.3.1", + "vite-plugin-vue-devtools": "^8.0.6" + }, + "engines": { + "node": "18.20.8 || ^20.3.0 || >=22.0.0" + }, + "peerDependencies": { + "astro": "^6.0.0", + "vue": "^3.5.24" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.3", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.3.tgz", + "integrity": "sha512-LIVqM46zQWZhj17qA8wb4nW/ixr2y1Nw+r1etiAWgRM6U1IqP+LNhL1yg440jYZR72jCWcWbLWzIosH+uP1fqg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-annotate-as-pure": { + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz", + "integrity": "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.27.3" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-create-class-features-plugin": { + "version": "7.29.3", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.29.3.tgz", + "integrity": "sha512-RpLYy2sb51oNLjuu1iD3bwBqCBWUzjO0ocp+iaCP/lJtb2CPLcnC2Fftw+4sAzaMELGeWTgExSKADbdo0GFVzA==", + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.3", + "@babel/helper-member-expression-to-functions": "^7.28.5", + "@babel/helper-optimise-call-expression": "^7.27.1", + "@babel/helper-replace-supers": "^7.28.6", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", + "@babel/traverse": "^7.29.0", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-create-class-features-plugin/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-member-expression-to-functions": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.28.5.tgz", + "integrity": "sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg==", + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.5", + "@babel/types": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-optimise-call-expression": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.27.1.tgz", + "integrity": "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-replace-supers": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.28.6.tgz", + "integrity": "sha512-mq8e+laIk94/yFec3DxSjCRD2Z0TAjhVbEJY3UQrlwVo15Lmt7C2wAUbK4bjnTs4APkwsYLTahXRraQXhb1WCg==", + "license": "MIT", + "dependencies": { + "@babel/helper-member-expression-to-functions": "^7.28.5", + "@babel/helper-optimise-call-expression": "^7.27.1", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.27.1.tgz", + "integrity": "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg==", + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz", + "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==", + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.3", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.3.tgz", + "integrity": "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-proposal-decorators": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.29.0.tgz", + "integrity": "sha512-CVBVv3VY/XRMxRYq5dwr2DS7/MvqPm23cOCjbwNnVrfOqcWlnefua1uUs0sjdKOGjvPUG633o07uWzJq4oI6dA==", + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/plugin-syntax-decorators": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-decorators": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-7.28.6.tgz", + "integrity": "sha512-71EYI0ONURHJBL4rSFXnITXqXrrY8q4P0q006DPfN+Rk+ASM+++IBXem/ruokgBZR8YNEWZ8R6B+rCb8VcUTqA==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.28.6.tgz", + "integrity": "sha512-jiLC0ma9XkQT3TKJ9uYvlakm66Pamywo+qwL+oL8HJOvc6TWdZXVfhqJr8CCzbSGUAbDOzlGHJC1U+vRfLQDvw==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.28.6.tgz", + "integrity": "sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.28.6.tgz", + "integrity": "sha512-+nDNmQye7nlnuuHDboPbGm00Vqg3oO8niRRL27/4LYHUsHYh0zJ1xWOz0uRwNFmM1Avzk8wZbc6rdiYhomzv/A==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-typescript": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.28.6.tgz", + "integrity": "sha512-0YWL2RFxOqEm9Efk5PvreamxPME8OyY0wM5wh5lHjF+VtVhdneCWGzZeSqzOfiobVqQaNCd2z0tQvnI9DaPWPw==", + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.3", + "@babel/helper-create-class-features-plugin": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", + "@babel/plugin-syntax-typescript": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@capsizecss/unpack": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@capsizecss/unpack/-/unpack-4.0.0.tgz", + "integrity": "sha512-VERIM64vtTP1C4mxQ5thVT9fK0apjPFobqybMtA1UdUujWka24ERHbRHFGmpbbhp73MhV+KSsHQH9C6uOTdEQA==", + "license": "MIT", + "dependencies": { + "fontkitten": "^1.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@clack/core": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@clack/core/-/core-1.3.0.tgz", + "integrity": "sha512-xJPHpAmEQUBrXSLx0gF+q5K/IyihXpsHZcha+jB+tyahsKRK3Dxo4D0coZDewHo12NhiuzC3dTtMPbm53GEAAA==", + "license": "MIT", + "dependencies": { + "fast-wrap-ansi": "^0.2.0", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 20.12.0" + } + }, + "node_modules/@clack/prompts": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@clack/prompts/-/prompts-1.3.0.tgz", + "integrity": "sha512-GgcWwRCs/xPtaqlMy8qRhPnZf9vlWcWZNHAitnVQ3yk7JmSralSiq5q07yaffYE8SogtDm7zFeKccx1QNVARpw==", + "license": "MIT", + "dependencies": { + "@clack/core": "1.3.0", + "fast-string-width": "^3.0.2", + "fast-wrap-ansi": "^0.2.0", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 20.12.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", + "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", + "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz", + "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", + "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz", + "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", + "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", + "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", + "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", + "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", + "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", + "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", + "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", + "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", + "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", + "cpu": [ + "mips64el" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", + "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", + "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", + "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", + "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", + "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", + "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", + "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", + "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", + "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", + "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", + "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", + "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", + "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/colour": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", + "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", + "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", + "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", + "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", + "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", + "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", + "cpu": [ + "arm" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", + "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", + "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", + "cpu": [ + "ppc64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", + "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", + "cpu": [ + "riscv64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", + "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", + "cpu": [ + "s390x" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", + "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", + "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", + "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", + "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", + "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", + "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", + "cpu": [ + "ppc64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", + "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", + "cpu": [ + "riscv64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", + "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", + "cpu": [ + "s390x" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", + "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", + "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", + "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", + "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.7.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", + "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", + "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", + "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@oslojs/encoding": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@oslojs/encoding/-/encoding-1.1.0.tgz", + "integrity": "sha512-70wQhgYmndg4GCPxPPxPGevRKqTIJ2Nh4OkiMWmDAVYsTQ+Ta7Sq+rPevXyXGdzr30/qZBnyOalCszoMxlyldQ==", + "license": "MIT" + }, + "node_modules/@polka/url": { + "version": "1.0.0-next.29", + "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz", + "integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==", + "license": "MIT" + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.13", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.13.tgz", + "integrity": "sha512-3ngTAv6F/Py35BsYbeeLeecvhMKdsKm4AoOETVhAA+Qc8nrA2I0kF7oa93mE9qnIurngOSpMnQ0x2nQY2FPviA==", + "license": "MIT" + }, + "node_modules/@rollup/pluginutils": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.3.0.tgz", + "integrity": "sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.3.tgz", + "integrity": "sha512-x35CNW/ANXG3hE/EZpRU8MXX1JDN86hBb2wMGAtltkz7pc6cxgjpy1OMMfDosOQ+2hWqIkag/fGok1Yady9nGw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.3.tgz", + "integrity": "sha512-xw3xtkDApIOGayehp2+Rz4zimfkaX65r4t47iy+ymQB2G4iJCBBfj0ogVg5jpvjpn8UWn/+q9tprxleYeNp3Hw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.3.tgz", + "integrity": "sha512-vo6Y5Qfpx7/5EaamIwi0WqW2+zfiusVihKatLvtN1VFVy3D13uERk/6gZLU1UiHRL6fDXqj/ELIeVRGnvcTE1g==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.3.tgz", + "integrity": "sha512-D+0QGcZhBzTN82weOnsSlY7V7+RMmPuF1CkbxyMAGE8+ZHeUjyb76ZiWmBlCu//AQQONvxcqRbwZTajZKqjuOw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.3.tgz", + "integrity": "sha512-6HnvHCT7fDyj6R0Ph7A6x8dQS/S38MClRWeDLqc0MdfWkxjiu1HSDYrdPhqSILzjTIC/pnXbbJbo+ft+gy/9hQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.3.tgz", + "integrity": "sha512-KHLgC3WKlUYW3ShFKnnosZDOJ0xjg9zp7au3sIm2bs/tGBeC2ipmvRh/N7JKi0t9Ue20C0dpEshi8WUubg+cnA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.3.tgz", + "integrity": "sha512-DV6fJoxEYWJOvaZIsok7KrYl0tPvga5OZ2yvKHNNYyk/2roMLqQAbGhr78EQ5YhHpnhLKJD3S1WFusAkmUuV5g==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.3.tgz", + "integrity": "sha512-mQKoJAzvuOs6F+TZybQO4GOTSMUu7v0WdxEk24krQ/uUxXoPTtHjuaUuPmFhtBcM4K0ons8nrE3JyhTuCFtT/w==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.3.tgz", + "integrity": "sha512-Whjj2qoiJ6+OOJMGptTYazaJvjOJm+iKHpXQM1P3LzGjt7Ff++Tp7nH4N8J/BUA7R9IHfDyx4DJIflifwnbmIA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.3.tgz", + "integrity": "sha512-4YTNHKqGng5+yiZt3mg77nmyuCfmNfX4fPmyUapBcIk+BdwSwmCWGXOUxhXbBEkFHtoN5boLj/5NON+u5QC9tg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.3.tgz", + "integrity": "sha512-SU3kNlhkpI4UqlUc2VXPGK9o886ZsSeGfMAX2ba2b8DKmMXq4AL7KUrkSWVbb7koVqx41Yczx6dx5PNargIrEA==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.3.tgz", + "integrity": "sha512-6lDLl5h4TXpB1mTf2rQWnAk/LcXrx9vBfu/DT5TIPhvMhRWaZ5MxkIc8u4lJAmBo6klTe1ywXIUHFjylW505sg==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.3.tgz", + "integrity": "sha512-BMo8bOw8evlup/8G+cj5xWtPyp93xPdyoSN16Zy90Q2QZ0ZYRhCt6ZJSwbrRzG9HApFabjwj2p25TUPDWrhzqQ==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.3.tgz", + "integrity": "sha512-E0L8X1dZN1/Rph+5VPF6Xj2G7JJvMACVXtamTJIDrVI44Y3K+G8gQaMEAavbqCGTa16InptiVrX6eM6pmJ+7qA==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.3.tgz", + "integrity": "sha512-oZJ/WHaVfHUiRAtmTAeo3DcevNsVvH8mbvodjZy7D5QKvCefO371SiKRpxoDcCxB3PTRTLayWBkvmDQKTcX/sw==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.3.tgz", + "integrity": "sha512-Dhbyh7j9FybM3YaTgaHmVALwA8AkUwTPccyCQ79TG9AJUsMQqgN1DDEZNr4+QUfwiWvLDumW5vdwzoeUF+TNxQ==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.3.tgz", + "integrity": "sha512-cJd1X5XhHHlltkaypz1UcWLA8AcoIi1aWhsvaWDskD1oz2eKCypnqvTQ8ykMNI0RSmm7NkTdSqSSD7zM0xa6Ig==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.3.tgz", + "integrity": "sha512-DAZDBHQfG2oQuhY7mc6I3/qB4LU2fQCjRvxbDwd/Jdvb9fypP4IJ4qmtu6lNjes6B531AI8cg1aKC2di97bUxA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.3.tgz", + "integrity": "sha512-cRxsE8c13mZOh3vP+wLDxpQBRrOHDIGOWyDL93Sy0Ga8y515fBcC2pjUfFwUe5T7tqvTvWbCpg1URM/AXdWIXA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.3.tgz", + "integrity": "sha512-QaWcIgRxqEdQdhJqW4DJctsH6HCmo5vHxY0krHSX4jMtOqfzC+dqDGuHM87bu4H8JBeibWx7jFz+h6/4C8wA5Q==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.3.tgz", + "integrity": "sha512-AaXwSvUi3QIPtroAUw1t5yHGIyqKEXwH54WUocFolZhpGDruJcs8c+xPNDRn4XiQsS7MEwnYsHW2l0MBLDMkWg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.3.tgz", + "integrity": "sha512-65LAKM/bAWDqKNEelHlcHvm2V+Vfb8C6INFxQXRHCvaVN1rJfwr4NvdP4FyzUaLqWfaCGaadf6UbTm8xJeYfEg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.3.tgz", + "integrity": "sha512-EEM2gyhBF5MFnI6vMKdX1LAosE627RGBzIoGMdLloPZkXrUN0Ckqgr2Qi8+J3zip/8NVVro3/FjB+tjhZUgUHA==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.3.tgz", + "integrity": "sha512-E5Eb5H/DpxaoXH++Qkv28RcUJboMopmdDUALBczvHMf7hNIxaDZqwY5lK12UK1BHacSmvupoEWGu+n993Z0y1A==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.3.tgz", + "integrity": "sha512-hPt/bgL5cE+Qp+/TPHBqptcAgPzgj46mPcg/16zNUmbQk0j+mOEQV/+Lqu8QRtDV3Ek95Q6FeFITpuhl6OTsAA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@shikijs/core": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-4.0.2.tgz", + "integrity": "sha512-hxT0YF4ExEqB8G/qFdtJvpmHXBYJ2lWW7qTHDarVkIudPFE6iCIrqdgWxGn5s+ppkGXI0aEGlibI0PAyzP3zlw==", + "license": "MIT", + "dependencies": { + "@shikijs/primitive": "4.0.2", + "@shikijs/types": "4.0.2", + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4", + "hast-util-to-html": "^9.0.5" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@shikijs/engine-javascript": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-4.0.2.tgz", + "integrity": "sha512-7PW0Nm49DcoUIQEXlJhNNBHyoGMjalRETTCcjMqEaMoJRLljy1Bi/EGV3/qLBgLKQejdspiiYuHGQW6dX94Nag==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "4.0.2", + "@shikijs/vscode-textmate": "^10.0.2", + "oniguruma-to-es": "^4.3.4" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@shikijs/engine-oniguruma": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-4.0.2.tgz", + "integrity": "sha512-UpCB9Y2sUKlS9z8juFSKz7ZtysmeXCgnRF0dlhXBkmQnek7lAToPte8DkxmEYGNTMii72zU/lyXiCB6StuZeJg==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "4.0.2", + "@shikijs/vscode-textmate": "^10.0.2" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@shikijs/langs": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-4.0.2.tgz", + "integrity": "sha512-KaXby5dvoeuZzN0rYQiPMjFoUrz4hgwIE+D6Du9owcHcl6/g16/yT5BQxSW5cGt2MZBz6Hl0YuRqf12omRfUUg==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "4.0.2" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@shikijs/primitive": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@shikijs/primitive/-/primitive-4.0.2.tgz", + "integrity": "sha512-M6UMPrSa3fN5ayeJwFVl9qWofl273wtK1VG8ySDZ1mQBfhCpdd8nEx7nPZ/tk7k+TYcpqBZzj/AnwxT9lO+HJw==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "4.0.2", + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@shikijs/themes": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-4.0.2.tgz", + "integrity": "sha512-mjCafwt8lJJaVSsQvNVrJumbnnj1RI8jbUKrPKgE6E3OvQKxnuRoBaYC51H4IGHePsGN/QtALglWBU7DoKDFnA==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "4.0.2" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@shikijs/types": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-4.0.2.tgz", + "integrity": "sha512-qzbeRooUTPnLE+sHD/Z8DStmaDgnbbc/pMrU203950aRqjX/6AFHeDYT+j00y2lPdz0ywJKx7o/7qnqTivtlXg==", + "license": "MIT", + "dependencies": { + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@shikijs/vscode-textmate": { + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/@shikijs/vscode-textmate/-/vscode-textmate-10.0.2.tgz", + "integrity": "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==", + "license": "MIT" + }, + "node_modules/@tailwindcss/node": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.2.4.tgz", + "integrity": "sha512-Ai7+yQPxz3ddrDQzFfBKdHEVBg0w3Zl83jnjuwxnZOsnH9pGn93QHQtpU0p/8rYWxvbFZHneni6p1BSLK4DkGA==", + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "enhanced-resolve": "^5.19.0", + "jiti": "^2.6.1", + "lightningcss": "1.32.0", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.2.4" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.2.4.tgz", + "integrity": "sha512-9El/iI069DKDSXwTvB9J4BwdO5JhRrOweGaK25taBAvBXyXqJAX+Jqdvs8r8gKpsI/1m0LeJLyQYTf/WLrBT1Q==", + "license": "MIT", + "engines": { + "node": ">= 20" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.2.4", + "@tailwindcss/oxide-darwin-arm64": "4.2.4", + "@tailwindcss/oxide-darwin-x64": "4.2.4", + "@tailwindcss/oxide-freebsd-x64": "4.2.4", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.4", + "@tailwindcss/oxide-linux-arm64-gnu": "4.2.4", + "@tailwindcss/oxide-linux-arm64-musl": "4.2.4", + "@tailwindcss/oxide-linux-x64-gnu": "4.2.4", + "@tailwindcss/oxide-linux-x64-musl": "4.2.4", + "@tailwindcss/oxide-wasm32-wasi": "4.2.4", + "@tailwindcss/oxide-win32-arm64-msvc": "4.2.4", + "@tailwindcss/oxide-win32-x64-msvc": "4.2.4" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.2.4.tgz", + "integrity": "sha512-e7MOr1SAn9U8KlZzPi1ZXGZHeC5anY36qjNwmZv9pOJ8E4Q6jmD1vyEHkQFmNOIN7twGPEMXRHmitN4zCMN03g==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.2.4.tgz", + "integrity": "sha512-tSC/Kbqpz/5/o/C2sG7QvOxAKqyd10bq+ypZNf+9Fi2TvbVbv1zNpcEptcsU7DPROaSbVgUXmrzKhurFvo5eDg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.2.4.tgz", + "integrity": "sha512-yPyUXn3yO/ufR6+Kzv0t4fCg2qNr90jxXc5QqBpjlPNd0NqyDXcmQb/6weunH/MEDXW5dhyEi+agTDiqa3WsGg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.2.4.tgz", + "integrity": "sha512-BoMIB4vMQtZsXdGLVc2z+P9DbETkiopogfWZKbWwM8b/1Vinbs4YcUwo+kM/KeLkX3Ygrf4/PsRndKaYhS8Eiw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.2.4.tgz", + "integrity": "sha512-7pIHBLTHYRAlS7V22JNuTh33yLH4VElwKtB3bwchK/UaKUPpQ0lPQiOWcbm4V3WP2I6fNIJ23vABIvoy2izdwA==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.2.4.tgz", + "integrity": "sha512-+E4wxJ0ZGOzSH325reXTWB48l42i93kQqMvDyz5gqfRzRZ7faNhnmvlV4EPGJU3QJM/3Ab5jhJ5pCRUsKn6OQw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.2.4.tgz", + "integrity": "sha512-bBADEGAbo4ASnppIziaQJelekCxdMaxisrk+fB7Thit72IBnALp9K6ffA2G4ruj90G9XRS2VQ6q2bCKbfFV82g==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.2.4.tgz", + "integrity": "sha512-7Mx25E4WTfnht0TVRTyC00j3i0M+EeFe7wguMDTlX4mRxafznw0CA8WJkFjWYH5BlgELd1kSjuU2JiPnNZbJDA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.2.4.tgz", + "integrity": "sha512-2wwJRF7nyhOR0hhHoChc04xngV3iS+akccHTGtz965FwF0up4b2lOdo6kI1EbDaEXKgvcrFBYcYQQ/rrnWFVfA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.2.4.tgz", + "integrity": "sha512-FQsqApeor8Fo6gUEklzmaa9994orJZZDBAlQpK2Mq+DslRKFJeD6AjHpBQ0kZFQohVr8o85PPh8eOy86VlSCmw==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.8.1", + "@emnapi/runtime": "^1.8.1", + "@emnapi/wasi-threads": "^1.1.0", + "@napi-rs/wasm-runtime": "^1.1.1", + "@tybys/wasm-util": "^0.10.1", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.2.4.tgz", + "integrity": "sha512-L9BXqxC4ToVgwMFqj3pmZRqyHEztulpUJzCxUtLjobMCzTPsGt1Fa9enKbOpY2iIyVtaHNeNvAK8ERP/64sqGQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.2.4.tgz", + "integrity": "sha512-ESlKG0EpVJQwRjXDDa9rLvhEAh0mhP1sF7sap9dNZT0yyl9SAG6T7gdP09EH0vIv0UNTlo6jPWyujD6559fZvw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/vite": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.2.4.tgz", + "integrity": "sha512-pCvohwOCspk3ZFn6eJzrrX3g4n2JY73H6MmYC87XfGPyTty4YsCjYTMArRZm/zOI8dIt3+EcrLHAFPe5A4bgtw==", + "license": "MIT", + "dependencies": { + "@tailwindcss/node": "4.2.4", + "@tailwindcss/oxide": "4.2.4", + "tailwindcss": "4.2.4" + }, + "peerDependencies": { + "vite": "^5.2.0 || ^6 || ^7 || ^8" + } + }, + "node_modules/@types/debug": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz", + "integrity": "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==", + "license": "MIT", + "dependencies": { + "@types/ms": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "license": "MIT" + }, + "node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "license": "MIT" + }, + "node_modules/@types/nlcst": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/nlcst/-/nlcst-2.0.3.tgz", + "integrity": "sha512-vSYNSDe6Ix3q+6Z7ri9lyWqgGhJTmzRjZRqyq15N0Z/1/UnVsno9G/N40NBijoYx2seFDIl0+B2mgAb9mezUCA==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "license": "MIT" + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.1.tgz", + "integrity": "sha512-mUFwbeTqrVgDQxFveS+df2yfap6iuP20NAKAsBt5jDEoOTDew+zwLAOilHCeQJOVSvmgCX4ogqIrA0mnyr08yQ==", + "license": "ISC" + }, + "node_modules/@vitejs/plugin-vue": { + "version": "6.0.6", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-6.0.6.tgz", + "integrity": "sha512-u9HHgfrq3AjXlysn0eINFnWQOJQLO9WN6VprZ8FXl7A2bYisv3Hui9Ij+7QZ41F/WYWarHjwBbXtD7dKg3uxbg==", + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "1.0.0-rc.13" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0", + "vue": "^3.2.25" + } + }, + "node_modules/@vitejs/plugin-vue-jsx": { + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue-jsx/-/plugin-vue-jsx-5.1.5.tgz", + "integrity": "sha512-jIAsvHOEtWpslLOI2MeElGFxH7M8pM83BU/Tor4RLyiwH0FM4nUW3xdvbw20EeU9wc5IspQwMq225K3CMnJEpA==", + "license": "MIT", + "dependencies": { + "@babel/core": "^7.29.0", + "@babel/plugin-syntax-typescript": "^7.28.6", + "@babel/plugin-transform-typescript": "^7.28.6", + "@rolldown/pluginutils": "^1.0.0-rc.2", + "@vue/babel-plugin-jsx": "^2.0.1" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0", + "vue": "^3.0.0" + } + }, + "node_modules/@vue/babel-helper-vue-transform-on": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@vue/babel-helper-vue-transform-on/-/babel-helper-vue-transform-on-2.0.1.tgz", + "integrity": "sha512-uZ66EaFbnnZSYqYEyplWvn46GhZ1KuYSThdT68p+am7MgBNbQ3hphTL9L+xSIsWkdktwhPYLwPgVWqo96jDdRA==", + "license": "MIT" + }, + "node_modules/@vue/babel-plugin-jsx": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@vue/babel-plugin-jsx/-/babel-plugin-jsx-2.0.1.tgz", + "integrity": "sha512-a8CaLQjD/s4PVdhrLD/zT574ZNPnZBOY+IhdtKWRB4HRZ0I2tXBi5ne7d9eCfaYwp5gU5+4KIyFTV1W1YL9xZA==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/plugin-syntax-jsx": "^7.27.1", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.4", + "@babel/types": "^7.28.4", + "@vue/babel-helper-vue-transform-on": "2.0.1", + "@vue/babel-plugin-resolve-type": "2.0.1", + "@vue/shared": "^3.5.22" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + } + } + }, + "node_modules/@vue/babel-plugin-resolve-type": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@vue/babel-plugin-resolve-type/-/babel-plugin-resolve-type-2.0.1.tgz", + "integrity": "sha512-ybwgIuRGRRBhOU37GImDoWQoz+TlSqap65qVI6iwg/J7FfLTLmMf97TS7xQH9I7Qtr/gp161kYVdhr1ZMraSYQ==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/parser": "^7.28.4", + "@vue/compiler-sfc": "^3.5.22" + }, + "funding": { + "url": "https://github.com/sponsors/sxzz" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.5.34", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.34.tgz", + "integrity": "sha512-s9cLyK5mLcvZ4Agva5QgRsQyLKvts9WbU9DB6NqiZkkGEdwmcEiylj5Jbwkp680drF/NNCV8OlAJSe+yMLxaJw==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.3", + "@vue/shared": "3.5.34", + "entities": "^7.0.1", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-core/node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.5.34", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.34.tgz", + "integrity": "sha512-EbF/T++k0e2MMZlJsBhzK8Sgwt0HcIPOhzn1CTB/lv6sQcyk+OWf8YeiLxZp3ro7MbbLcAfAJ6sEvjFWuNgUCw==", + "license": "MIT", + "dependencies": { + "@vue/compiler-core": "3.5.34", + "@vue/shared": "3.5.34" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.5.34", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.34.tgz", + "integrity": "sha512-D/ihr6uZeIt6r+pVZf46RWT1fAsLFMbUP7k8G1VkiiWexriED9GrX3echHd4Abbt17zjlfiFJ8z7a3BxZOPNjg==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.3", + "@vue/compiler-core": "3.5.34", + "@vue/compiler-dom": "3.5.34", + "@vue/compiler-ssr": "3.5.34", + "@vue/shared": "3.5.34", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.21", + "postcss": "^8.5.14", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.5.34", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.34.tgz", + "integrity": "sha512-cDtTHKibkThKGHH1SP+WdccquNRYQDFH6rRjQCqT9G2ltFAfoR5pUftpab/z+aM5mW9HLLVQW7hfKKQe/1GBeQ==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.34", + "@vue/shared": "3.5.34" + } + }, + "node_modules/@vue/devtools-core": { + "version": "8.1.2", + "resolved": "https://registry.npmjs.org/@vue/devtools-core/-/devtools-core-8.1.2.tgz", + "integrity": "sha512-ZGGyaSBP4/+bN2Nd9ZHNYAVDRIzMw1rv2RyXWtyZlo6mQal+IDmTvKY4V+DjAEBhaXt30mHmsgYp1yXJ/2tIWg==", + "license": "MIT", + "dependencies": { + "@vue/devtools-kit": "^8.1.2", + "@vue/devtools-shared": "^8.1.2" + }, + "peerDependencies": { + "vue": "^3.0.0" + } + }, + "node_modules/@vue/devtools-kit": { + "version": "8.1.2", + "resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-8.1.2.tgz", + "integrity": "sha512-f75/upc+GCyjXErpgPGz4582ujS0L/adAltGy+tqXMGUJpgAcfGr6CxnnhpZY8BHuMYt6KpbF8uaFrrQG66rGQ==", + "license": "MIT", + "dependencies": { + "@vue/devtools-shared": "^8.1.2", + "birpc": "^2.6.1", + "hookable": "^5.5.3", + "perfect-debounce": "^2.0.0" + } + }, + "node_modules/@vue/devtools-shared": { + "version": "8.1.2", + "resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-8.1.2.tgz", + "integrity": "sha512-X9RyVFYAdkBe4IUf5v48TxBF/6QPmF8CmWrDAjXzfUHrgQ/HGfTC1A6TqgXqZ03ye66l3AD51BAGD69IvKM9sw==", + "license": "MIT" + }, + "node_modules/@vue/reactivity": { + "version": "3.5.34", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.34.tgz", + "integrity": "sha512-y9XDjCEuBp+98k+UL5dbYkh57AHU4o6cxZedOPXw3bmrZZYLQsVHguGurq7hVrPCSrQtrnz1f9dssyFr+dMXfQ==", + "license": "MIT", + "dependencies": { + "@vue/shared": "3.5.34" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.5.34", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.34.tgz", + "integrity": "sha512-mKeBYvu8tcMSLhypAHBmriUFfWXKTCF/23Z4jiCoYK3UtWepkliViNLuR90V9XOyD62mUxs9p1jsrpK3CCGIzw==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.34", + "@vue/shared": "3.5.34" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.5.34", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.34.tgz", + "integrity": "sha512-e8kZzERmCwUnBRVsgSQlAfrfU2rGoy0FFKPBXSlfEjc/O3KfA7QP0t1/2ZylrbchjmIKB4dPTd07A6WPr0eOrg==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.34", + "@vue/runtime-core": "3.5.34", + "@vue/shared": "3.5.34", + "csstype": "^3.2.3" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.5.34", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.34.tgz", + "integrity": "sha512-nHxmJoTrKsmrkbILRhkC9gY1G3moZbJTqCzDd7DOOzG5KH9oeJ0Unqrff5f9v0pW//jES05ZkJcNtfE8JjOIew==", + "license": "MIT", + "dependencies": { + "@vue/compiler-ssr": "3.5.34", + "@vue/shared": "3.5.34" + }, + "peerDependencies": { + "vue": "3.5.34" + } + }, + "node_modules/@vue/shared": { + "version": "3.5.34", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.34.tgz", + "integrity": "sha512-24uqU4OIiX29ryC3MeWid/Xf2fa2EFRUVLb77nRhk+UrTVrh/XiGtFAFmJBAtBRbjwNdsPRP+jj/OL27Eg1NDA==", + "license": "MIT" + }, + "node_modules/ansis": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/ansis/-/ansis-4.2.0.tgz", + "integrity": "sha512-HqZ5rWlFjGiV0tDm3UxxgNRqsOTniqoKZu0pIAfh7TZQMGuZK+hH0drySty0si0QXj1ieop4+SkSfPZBPPkHig==", + "license": "ISC", + "engines": { + "node": ">=14" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/anymatch/node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, + "node_modules/aria-query": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", + "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/array-iterate": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/array-iterate/-/array-iterate-2.0.1.tgz", + "integrity": "sha512-I1jXZMjAgCMmxT4qxXfPXa6SthSoE8h6gkSI9BGGNv8mP8G/v0blc+qFnZu6K42vTOiuME596QaLO0TP3Lk0xg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/astro": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/astro/-/astro-6.3.1.tgz", + "integrity": "sha512-atz6dmkE3Gu24bDgb7g2RE/BYnKqPYIHd6hTUM1UXvu/i7qNZOKLAqEHvgYpv9PQVcgWsXpk4/OOXZ0E/FzvSQ==", + "license": "MIT", + "dependencies": { + "@astrojs/compiler": "^4.0.0", + "@astrojs/internal-helpers": "0.9.0", + "@astrojs/markdown-remark": "7.1.1", + "@astrojs/telemetry": "3.3.2", + "@capsizecss/unpack": "^4.0.0", + "@clack/prompts": "^1.1.0", + "@oslojs/encoding": "^1.1.0", + "@rollup/pluginutils": "^5.3.0", + "aria-query": "^5.3.2", + "axobject-query": "^4.1.0", + "ci-info": "^4.4.0", + "clsx": "^2.1.1", + "common-ancestor-path": "^2.0.0", + "cookie": "^1.1.1", + "devalue": "^5.6.3", + "diff": "^8.0.3", + "dset": "^3.1.4", + "es-module-lexer": "^2.0.0", + "esbuild": "^0.27.3", + "flattie": "^1.1.1", + "fontace": "~0.4.1", + "get-tsconfig": "5.0.0-beta.4", + "github-slugger": "^2.0.0", + "html-escaper": "3.0.3", + "http-cache-semantics": "^4.2.0", + "js-yaml": "^4.1.1", + "jsonc-parser": "^3.3.1", + "magic-string": "^0.30.21", + "magicast": "^0.5.2", + "mrmime": "^2.0.1", + "neotraverse": "^0.6.18", + "obug": "^2.1.1", + "p-limit": "^7.3.0", + "p-queue": "^9.1.0", + "package-manager-detector": "^1.6.0", + "piccolore": "^0.1.3", + "picomatch": "^4.0.4", + "rehype": "^13.0.2", + "semver": "^7.7.4", + "shiki": "^4.0.2", + "smol-toml": "^1.6.0", + "svgo": "^4.0.1", + "tinyclip": "^0.1.12", + "tinyexec": "^1.0.4", + "tinyglobby": "^0.2.15", + "ultrahtml": "^1.6.0", + "unifont": "~0.7.4", + "unist-util-visit": "^5.1.0", + "unstorage": "^1.17.5", + "vfile": "^6.0.3", + "vite": "^7.3.2", + "vitefu": "^1.1.2", + "xxhash-wasm": "^1.1.0", + "yargs-parser": "^22.0.0", + "zod": "^4.3.6" + }, + "bin": { + "astro": "bin/astro.mjs" + }, + "engines": { + "node": ">=22.12.0", + "npm": ">=9.6.5", + "pnpm": ">=7.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/astrodotbuild" + }, + "optionalDependencies": { + "sharp": "^0.34.0" + } + }, + "node_modules/axobject-query": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", + "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/bail": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", + "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.28", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.28.tgz", + "integrity": "sha512-Ic44hnOtFIgravCunj1ifSoQPSUrkNiJuH9Mf6jr2jjoA74icqV8wU0KuadXeOR8zuIJMOoTv0GuQjZ9ZYNMeA==", + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/birpc": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/birpc/-/birpc-2.9.0.tgz", + "integrity": "sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "license": "ISC" + }, + "node_modules/browserslist": { + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.10.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bundle-name": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", + "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==", + "license": "MIT", + "dependencies": { + "run-applescript": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001792", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001792.tgz", + "integrity": "sha512-hVLMUZFgR4JJ6ACt1uEESvQN1/dBVqPAKY0hgrV70eN3391K6juAfTjKZLKvOMsx8PxA7gsY1/tLMMTcfFLLpw==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/ccount": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", + "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz", + "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-html4": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", + "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-legacy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", + "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/chokidar": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", + "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", + "license": "MIT", + "dependencies": { + "readdirp": "^5.0.0" + }, + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/ci-info": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.4.0.tgz", + "integrity": "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/comma-separated-tokens": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", + "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/commander": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz", + "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==", + "license": "MIT", + "engines": { + "node": ">=16" + } + }, + "node_modules/common-ancestor-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/common-ancestor-path/-/common-ancestor-path-2.0.0.tgz", + "integrity": "sha512-dnN3ibLeoRf2HNC+OlCiNc5d2zxbLJXOtiZUudNFSXZrNSydxcCsSpRzXwfu7BBWCIfHPw+xTayeBvJCP/D8Ng==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">= 18" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "license": "MIT" + }, + "node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/cookie-es": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/cookie-es/-/cookie-es-1.2.3.tgz", + "integrity": "sha512-lXVyvUvrNXblMqzIRrxHb57UUVmqsSWlxqt3XIjCkUP0wDAf6uicO6KMbEgYrMNtEvWgWHwe42CKxPu9MYAnWw==", + "license": "MIT" + }, + "node_modules/crossws": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/crossws/-/crossws-0.3.5.tgz", + "integrity": "sha512-ojKiDvcmByhwa8YYqbQI/hg7MEU0NC03+pSdEq4ZUnZR9xXpwk7E43SMNGkn+JxJGPFtNvQ48+vV2p+P1ml5PA==", + "license": "MIT", + "dependencies": { + "uncrypto": "^0.1.3" + } + }, + "node_modules/css-select": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz", + "integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-tree": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz", + "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==", + "license": "MIT", + "dependencies": { + "mdn-data": "2.27.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, + "node_modules/css-what": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz", + "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/csso": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/csso/-/csso-5.0.5.tgz", + "integrity": "sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==", + "license": "MIT", + "dependencies": { + "css-tree": "~2.2.0" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/csso/node_modules/css-tree": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.2.1.tgz", + "integrity": "sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==", + "license": "MIT", + "dependencies": { + "mdn-data": "2.0.28", + "source-map-js": "^1.0.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/csso/node_modules/mdn-data": { + "version": "2.0.28", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.28.tgz", + "integrity": "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==", + "license": "CC0-1.0" + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decode-named-character-reference": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.3.0.tgz", + "integrity": "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==", + "license": "MIT", + "dependencies": { + "character-entities": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/default-browser": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.5.0.tgz", + "integrity": "sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw==", + "license": "MIT", + "dependencies": { + "bundle-name": "^4.1.0", + "default-browser-id": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-browser-id": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.1.tgz", + "integrity": "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/define-lazy-prop": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", + "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/defu": { + "version": "6.1.7", + "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.7.tgz", + "integrity": "sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==", + "license": "MIT" + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/destr": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/destr/-/destr-2.0.5.tgz", + "integrity": "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==", + "license": "MIT" + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/devalue": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.8.0.tgz", + "integrity": "sha512-2zA9pFEsnp7vWBZbXF5JAgAq0fsUIt/1XPbRiAmRV3lp/2C3upzH+sADiyy66aFCihoLEsrQHxNM5w1gIDfsBg==", + "license": "MIT" + }, + "node_modules/devlop": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", + "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", + "license": "MIT", + "dependencies": { + "dequal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/diff": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/diff/-/diff-8.0.4.tgz", + "integrity": "sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/dom-serializer/node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/dset": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/dset/-/dset-3.1.4.tgz", + "integrity": "sha512-2QF/g9/zTaPDc3BjNcVTGoBbXBgYfMTTceLaYcFJ/W9kggFUkhxD/hMEeuLKbugyef9SqAx8cpgwlIP/jinUTA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.352", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.352.tgz", + "integrity": "sha512-9wHk8x6dyuimoe18EdiDPWKExNdxYqo4fn4FwOVVper6RxT3cmpBwBkWWfSOCYJjQdIco/nPhJhNLmn4Ufg1Yg==", + "license": "ISC" + }, + "node_modules/embla-carousel": { + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/embla-carousel/-/embla-carousel-8.6.0.tgz", + "integrity": "sha512-SjWyZBHJPbqxHOzckOfo8lHisEaJWmwd23XppYFYVh10bU66/Pn5tkVkbkCMZVdbUE5eTCI2nD8OyIP4Z+uwkA==", + "license": "MIT" + }, + "node_modules/embla-carousel-reactive-utils": { + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/embla-carousel-reactive-utils/-/embla-carousel-reactive-utils-8.6.0.tgz", + "integrity": "sha512-fMVUDUEx0/uIEDM0Mz3dHznDhfX+znCCDCeIophYb1QGVM7YThSWX+wz11zlYwWFOr74b4QLGg0hrGPJeG2s4A==", + "license": "MIT", + "peerDependencies": { + "embla-carousel": "8.6.0" + } + }, + "node_modules/embla-carousel-vue": { + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/embla-carousel-vue/-/embla-carousel-vue-8.6.0.tgz", + "integrity": "sha512-v8UO5UsyLocZnu/LbfQA7Dn2QHuZKurJY93VUmZYP//QRWoCWOsionmvLLAlibkET3pGPs7++03VhJKbWD7vhQ==", + "license": "MIT", + "dependencies": { + "embla-carousel": "8.6.0", + "embla-carousel-reactive-utils": "8.6.0" + }, + "peerDependencies": { + "vue": "^3.2.37" + } + }, + "node_modules/enhanced-resolve": { + "version": "5.21.2", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.21.2.tgz", + "integrity": "sha512-xe9vQb5kReirPUxgQrXA3ihgbCqssmTiM7cOZ+Gzu+VeGWgpV98lLZvp0dl4yriyAePcewxGUs9UpKD8PET9KQ==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/error-stack-parser-es": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/error-stack-parser-es/-/error-stack-parser-es-1.0.5.tgz", + "integrity": "sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/es-module-lexer": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.1.0.tgz", + "integrity": "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==", + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", + "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.7", + "@esbuild/android-arm": "0.27.7", + "@esbuild/android-arm64": "0.27.7", + "@esbuild/android-x64": "0.27.7", + "@esbuild/darwin-arm64": "0.27.7", + "@esbuild/darwin-x64": "0.27.7", + "@esbuild/freebsd-arm64": "0.27.7", + "@esbuild/freebsd-x64": "0.27.7", + "@esbuild/linux-arm": "0.27.7", + "@esbuild/linux-arm64": "0.27.7", + "@esbuild/linux-ia32": "0.27.7", + "@esbuild/linux-loong64": "0.27.7", + "@esbuild/linux-mips64el": "0.27.7", + "@esbuild/linux-ppc64": "0.27.7", + "@esbuild/linux-riscv64": "0.27.7", + "@esbuild/linux-s390x": "0.27.7", + "@esbuild/linux-x64": "0.27.7", + "@esbuild/netbsd-arm64": "0.27.7", + "@esbuild/netbsd-x64": "0.27.7", + "@esbuild/openbsd-arm64": "0.27.7", + "@esbuild/openbsd-x64": "0.27.7", + "@esbuild/openharmony-arm64": "0.27.7", + "@esbuild/sunos-x64": "0.27.7", + "@esbuild/win32-arm64": "0.27.7", + "@esbuild/win32-ia32": "0.27.7", + "@esbuild/win32-x64": "0.27.7" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "license": "MIT" + }, + "node_modules/eventemitter3": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", + "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", + "license": "MIT" + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT" + }, + "node_modules/fast-string-truncated-width": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/fast-string-truncated-width/-/fast-string-truncated-width-3.0.3.tgz", + "integrity": "sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g==", + "license": "MIT" + }, + "node_modules/fast-string-width": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/fast-string-width/-/fast-string-width-3.0.2.tgz", + "integrity": "sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg==", + "license": "MIT", + "dependencies": { + "fast-string-truncated-width": "^3.0.2" + } + }, + "node_modules/fast-wrap-ansi": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/fast-wrap-ansi/-/fast-wrap-ansi-0.2.0.tgz", + "integrity": "sha512-rLV8JHxTyhVmFYhBJuMujcrHqOT2cnO5Zxj37qROj23CP39GXubJRBUFF0z8KFK77Uc0SukZUf7JZhsVEQ6n8w==", + "license": "MIT", + "dependencies": { + "fast-string-width": "^3.0.2" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/flattie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/flattie/-/flattie-1.1.1.tgz", + "integrity": "sha512-9UbaD6XdAL97+k/n+N7JwX46K/M6Zc6KcFYskrYL8wbBV/Uyk0CTAMY0VT+qiK5PM7AIc9aTWYtq65U7T+aCNQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/fontace": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/fontace/-/fontace-0.4.1.tgz", + "integrity": "sha512-lDMvbAzSnHmbYMTEld5qdtvNH2/pWpICOqpean9IgC7vUbUJc3k+k5Dokp85CegamqQpFbXf0rAVkbzpyTA8aw==", + "license": "MIT", + "dependencies": { + "fontkitten": "^1.0.2" + } + }, + "node_modules/fontkitten": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/fontkitten/-/fontkitten-1.0.3.tgz", + "integrity": "sha512-Wp1zXWPVUPBmfoa3Cqc9ctaKuzKAV6uLstRqlR56kSjplf5uAce+qeyYym7F+PHbGTk+tCEdkCW6RD7DX/gBZw==", + "license": "MIT", + "dependencies": { + "tiny-inflate": "^1.0.3" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-tsconfig": { + "version": "5.0.0-beta.4", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-5.0.0-beta.4.tgz", + "integrity": "sha512-7nF7C9fIPFEMHgEMEfgIlO9wDdZ8CyHw27rWciFZfHvHDReIiPhsYuzPRXsfvBCqFy1l8RRyyWV7QLM+ZhUJsQ==", + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "engines": { + "node": ">=20.20.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/github-slugger": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/github-slugger/-/github-slugger-2.0.0.tgz", + "integrity": "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw==", + "license": "ISC" + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "node_modules/h3": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/h3/-/h3-1.15.11.tgz", + "integrity": "sha512-L3THSe2MPeBwgIZVSH5zLdBBU90TOxarvhK9d04IDY2AmVS8j2Jz2LIWtwsGOU3lu2I5jCN7FNvVfY2+XyF+mg==", + "license": "MIT", + "dependencies": { + "cookie-es": "^1.2.3", + "crossws": "^0.3.5", + "defu": "^6.1.6", + "destr": "^2.0.5", + "iron-webcrypto": "^1.2.1", + "node-mock-http": "^1.0.4", + "radix3": "^1.1.2", + "ufo": "^1.6.3", + "uncrypto": "^0.1.3" + } + }, + "node_modules/hast-util-from-html": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/hast-util-from-html/-/hast-util-from-html-2.0.3.tgz", + "integrity": "sha512-CUSRHXyKjzHov8yKsQjGOElXy/3EKpyX56ELnkHH34vDVw1N1XSQ1ZcAvTyAPtGqLTuKP/uxM+aLkSPqF/EtMw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "devlop": "^1.1.0", + "hast-util-from-parse5": "^8.0.0", + "parse5": "^7.0.0", + "vfile": "^6.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-from-parse5": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/hast-util-from-parse5/-/hast-util-from-parse5-8.0.3.tgz", + "integrity": "sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "devlop": "^1.0.0", + "hastscript": "^9.0.0", + "property-information": "^7.0.0", + "vfile": "^6.0.0", + "vfile-location": "^5.0.0", + "web-namespaces": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-is-element": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-is-element/-/hast-util-is-element-3.0.0.tgz", + "integrity": "sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-parse-selector": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-4.0.0.tgz", + "integrity": "sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-raw": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/hast-util-raw/-/hast-util-raw-9.1.0.tgz", + "integrity": "sha512-Y8/SBAHkZGoNkpzqqfCldijcuUKh7/su31kEBp67cFY09Wy0mTRgtsLYsiIxMJxlu0f6AA5SUTbDR8K0rxnbUw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "@ungap/structured-clone": "^1.0.0", + "hast-util-from-parse5": "^8.0.0", + "hast-util-to-parse5": "^8.0.0", + "html-void-elements": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "parse5": "^7.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0", + "web-namespaces": "^2.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-html": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-9.0.5.tgz", + "integrity": "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-whitespace": "^3.0.0", + "html-void-elements": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "stringify-entities": "^4.0.0", + "zwitch": "^2.0.4" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-parse5": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/hast-util-to-parse5/-/hast-util-to-parse5-8.0.1.tgz", + "integrity": "sha512-MlWT6Pjt4CG9lFCjiz4BH7l9wmrMkfkJYCxFwKQic8+RTZgWPuWxwAfjJElsXkex7DJjfSJsQIt931ilUgmwdA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "devlop": "^1.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "web-namespaces": "^2.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-text": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/hast-util-to-text/-/hast-util-to-text-4.0.2.tgz", + "integrity": "sha512-KK6y/BN8lbaq654j7JgBydev7wuNMcID54lkRav1P0CaE1e47P72AWWPiGKXTJU271ooYzcvTAn/Zt0REnvc7A==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "hast-util-is-element": "^3.0.0", + "unist-util-find-after": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-whitespace": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", + "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hastscript": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-9.0.1.tgz", + "integrity": "sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-parse-selector": "^4.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hookable": { + "version": "5.5.3", + "resolved": "https://registry.npmjs.org/hookable/-/hookable-5.5.3.tgz", + "integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==", + "license": "MIT" + }, + "node_modules/html-escaper": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-3.0.3.tgz", + "integrity": "sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ==", + "license": "MIT" + }, + "node_modules/html-void-elements": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz", + "integrity": "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/http-cache-semantics": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", + "license": "BSD-2-Clause" + }, + "node_modules/iron-webcrypto": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/iron-webcrypto/-/iron-webcrypto-1.2.1.tgz", + "integrity": "sha512-feOM6FaSr6rEABp/eDfVseKyTMDt+KGpeB35SkVn9Tyn0CqvVsY3EwI0v5i8nMHyJnzCIQf7nsy3p41TPkJZhg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/brc-dd" + } + }, + "node_modules/is-docker": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-4.0.0.tgz", + "integrity": "sha512-LHE+wROyG/Y/0ZnbktRCoTix2c1RhgWaZraMZ8o1Q7zCh0VSrICJQO5oqIIISrcSBtrXv0o233w1IYwsWCjTzA==", + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-inside-container": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", + "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", + "license": "MIT", + "dependencies": { + "is-docker": "^3.0.0" + }, + "bin": { + "is-inside-container": "cli.js" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-inside-container/node_modules/is-docker": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", + "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-wsl": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.1.tgz", + "integrity": "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==", + "license": "MIT", + "dependencies": { + "is-inside-container": "^1.0.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jiti": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", + "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonc-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", + "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", + "license": "MIT" + }, + "node_modules/kolorist": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/kolorist/-/kolorist-1.8.0.tgz", + "integrity": "sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==", + "license": "MIT" + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/longest-streak": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", + "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/lru-cache": { + "version": "11.3.6", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.3.6.tgz", + "integrity": "sha512-Gf/KoL3C/MlI7Bt0PGI9I+TeTC/I6r/csU58N4BSNc4lppLBeKsOdFYkK+dX0ABDUMJNfCHTyPpzwwO21Awd3A==", + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/magicast": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.5.2.tgz", + "integrity": "sha512-E3ZJh4J3S9KfwdjZhe2afj6R9lGIN5Pher1pF39UGrXRqq/VDaGVIGN13BjHd2u8B61hArAGOnso7nBOouW3TQ==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "source-map-js": "^1.2.1" + } + }, + "node_modules/markdown-table": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz", + "integrity": "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/mdast-util-definitions": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-definitions/-/mdast-util-definitions-6.0.0.tgz", + "integrity": "sha512-scTllyX6pnYNZH/AIp/0ePz6s4cZtARxImwoPJ7kS42n+MnVsI4XbnG6d4ibehRIldYMWM2LD7ImQblVhUejVQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "unist-util-visit": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-find-and-replace": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.2.tgz", + "integrity": "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "escape-string-regexp": "^5.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-from-markdown": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.3.tgz", + "integrity": "sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark": "^4.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm/-/mdast-util-gfm-3.1.0.tgz", + "integrity": "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==", + "license": "MIT", + "dependencies": { + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-gfm-autolink-literal": "^2.0.0", + "mdast-util-gfm-footnote": "^2.0.0", + "mdast-util-gfm-strikethrough": "^2.0.0", + "mdast-util-gfm-table": "^2.0.0", + "mdast-util-gfm-task-list-item": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-autolink-literal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-2.0.1.tgz", + "integrity": "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "ccount": "^2.0.0", + "devlop": "^1.0.0", + "mdast-util-find-and-replace": "^3.0.0", + "micromark-util-character": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-footnote": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-2.1.0.tgz", + "integrity": "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-strikethrough": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-2.0.0.tgz", + "integrity": "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-table": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-table/-/mdast-util-gfm-table-2.0.0.tgz", + "integrity": "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "markdown-table": "^3.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-task-list-item": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-2.0.0.tgz", + "integrity": "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-phrasing": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz", + "integrity": "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-hast": { + "version": "13.2.1", + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz", + "integrity": "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@ungap/structured-clone": "^1.0.0", + "devlop": "^1.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "trim-lines": "^3.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-markdown": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.2.tgz", + "integrity": "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "longest-streak": "^3.0.0", + "mdast-util-phrasing": "^4.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "unist-util-visit": "^5.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz", + "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdn-data": { + "version": "2.27.1", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz", + "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==", + "license": "CC0-1.0" + }, + "node_modules/micromark": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz", + "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/debug": "^4.0.0", + "debug": "^4.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-core-commonmark": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz", + "integrity": "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-factory-destination": "^2.0.0", + "micromark-factory-label": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-factory-title": "^2.0.0", + "micromark-factory-whitespace": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-html-tag-name": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-gfm": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm/-/micromark-extension-gfm-3.0.0.tgz", + "integrity": "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==", + "license": "MIT", + "dependencies": { + "micromark-extension-gfm-autolink-literal": "^2.0.0", + "micromark-extension-gfm-footnote": "^2.0.0", + "micromark-extension-gfm-strikethrough": "^2.0.0", + "micromark-extension-gfm-table": "^2.0.0", + "micromark-extension-gfm-tagfilter": "^2.0.0", + "micromark-extension-gfm-task-list-item": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-autolink-literal": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-2.1.0.tgz", + "integrity": "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==", + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-footnote": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-2.1.0.tgz", + "integrity": "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-strikethrough": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-2.1.0.tgz", + "integrity": "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-table": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.1.1.tgz", + "integrity": "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-tagfilter": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-2.0.0.tgz", + "integrity": "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==", + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-task-list-item": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-2.1.0.tgz", + "integrity": "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-factory-destination": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz", + "integrity": "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-label": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz", + "integrity": "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-space": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-title": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz", + "integrity": "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-whitespace": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz", + "integrity": "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-chunked": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz", + "integrity": "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-classify-character": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz", + "integrity": "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-combine-extensions": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz", + "integrity": "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-chunked": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-numeric-character-reference": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz", + "integrity": "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-string": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz", + "integrity": "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-encode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", + "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-html-tag-name": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz", + "integrity": "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-normalize-identifier": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz", + "integrity": "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-resolve-all": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz", + "integrity": "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-sanitize-uri": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", + "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-subtokenize": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz", + "integrity": "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-types": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz", + "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/mrmime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", + "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/neotraverse": { + "version": "0.6.18", + "resolved": "https://registry.npmjs.org/neotraverse/-/neotraverse-0.6.18.tgz", + "integrity": "sha512-Z4SmBUweYa09+o6pG+eASabEpP6QkQ70yHj351pQoEXIs8uHbaU2DWVmzBANKgflPa47A50PtB2+NgRpQvr7vA==", + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/nlcst-to-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/nlcst-to-string/-/nlcst-to-string-4.0.0.tgz", + "integrity": "sha512-YKLBCcUYKAg0FNlOBT6aI91qFmSiFKiluk655WzPF+DDMA02qIyy8uiRqI8QXtcFpEvll12LpL5MXqEmAZ+dcA==", + "license": "MIT", + "dependencies": { + "@types/nlcst": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/node-fetch-native": { + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.7.tgz", + "integrity": "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==", + "license": "MIT" + }, + "node_modules/node-mock-http": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/node-mock-http/-/node-mock-http-1.0.4.tgz", + "integrity": "sha512-8DY+kFsDkNXy1sJglUfuODx1/opAGJGyrTuFqEoN90oRc2Vk0ZbD4K2qmKXBBEhZQzdKHIVfEJpDU8Ak2NJEvQ==", + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.38", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.38.tgz", + "integrity": "sha512-3qT/88Y3FbH/Kx4szpQQ4HzUbVrHPKTLVpVocKiLfoYvw9XSGOX2FmD2d6DrXbVYyAQTF2HeF6My8jmzx7/CRw==", + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/obug": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", + "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==", + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT" + }, + "node_modules/ofetch": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/ofetch/-/ofetch-1.5.1.tgz", + "integrity": "sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA==", + "license": "MIT", + "dependencies": { + "destr": "^2.0.5", + "node-fetch-native": "^1.6.7", + "ufo": "^1.6.1" + } + }, + "node_modules/ohash": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.11.tgz", + "integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==", + "license": "MIT" + }, + "node_modules/oniguruma-parser": { + "version": "0.12.2", + "resolved": "https://registry.npmjs.org/oniguruma-parser/-/oniguruma-parser-0.12.2.tgz", + "integrity": "sha512-6HVa5oIrgMC6aA6WF6XyyqbhRPJrKR02L20+2+zpDtO5QAzGHAUGw5TKQvwi5vctNnRHkJYmjAhRVQF2EKdTQw==", + "license": "MIT" + }, + "node_modules/oniguruma-to-es": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/oniguruma-to-es/-/oniguruma-to-es-4.3.6.tgz", + "integrity": "sha512-csuQ9x3Yr0cEIs/Zgx/OEt9iBw9vqIunAPQkx19R/fiMq2oGVTgcMqO/V3Ybqefr1TBvosI6jU539ksaBULJyA==", + "license": "MIT", + "dependencies": { + "oniguruma-parser": "^0.12.2", + "regex": "^6.1.0", + "regex-recursion": "^6.0.2" + } + }, + "node_modules/open": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/open/-/open-10.2.0.tgz", + "integrity": "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==", + "license": "MIT", + "dependencies": { + "default-browser": "^5.2.1", + "define-lazy-prop": "^3.0.0", + "is-inside-container": "^1.0.0", + "wsl-utils": "^0.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-limit": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-7.3.0.tgz", + "integrity": "sha512-7cIXg/Z0M5WZRblrsOla88S4wAK+zOQQWeBYfV3qJuJXMr+LnbYjaadrFaS0JILfEDPVqHyKnZ1Z/1d6J9VVUw==", + "license": "MIT", + "dependencies": { + "yocto-queue": "^1.2.1" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-queue": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-9.2.0.tgz", + "integrity": "sha512-dWgLE8AH0HjQ9fe74pUkKkvzzYT18Inp4zra3lKHnnwqGvcfcUBrvF2EAVX+envufDNBOzpPq/IBUONDbI7+3g==", + "license": "MIT", + "dependencies": { + "eventemitter3": "^5.0.4", + "p-timeout": "^7.0.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-timeout": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-7.0.1.tgz", + "integrity": "sha512-AxTM2wDGORHGEkPCt8yqxOTMgpfbEHqF51f/5fJCmwFC3C/zNcGT63SymH2ttOAaiIws2zVg4+izQCjrakcwHg==", + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/package-manager-detector": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-1.6.0.tgz", + "integrity": "sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA==", + "license": "MIT" + }, + "node_modules/parse-latin": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/parse-latin/-/parse-latin-7.0.0.tgz", + "integrity": "sha512-mhHgobPPua5kZ98EF4HWiH167JWBfl4pvAIXXdbaVohtK7a6YBOy56kvhCqduqyo/f3yrHFWmqmiMg/BkBkYYQ==", + "license": "MIT", + "dependencies": { + "@types/nlcst": "^2.0.0", + "@types/unist": "^3.0.0", + "nlcst-to-string": "^4.0.0", + "unist-util-modify-children": "^4.0.0", + "unist-util-visit-children": "^3.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "license": "MIT" + }, + "node_modules/perfect-debounce": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-2.1.0.tgz", + "integrity": "sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g==", + "license": "MIT" + }, + "node_modules/piccolore": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/piccolore/-/piccolore-0.1.3.tgz", + "integrity": "sha512-o8bTeDWjE086iwKrROaDf31K0qC/BENdm15/uH9usSC/uZjJOKb2YGiVHfLY4GhwsERiPI1jmwI2XrA7ACOxVw==", + "license": "ISC" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.14", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz", + "integrity": "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prismjs": { + "version": "1.30.0", + "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.30.0.tgz", + "integrity": "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/property-information": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz", + "integrity": "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/radix3": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/radix3/-/radix3-1.1.2.tgz", + "integrity": "sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA==", + "license": "MIT" + }, + "node_modules/readdirp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz", + "integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==", + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/regex/-/regex-6.1.0.tgz", + "integrity": "sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg==", + "license": "MIT", + "dependencies": { + "regex-utilities": "^2.3.0" + } + }, + "node_modules/regex-recursion": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/regex-recursion/-/regex-recursion-6.0.2.tgz", + "integrity": "sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==", + "license": "MIT", + "dependencies": { + "regex-utilities": "^2.3.0" + } + }, + "node_modules/regex-utilities": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/regex-utilities/-/regex-utilities-2.3.0.tgz", + "integrity": "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==", + "license": "MIT" + }, + "node_modules/rehype": { + "version": "13.0.2", + "resolved": "https://registry.npmjs.org/rehype/-/rehype-13.0.2.tgz", + "integrity": "sha512-j31mdaRFrwFRUIlxGeuPXXKWQxet52RBQRvCmzl5eCefn/KGbomK5GMHNMsOJf55fgo3qw5tST5neDuarDYR2A==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "rehype-parse": "^9.0.0", + "rehype-stringify": "^10.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rehype-parse": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/rehype-parse/-/rehype-parse-9.0.1.tgz", + "integrity": "sha512-ksCzCD0Fgfh7trPDxr2rSylbwq9iYDkSn8TCDmEJ49ljEUBxDVCzCHv7QNzZOfODanX4+bWQ4WZqLCRWYLfhag==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-from-html": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rehype-raw": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/rehype-raw/-/rehype-raw-7.0.0.tgz", + "integrity": "sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-raw": "^9.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rehype-stringify": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/rehype-stringify/-/rehype-stringify-10.0.1.tgz", + "integrity": "sha512-k9ecfXHmIPuFVI61B9DeLPN0qFHfawM6RsuX48hoqlaKSF61RskNjSm1lI8PhBEM0MRdLxVVm4WmTqJQccH9mA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-to-html": "^9.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-gfm": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.1.tgz", + "integrity": "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-gfm": "^3.0.0", + "micromark-extension-gfm": "^3.0.0", + "remark-parse": "^11.0.0", + "remark-stringify": "^11.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-parse": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz", + "integrity": "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-rehype": { + "version": "11.1.2", + "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-11.1.2.tgz", + "integrity": "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "mdast-util-to-hast": "^13.0.0", + "unified": "^11.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-smartypants": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/remark-smartypants/-/remark-smartypants-3.0.2.tgz", + "integrity": "sha512-ILTWeOriIluwEvPjv67v7Blgrcx+LZOkAUVtKI3putuhlZm84FnqDORNXPPm+HY3NdZOMhyDwZ1E+eZB/Df5dA==", + "license": "MIT", + "dependencies": { + "retext": "^9.0.0", + "retext-smartypants": "^6.0.0", + "unified": "^11.0.4", + "unist-util-visit": "^5.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/remark-stringify": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-11.0.0.tgz", + "integrity": "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-to-markdown": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/retext": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/retext/-/retext-9.0.0.tgz", + "integrity": "sha512-sbMDcpHCNjvlheSgMfEcVrZko3cDzdbe1x/e7G66dFp0Ff7Mldvi2uv6JkJQzdRcvLYE8CA8Oe8siQx8ZOgTcA==", + "license": "MIT", + "dependencies": { + "@types/nlcst": "^2.0.0", + "retext-latin": "^4.0.0", + "retext-stringify": "^4.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/retext-latin": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/retext-latin/-/retext-latin-4.0.0.tgz", + "integrity": "sha512-hv9woG7Fy0M9IlRQloq/N6atV82NxLGveq+3H2WOi79dtIYWN8OaxogDm77f8YnVXJL2VD3bbqowu5E3EMhBYA==", + "license": "MIT", + "dependencies": { + "@types/nlcst": "^2.0.0", + "parse-latin": "^7.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/retext-smartypants": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/retext-smartypants/-/retext-smartypants-6.2.0.tgz", + "integrity": "sha512-kk0jOU7+zGv//kfjXEBjdIryL1Acl4i9XNkHxtM7Tm5lFiCog576fjNC9hjoR7LTKQ0DsPWy09JummSsH1uqfQ==", + "license": "MIT", + "dependencies": { + "@types/nlcst": "^2.0.0", + "nlcst-to-string": "^4.0.0", + "unist-util-visit": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/retext-stringify": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/retext-stringify/-/retext-stringify-4.0.0.tgz", + "integrity": "sha512-rtfN/0o8kL1e+78+uxPTqu1Klt0yPzKuQ2BfWwwfgIUSayyzxpM1PJzkKt4V8803uB9qSy32MvI7Xep9khTpiA==", + "license": "MIT", + "dependencies": { + "@types/nlcst": "^2.0.0", + "nlcst-to-string": "^4.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rollup": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.3.tgz", + "integrity": "sha512-pAQK9HalE84QSm4Po3EmWIZPd3FnjkShVkiMlz1iligWYkWQ7wHYd1PF/T7QZ5TVSD6uSTon5gBVMSM4JfBV+A==", + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.60.3", + "@rollup/rollup-android-arm64": "4.60.3", + "@rollup/rollup-darwin-arm64": "4.60.3", + "@rollup/rollup-darwin-x64": "4.60.3", + "@rollup/rollup-freebsd-arm64": "4.60.3", + "@rollup/rollup-freebsd-x64": "4.60.3", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.3", + "@rollup/rollup-linux-arm-musleabihf": "4.60.3", + "@rollup/rollup-linux-arm64-gnu": "4.60.3", + "@rollup/rollup-linux-arm64-musl": "4.60.3", + "@rollup/rollup-linux-loong64-gnu": "4.60.3", + "@rollup/rollup-linux-loong64-musl": "4.60.3", + "@rollup/rollup-linux-ppc64-gnu": "4.60.3", + "@rollup/rollup-linux-ppc64-musl": "4.60.3", + "@rollup/rollup-linux-riscv64-gnu": "4.60.3", + "@rollup/rollup-linux-riscv64-musl": "4.60.3", + "@rollup/rollup-linux-s390x-gnu": "4.60.3", + "@rollup/rollup-linux-x64-gnu": "4.60.3", + "@rollup/rollup-linux-x64-musl": "4.60.3", + "@rollup/rollup-openbsd-x64": "4.60.3", + "@rollup/rollup-openharmony-arm64": "4.60.3", + "@rollup/rollup-win32-arm64-msvc": "4.60.3", + "@rollup/rollup-win32-ia32-msvc": "4.60.3", + "@rollup/rollup-win32-x64-gnu": "4.60.3", + "@rollup/rollup-win32-x64-msvc": "4.60.3", + "fsevents": "~2.3.2" + } + }, + "node_modules/rollup/node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "license": "MIT" + }, + "node_modules/run-applescript": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.1.0.tgz", + "integrity": "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/sax": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.0.tgz", + "integrity": "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=11.0.0" + } + }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/sharp": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", + "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", + "hasInstallScript": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@img/colour": "^1.0.0", + "detect-libc": "^2.1.2", + "semver": "^7.7.3" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.34.5", + "@img/sharp-darwin-x64": "0.34.5", + "@img/sharp-libvips-darwin-arm64": "1.2.4", + "@img/sharp-libvips-darwin-x64": "1.2.4", + "@img/sharp-libvips-linux-arm": "1.2.4", + "@img/sharp-libvips-linux-arm64": "1.2.4", + "@img/sharp-libvips-linux-ppc64": "1.2.4", + "@img/sharp-libvips-linux-riscv64": "1.2.4", + "@img/sharp-libvips-linux-s390x": "1.2.4", + "@img/sharp-libvips-linux-x64": "1.2.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", + "@img/sharp-libvips-linuxmusl-x64": "1.2.4", + "@img/sharp-linux-arm": "0.34.5", + "@img/sharp-linux-arm64": "0.34.5", + "@img/sharp-linux-ppc64": "0.34.5", + "@img/sharp-linux-riscv64": "0.34.5", + "@img/sharp-linux-s390x": "0.34.5", + "@img/sharp-linux-x64": "0.34.5", + "@img/sharp-linuxmusl-arm64": "0.34.5", + "@img/sharp-linuxmusl-x64": "0.34.5", + "@img/sharp-wasm32": "0.34.5", + "@img/sharp-win32-arm64": "0.34.5", + "@img/sharp-win32-ia32": "0.34.5", + "@img/sharp-win32-x64": "0.34.5" + } + }, + "node_modules/shiki": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/shiki/-/shiki-4.0.2.tgz", + "integrity": "sha512-eAVKTMedR5ckPo4xne/PjYQYrU3qx78gtJZ+sHlXEg5IHhhoQhMfZVzetTYuaJS0L2Ef3AcCRzCHV8T0WI6nIQ==", + "license": "MIT", + "dependencies": { + "@shikijs/core": "4.0.2", + "@shikijs/engine-javascript": "4.0.2", + "@shikijs/engine-oniguruma": "4.0.2", + "@shikijs/langs": "4.0.2", + "@shikijs/themes": "4.0.2", + "@shikijs/types": "4.0.2", + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/sirv": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.2.tgz", + "integrity": "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==", + "license": "MIT", + "dependencies": { + "@polka/url": "^1.0.0-next.24", + "mrmime": "^2.0.0", + "totalist": "^3.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "license": "MIT" + }, + "node_modules/smol-toml": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/smol-toml/-/smol-toml-1.6.1.tgz", + "integrity": "sha512-dWUG8F5sIIARXih1DTaQAX4SsiTXhInKf1buxdY9DIg4ZYPZK5nGM1VRIYmEbDbsHt7USo99xSLFu5Q1IqTmsg==", + "license": "BSD-3-Clause", + "engines": { + "node": ">= 18" + }, + "funding": { + "url": "https://github.com/sponsors/cyyynthia" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/space-separated-tokens": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", + "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/stringify-entities": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", + "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==", + "license": "MIT", + "dependencies": { + "character-entities-html4": "^2.0.0", + "character-entities-legacy": "^3.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/svgo": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/svgo/-/svgo-4.0.1.tgz", + "integrity": "sha512-XDpWUOPC6FEibaLzjfe0ucaV0YrOjYotGJO1WpF0Zd+n6ZGEQUsSugaoLq9QkEZtAfQIxT42UChcssDVPP3+/w==", + "license": "MIT", + "dependencies": { + "commander": "^11.1.0", + "css-select": "^5.1.0", + "css-tree": "^3.0.1", + "css-what": "^6.1.0", + "csso": "^5.0.5", + "picocolors": "^1.1.1", + "sax": "^1.5.0" + }, + "bin": { + "svgo": "bin/svgo.js" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/svgo" + } + }, + "node_modules/tailwindcss": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.4.tgz", + "integrity": "sha512-HhKppgO81FQof5m6TEnuBWCZGgfRAWbaeOaGT00KOy/Pf/j6oUihdvBpA7ltCeAvZpFhW3j0PTclkxsd4IXYDA==", + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", + "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/tiny-inflate": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.3.tgz", + "integrity": "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==", + "license": "MIT" + }, + "node_modules/tinyclip": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/tinyclip/-/tinyclip-0.1.12.tgz", + "integrity": "sha512-Ae3OVUqifDw0wBriIBS7yVaW44Dp6eSHQcyq4Igc7eN2TJH/2YsicswaW+J/OuMvhpDPOKEgpAZCjkb4hpoyeA==", + "license": "MIT", + "engines": { + "node": "^16.14.0 || >= 17.3.0" + } + }, + "node_modules/tinyexec": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.1.2.tgz", + "integrity": "sha512-dAqSqE/RabpBKI8+h26GfLq6Vb3JVXs30XYQjdMjaj/c2tS8IYYMbIzP599KtRj7c57/wYApb3QjgRgXmrCukA==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.16", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", + "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/totalist": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", + "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/trim-lines": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", + "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/trough": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz", + "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD", + "optional": true + }, + "node_modules/ufo": { + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.4.tgz", + "integrity": "sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==", + "license": "MIT" + }, + "node_modules/ultrahtml": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/ultrahtml/-/ultrahtml-1.6.0.tgz", + "integrity": "sha512-R9fBn90VTJrqqLDwyMph+HGne8eqY1iPfYhPzZrvKpIfwkWZbcYlfpsb8B9dTvBfpy1/hqAD7Wi8EKfP9e8zdw==", + "license": "MIT" + }, + "node_modules/uncrypto": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/uncrypto/-/uncrypto-0.1.3.tgz", + "integrity": "sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==", + "license": "MIT" + }, + "node_modules/unified": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", + "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "bail": "^2.0.0", + "devlop": "^1.0.0", + "extend": "^3.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unifont": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/unifont/-/unifont-0.7.4.tgz", + "integrity": "sha512-oHeis4/xl42HUIeHuNZRGEvxj5AaIKR+bHPNegRq5LV1gdc3jundpONbjglKpihmJf+dswygdMJn3eftGIMemg==", + "license": "MIT", + "dependencies": { + "css-tree": "^3.1.0", + "ofetch": "^1.5.1", + "ohash": "^2.0.11" + } + }, + "node_modules/unist-util-find-after": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-find-after/-/unist-util-find-after-5.0.0.tgz", + "integrity": "sha512-amQa0Ep2m6hE2g72AugUItjbuM8X8cGQnFoHk0pGfrFeT9GZhzN5SW8nRsiGKK7Aif4CrACPENkA6P/Lw6fHGQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-is": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz", + "integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-modify-children": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-modify-children/-/unist-util-modify-children-4.0.0.tgz", + "integrity": "sha512-+tdN5fGNddvsQdIzUF3Xx82CU9sMM+fA0dLgR9vOmT0oPT2jH+P1nd5lSqfCfXAw+93NhcXNY2qqvTUtE4cQkw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "array-iterate": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-position": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", + "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-remove-position": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-remove-position/-/unist-util-remove-position-5.0.0.tgz", + "integrity": "sha512-Hp5Kh3wLxv0PHj9m2yZhhLt58KzPtEYKQQ4yxfYFEO7EvHwzyDYnduhHnY1mDxoqr7VUwVuHXk9RXKIiYS1N8Q==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-visit": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.1.0.tgz", + "integrity": "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit-children": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/unist-util-visit-children/-/unist-util-visit-children-3.0.0.tgz", + "integrity": "sha512-RgmdTfSBOg04sdPcpTSD1jzoNBjt9a80/ZCzp5cI9n1qPzLZWF9YdvWGN2zmTumP1HWhXKdUWexjy/Wy/lJ7tA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit-parents": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz", + "integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unplugin-utils": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/unplugin-utils/-/unplugin-utils-0.3.1.tgz", + "integrity": "sha512-5lWVjgi6vuHhJ526bI4nlCOmkCIF3nnfXkCMDeMJrtdvxTs6ZFCM8oNufGTsDbKv/tJ/xj8RpvXjRuPBZJuJog==", + "license": "MIT", + "dependencies": { + "pathe": "^2.0.3", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/sponsors/sxzz" + } + }, + "node_modules/unstorage": { + "version": "1.17.5", + "resolved": "https://registry.npmjs.org/unstorage/-/unstorage-1.17.5.tgz", + "integrity": "sha512-0i3iqvRfx29hkNntHyQvJTpf5W9dQ9ZadSoRU8+xVlhVtT7jAX57fazYO9EHvcRCfBCyi5YRya7XCDOsbTgkPg==", + "license": "MIT", + "dependencies": { + "anymatch": "^3.1.3", + "chokidar": "^5.0.0", + "destr": "^2.0.5", + "h3": "^1.15.10", + "lru-cache": "^11.2.7", + "node-fetch-native": "^1.6.7", + "ofetch": "^1.5.1", + "ufo": "^1.6.3" + }, + "peerDependencies": { + "@azure/app-configuration": "^1.8.0", + "@azure/cosmos": "^4.2.0", + "@azure/data-tables": "^13.3.0", + "@azure/identity": "^4.6.0", + "@azure/keyvault-secrets": "^4.9.0", + "@azure/storage-blob": "^12.26.0", + "@capacitor/preferences": "^6 || ^7 || ^8", + "@deno/kv": ">=0.9.0", + "@netlify/blobs": "^6.5.0 || ^7.0.0 || ^8.1.0 || ^9.0.0 || ^10.0.0", + "@planetscale/database": "^1.19.0", + "@upstash/redis": "^1.34.3", + "@vercel/blob": ">=0.27.1", + "@vercel/functions": "^2.2.12 || ^3.0.0", + "@vercel/kv": "^1 || ^2 || ^3", + "aws4fetch": "^1.0.20", + "db0": ">=0.2.1", + "idb-keyval": "^6.2.1", + "ioredis": "^5.4.2", + "uploadthing": "^7.4.4" + }, + "peerDependenciesMeta": { + "@azure/app-configuration": { + "optional": true + }, + "@azure/cosmos": { + "optional": true + }, + "@azure/data-tables": { + "optional": true + }, + "@azure/identity": { + "optional": true + }, + "@azure/keyvault-secrets": { + "optional": true + }, + "@azure/storage-blob": { + "optional": true + }, + "@capacitor/preferences": { + "optional": true + }, + "@deno/kv": { + "optional": true + }, + "@netlify/blobs": { + "optional": true + }, + "@planetscale/database": { + "optional": true + }, + "@upstash/redis": { + "optional": true + }, + "@vercel/blob": { + "optional": true + }, + "@vercel/functions": { + "optional": true + }, + "@vercel/kv": { + "optional": true + }, + "aws4fetch": { + "optional": true + }, + "db0": { + "optional": true + }, + "idb-keyval": { + "optional": true + }, + "ioredis": { + "optional": true + }, + "uploadthing": { + "optional": true + } + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/vfile": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", + "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-location": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/vfile-location/-/vfile-location-5.0.3.tgz", + "integrity": "sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-message": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz", + "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vite": { + "version": "7.3.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.3.tgz", + "integrity": "sha512-/4XH147Ui7OGTjg3HbdWe5arnZQSbfuRzdr9Ec7TQi5I7R+ir0Rlc9GIvD4v0XZurELqA035KVXJXpR61xhiTA==", + "license": "MIT", + "dependencies": { + "esbuild": "^0.27.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite-dev-rpc": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/vite-dev-rpc/-/vite-dev-rpc-1.1.0.tgz", + "integrity": "sha512-pKXZlgoXGoE8sEKiKJSng4hI1sQ4wi5YT24FCrwrLt6opmkjlqPPVmiPWWJn8M8byMxRGzp1CrFuqQs4M/Z39A==", + "license": "MIT", + "dependencies": { + "birpc": "^2.4.0", + "vite-hot-client": "^2.1.0" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "vite": "^2.9.0 || ^3.0.0-0 || ^4.0.0-0 || ^5.0.0-0 || ^6.0.1 || ^7.0.0-0" + } + }, + "node_modules/vite-hot-client": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/vite-hot-client/-/vite-hot-client-2.2.0.tgz", + "integrity": "sha512-76Zs9zrHbH7M7wqeyooGQKdX+yg0pQ0xuQ1PbFp4z5a0Lzn2e5IPFoCswnmqZ4GiwqB4Jo3WcDAMO9jARTJl8w==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "vite": "^2.6.0 || ^3.0.0 || ^4.0.0 || ^5.0.0-0 || ^6.0.0-0 || ^7.0.0-0 || ^8.0.0" + } + }, + "node_modules/vite-plugin-inspect": { + "version": "11.3.3", + "resolved": "https://registry.npmjs.org/vite-plugin-inspect/-/vite-plugin-inspect-11.3.3.tgz", + "integrity": "sha512-u2eV5La99oHoYPHE6UvbwgEqKKOQGz86wMg40CCosP6q8BkB6e5xPneZfYagK4ojPJSj5anHCrnvC20DpwVdRA==", + "license": "MIT", + "dependencies": { + "ansis": "^4.1.0", + "debug": "^4.4.1", + "error-stack-parser-es": "^1.0.5", + "ohash": "^2.0.11", + "open": "^10.2.0", + "perfect-debounce": "^2.0.0", + "sirv": "^3.0.1", + "unplugin-utils": "^0.3.0", + "vite-dev-rpc": "^1.1.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "vite": "^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "@nuxt/kit": { + "optional": true + } + } + }, + "node_modules/vite-plugin-vue-devtools": { + "version": "8.1.2", + "resolved": "https://registry.npmjs.org/vite-plugin-vue-devtools/-/vite-plugin-vue-devtools-8.1.2.tgz", + "integrity": "sha512-gt5h1CNryR9Hy0tvhSbqY3j0F7aj0pGxBxWLa1lXSiZVkhdWDf0vbCOZyjh8ivFGE6FDHTGy3zkcZGlMZdVHig==", + "license": "MIT", + "dependencies": { + "@vue/devtools-core": "^8.1.2", + "@vue/devtools-kit": "^8.1.2", + "@vue/devtools-shared": "^8.1.2", + "sirv": "^3.0.2", + "vite-plugin-inspect": "^11.3.3", + "vite-plugin-vue-inspector": "^6.0.0" + }, + "engines": { + "node": ">=v14.21.3" + }, + "peerDependencies": { + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/vite-plugin-vue-inspector": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/vite-plugin-vue-inspector/-/vite-plugin-vue-inspector-6.0.0.tgz", + "integrity": "sha512-OpyITJLgZNibxlrik1EmRtvXHDjLRxNPsWkGFTERZs2LgMEdG4W0WoFt5GIgp3a3jRou+eJR8U1zOBk/XQgEbw==", + "license": "MIT", + "dependencies": { + "@babel/core": "^7.23.0", + "@babel/plugin-proposal-decorators": "^7.23.0", + "@babel/plugin-syntax-import-attributes": "^7.22.5", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-transform-typescript": "^7.22.15", + "@vue/babel-plugin-jsx": "^1.1.5", + "@vue/compiler-dom": "^3.3.4", + "kolorist": "^1.8.0", + "magic-string": "^0.30.4" + }, + "peerDependencies": { + "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/vite-plugin-vue-inspector/node_modules/@vue/babel-helper-vue-transform-on": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@vue/babel-helper-vue-transform-on/-/babel-helper-vue-transform-on-1.5.0.tgz", + "integrity": "sha512-0dAYkerNhhHutHZ34JtTl2czVQHUNWv6xEbkdF5W+Yrv5pCWsqjeORdOgbtW2I9gWlt+wBmVn+ttqN9ZxR5tzA==", + "license": "MIT" + }, + "node_modules/vite-plugin-vue-inspector/node_modules/@vue/babel-plugin-jsx": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@vue/babel-plugin-jsx/-/babel-plugin-jsx-1.5.0.tgz", + "integrity": "sha512-mneBhw1oOqCd2247O0Yw/mRwC9jIGACAJUlawkmMBiNmL4dGA2eMzuNZVNqOUfYTa6vqmND4CtOPzmEEEqLKFw==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/plugin-syntax-jsx": "^7.27.1", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.0", + "@babel/types": "^7.28.2", + "@vue/babel-helper-vue-transform-on": "1.5.0", + "@vue/babel-plugin-resolve-type": "1.5.0", + "@vue/shared": "^3.5.18" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + } + } + }, + "node_modules/vite-plugin-vue-inspector/node_modules/@vue/babel-plugin-resolve-type": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@vue/babel-plugin-resolve-type/-/babel-plugin-resolve-type-1.5.0.tgz", + "integrity": "sha512-Wm/60o+53JwJODm4Knz47dxJnLDJ9FnKnGZJbUUf8nQRAtt6P+undLUAVU3Ha33LxOJe6IPoifRQ6F/0RrU31w==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/parser": "^7.28.0", + "@vue/compiler-sfc": "^3.5.18" + }, + "funding": { + "url": "https://github.com/sponsors/sxzz" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/vitefu": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-1.1.3.tgz", + "integrity": "sha512-ub4okH7Z5KLjb6hDyjqrGXqWtWvoYdU3IGm/NorpgHncKoLTCfRIbvlhBm7r0YstIaQRYlp4yEbFqDcKSzXSSg==", + "license": "MIT", + "workspaces": [ + "tests/deps/*", + "tests/projects/*", + "tests/projects/workspace/packages/*" + ], + "peerDependencies": { + "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "vite": { + "optional": true + } + } + }, + "node_modules/vue": { + "version": "3.5.34", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.34.tgz", + "integrity": "sha512-WdLBG9gm02OgJIG9axd5Hpx0TFLdzVgfG2evFFu8Rur5O/IoGc5cMjnjh3tPL6GnRGsYvUhBSKVPYVcxRKpMCA==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.34", + "@vue/compiler-sfc": "3.5.34", + "@vue/runtime-dom": "3.5.34", + "@vue/server-renderer": "3.5.34", + "@vue/shared": "3.5.34" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/web-namespaces": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/web-namespaces/-/web-namespaces-2.0.1.tgz", + "integrity": "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/which-pm-runs": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/which-pm-runs/-/which-pm-runs-1.1.0.tgz", + "integrity": "sha512-n1brCuqClxfFfq/Rb0ICg9giSZqCS+pLtccdag6C2HyufBrh3fBOiy9nb6ggRMvWOVH5GrdJskj5iGTZNxd7SA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/wsl-utils": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-0.1.0.tgz", + "integrity": "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==", + "license": "MIT", + "dependencies": { + "is-wsl": "^3.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/xxhash-wasm": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/xxhash-wasm/-/xxhash-wasm-1.1.0.tgz", + "integrity": "sha512-147y/6YNh+tlp6nd/2pWq38i9h6mz/EuQ6njIrmW8D1BS5nCqs0P6DG+m6zTGnNz5I+uhZ0SHxBs9BsPrwcKDA==", + "license": "MIT" + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "license": "ISC" + }, + "node_modules/yargs-parser": { + "version": "22.0.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-22.0.0.tgz", + "integrity": "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==", + "license": "ISC", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=23" + } + }, + "node_modules/yocto-queue": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.2.2.tgz", + "integrity": "sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ==", + "license": "MIT", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", + "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zwitch": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", + "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..1030bc4 --- /dev/null +++ b/package.json @@ -0,0 +1,22 @@ +{ + "name": "astro-site-cerveau", + "type": "module", + "version": "0.0.1", + "engines": { + "node": ">=22.12.0" + }, + "scripts": { + "dev": "astro dev", + "build": "astro build", + "preview": "astro preview", + "astro": "astro" + }, + "dependencies": { + "@astrojs/vue": "^6.0.1", + "@tailwindcss/vite": "^4.2.4", + "astro": "^6.3.1", + "embla-carousel-vue": "^8.6.0", + "tailwindcss": "^4.2.4", + "vue": "^3.5.34" + } +} diff --git a/public/favicon.ico b/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..7f48a94d16071d6c8d06478c7458ab12e675019c GIT binary patch literal 655 zcmV;A0&x9_P)Rl-XF(A`bsas&GH{e7U1}Ri zJr5jR8B2*Jd6$=$AqgTM2o2FV$WZ9|#jJ3mmpEs{jB0ps@*Kxv}=RB|IJih8Z&fqwCG`%bN0000#bW%=J zQ=IH#a_&L{B{_6Lu_3m>0bMN%+@aOmN_3G~H^8EGi>+bXO=;-|Z`uFnf==AdP z{Oj-S=ltmI=<4`LcLE*&009F@L_t(|+I`d4ZUZ3@1<*Uo7H^LoCw6-8z4wsbd;b4l zA}zMFtOw2mLX6O5Mgl}(5P=uOM4%=tnuHiuAp%(G<c=npm$Fz%eL + + + diff --git a/src/components/astro/ColCentre.astro b/src/components/astro/ColCentre.astro new file mode 100644 index 0000000..60b534d --- /dev/null +++ b/src/components/astro/ColCentre.astro @@ -0,0 +1,11 @@ +--- +// Placeholder Centre : HAUT mindmap AEP (PC3) ; BAS iframe carte AEP (PC4) +--- +
+
+

Mindmap AEP — PC3

+
+
+

Iframe carte AEP — PC4

+
+
diff --git a/src/components/astro/ColInsta.astro b/src/components/astro/ColInsta.astro new file mode 100644 index 0000000..5215577 --- /dev/null +++ b/src/components/astro/ColInsta.astro @@ -0,0 +1,12 @@ +--- +// Placeholder Insta : 2 carrousels @aep + @julesneny — PC5 oEmbed +--- +
+

Insta

+
+

@aep carrousel — PC5

+
+
+

@julesneny carrousel — PC5

+
+
diff --git a/src/components/astro/ColJournal.astro b/src/components/astro/ColJournal.astro new file mode 100644 index 0000000..98e8a3a --- /dev/null +++ b/src/components/astro/ColJournal.astro @@ -0,0 +1,14 @@ +--- +// Placeholder Journal — PC6 remplit avec entries chrono +--- +
+

Journal

+
    +
  • Entry placeholder 1
  • +
  • Entry placeholder 2
  • +
  • Entry placeholder 3
  • +
  • Entry placeholder 4
  • +
  • Entry placeholder 5
  • +
+

Nav latérale + manifeste CTA — PC2

+
diff --git a/src/components/astro/HamburgerMenu.astro b/src/components/astro/HamburgerMenu.astro new file mode 100644 index 0000000..208fbd4 --- /dev/null +++ b/src/components/astro/HamburgerMenu.astro @@ -0,0 +1,12 @@ +--- +// Placeholder hamburger menu — PC2 ajoute liens nav +--- + diff --git a/src/components/astro/PopupOnboarding.astro b/src/components/astro/PopupOnboarding.astro new file mode 100644 index 0000000..5dda8b5 --- /dev/null +++ b/src/components/astro/PopupOnboarding.astro @@ -0,0 +1,11 @@ +--- +// Placeholder popup onboarding — PC2 fait l'animation et le contenu +--- + diff --git a/src/components/vue/CarteO.vue b/src/components/vue/CarteO.vue new file mode 100644 index 0000000..7f02c77 --- /dev/null +++ b/src/components/vue/CarteO.vue @@ -0,0 +1,9 @@ + + + diff --git a/src/components/vue/ChatbotPlaceholder.vue b/src/components/vue/ChatbotPlaceholder.vue new file mode 100644 index 0000000..8e187ed --- /dev/null +++ b/src/components/vue/ChatbotPlaceholder.vue @@ -0,0 +1,9 @@ + + + diff --git a/src/components/vue/JournalList.vue b/src/components/vue/JournalList.vue new file mode 100644 index 0000000..fe3f4be --- /dev/null +++ b/src/components/vue/JournalList.vue @@ -0,0 +1,9 @@ + + + diff --git a/src/components/vue/SwipeContainer.vue b/src/components/vue/SwipeContainer.vue new file mode 100644 index 0000000..49b08f8 --- /dev/null +++ b/src/components/vue/SwipeContainer.vue @@ -0,0 +1,102 @@ + + + diff --git a/src/layouts/BaseLayout.astro b/src/layouts/BaseLayout.astro new file mode 100644 index 0000000..913fb31 --- /dev/null +++ b/src/layouts/BaseLayout.astro @@ -0,0 +1,33 @@ +--- +import '../styles/global.css'; + +interface Props { + title?: string; + description?: string; +} + +const { + title = 'trans-former.fr', + description = 'Page-cerveau : journal, mindmap AEP, Insta', +} = Astro.props; +--- + + + + + + + + {title} + + + + + + + + + + + + diff --git a/src/pages/index.astro b/src/pages/index.astro new file mode 100644 index 0000000..844122f --- /dev/null +++ b/src/pages/index.astro @@ -0,0 +1,29 @@ +--- +import BaseLayout from '../layouts/BaseLayout.astro'; +import ColJournal from '../components/astro/ColJournal.astro'; +import ColCentre from '../components/astro/ColCentre.astro'; +import ColInsta from '../components/astro/ColInsta.astro'; +import SwipeContainer from '../components/vue/SwipeContainer.vue'; +import HamburgerMenu from '../components/astro/HamburgerMenu.astro'; +import PopupOnboarding from '../components/astro/PopupOnboarding.astro'; +--- + + + + + + + + +
+ + + + + +
+
diff --git a/src/pages/manifeste.astro b/src/pages/manifeste.astro new file mode 100644 index 0000000..d8b36ab --- /dev/null +++ b/src/pages/manifeste.astro @@ -0,0 +1,9 @@ +--- +import BaseLayout from '../layouts/BaseLayout.astro'; +--- + +
+

Manifeste

+

Placeholder — PC2 importe le contenu manifeste depuis astro-site existant.

+
+
diff --git a/src/styles/global.css b/src/styles/global.css new file mode 100644 index 0000000..f1d8c73 --- /dev/null +++ b/src/styles/global.css @@ -0,0 +1 @@ +@import "tailwindcss"; diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..0649e42 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "astro/tsconfigs/strict", + "include": [ + ".astro/types.d.ts", + "**/*" + ], + "exclude": [ + "dist" + ], + "compilerOptions": { + "jsx": "preserve" + } +} \ No newline at end of file From 71053ec9a69611cdb16ed51fb0a3da44309e8eaf Mon Sep 17 00:00:00 2001 From: Jules Neny Date: Sat, 9 May 2026 00:55:01 +0200 Subject: [PATCH 02/36] feat: PC5 col D embeds Insta via Behold (avec fallback profil statique) - InstaFeed.vue (Vue island) : consomme feeds.behold.so/{feedId}, skeleton loading, grid 2x3 thumbnails, fallback profil si placeholder ou erreur - ColInsta.astro enrichi : 2 sections @aep.politique + @julesneny, hydratation client:visible (lazy) - .env.example committe (PUBLIC_BEHOLD_AEP/JULESNENY vides) ; .env.local deja gitignore - docs/BEHOLD-SETUP.md : procedure inscription + recup feed IDs + alternatives + note CSP PC8 Action Jules requise (async) : inscription Behold + connexion 2 comptes Insta + remplir .env.local. --- .env.example | 6 ++ docs/BEHOLD-SETUP.md | 49 +++++++++++++ src/components/astro/ColInsta.astro | 32 ++++++--- src/components/vue/InstaFeed.vue | 107 ++++++++++++++++++++++++++++ 4 files changed, 184 insertions(+), 10 deletions(-) create mode 100644 .env.example create mode 100644 docs/BEHOLD-SETUP.md create mode 100644 src/components/vue/InstaFeed.vue diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..15ce499 --- /dev/null +++ b/.env.example @@ -0,0 +1,6 @@ +# Behold.so feed IDs (voir docs/BEHOLD-SETUP.md) +# 1) Inscris-toi sur https://behold.so/dashboard +# 2) Connecte les 2 comptes Insta (@aep.politique + @julesneny) +# 3) Recupere les feed IDs et copie ce fichier vers .env.local puis remplis ci-dessous +PUBLIC_BEHOLD_AEP= +PUBLIC_BEHOLD_JULESNENY= diff --git a/docs/BEHOLD-SETUP.md b/docs/BEHOLD-SETUP.md new file mode 100644 index 0000000..4c06985 --- /dev/null +++ b/docs/BEHOLD-SETUP.md @@ -0,0 +1,49 @@ +# Setup Behold pour embeds Insta + +Page cerveau col D (`ColInsta.astro`) consomme 2 feeds Insta via [Behold.so](https://behold.so) (gratuit, 2 feeds, sync 1h, sans login user). + +## Etapes + +1. Creer compte sur https://behold.so (gratuit jusqu'a 2 feeds) +2. Connecter `@aep.politique` (Login Instagram via Facebook Business) +3. Connecter `@julesneny` (idem) +4. Recuperer les 2 feed IDs depuis le dashboard Behold +5. Copier `.env.example` vers `.env.local` (a la racine du repo) +6. Remplir `.env.local` : + + ```bash + PUBLIC_BEHOLD_AEP=xxxxxxxxxxxx + PUBLIC_BEHOLD_JULESNENY=yyyyyyyyyyyy + ``` + +7. Relancer `npm run dev` ou rebuild + redeploy (PC8) + +## Comportement par defaut + +Sans feed IDs valides (placeholder), le composant `InstaFeed.vue` affiche un fallback gracieux : +- Titre du compte (lien direct vers Instagram) +- Bio courte +- Bouton "Voir sur Instagram" + +C'est OK pour un V1 visuel "complet". UX degradee mais pas casse. + +## Ressources techniques + +- API Behold : `https://feeds.behold.so/{feedId}` -> JSON array de posts +- Sync : 1h (Behold rafraichit depuis Insta automatiquement) +- CDN : images servies depuis Behold (pas de hot-link Insta direct) + +## CSP (PC8 deploy) + +Si une CSP est ajoutee au deploy Caddy, prevoir : + +``` +connect-src 'self' https://feeds.behold.so; +img-src 'self' data: https://feeds.behold.so https://*.cdninstagram.com; +``` + +## Alternatives si Behold ne convient pas + +- **EmbedSocial** (~$8/mois) : https://embedsocial.com +- **Scrape n8n nocturne** (V1.5) : Insta Graph API via compte Business + Page Facebook ; coherent avec PC6 journal-aggregate +- **Posts manuels oEmbed** (V2) : Jules selectionne 6-10 URLs, oEmbed post-by-post diff --git a/src/components/astro/ColInsta.astro b/src/components/astro/ColInsta.astro index 5215577..93560e4 100644 --- a/src/components/astro/ColInsta.astro +++ b/src/components/astro/ColInsta.astro @@ -1,12 +1,24 @@ --- -// Placeholder Insta : 2 carrousels @aep + @julesneny — PC5 oEmbed +import InstaFeed from '../vue/InstaFeed.vue'; + +// Feed IDs Behold a remplir apres inscription Behold (voir docs/BEHOLD-SETUP.md) +const FEED_AEP = import.meta.env.PUBLIC_BEHOLD_AEP || 'PLACEHOLDER_AEP'; +const FEED_JULESNENY = import.meta.env.PUBLIC_BEHOLD_JULESNENY || 'PLACEHOLDER_JULESNENY'; --- -
-

Insta

-
-

@aep carrousel — PC5

-
-
-

@julesneny carrousel — PC5

-
-
+
+ + + +
diff --git a/src/components/vue/InstaFeed.vue b/src/components/vue/InstaFeed.vue new file mode 100644 index 0000000..4e3ccbe --- /dev/null +++ b/src/components/vue/InstaFeed.vue @@ -0,0 +1,107 @@ + + + From 712ed0eefa2e577e8aa5a25b09e0915ca15c705b Mon Sep 17 00:00:00 2001 From: Jules Neny Date: Sat, 9 May 2026 00:58:19 +0200 Subject: [PATCH 03/36] feat(pc2): hamburger nav + manifeste V1 + popup onboarding - HamburgerMenu drawer slide-in left avec liens A propos, Manifeste, Mentions legales (ESC + clic overlay pour fermer) - ColJournal enrichi : CTA Manifeste, accordeon Hashtags (7 plateformes, ferme mobile / ouvert desktop, persistence localStorage), skeleton Journal pour PC6 - Page /manifeste : V1 redige integre (em-dashes remplaces par tirets/points-virgules), pivot stylise blockquote, diagramme mouvements en 3 sections boites - Page /manifeste/commander : stub form pre-inscription (V1 localStorage, V1.1 cable Listmonk) - Page /a-propos : extrait de Contexte Global, 3-4 paragraphes Jules - Page /mentions-legales : placeholder court (editeur, hebergeur Hetzner, pas de cookies) - PopupOnboarding : micro-resume 3 lignes proposees, dismiss X / CTA / scroll 200px / ESC, flag tf-onboarded --- src/components/astro/ColJournal.astro | 114 +++++++++-- src/components/astro/HamburgerMenu.astro | 103 +++++++++- src/components/astro/PopupOnboarding.astro | 100 +++++++++- src/pages/a-propos.astro | 79 ++++++++ src/pages/manifeste.astro | 219 ++++++++++++++++++++- src/pages/manifeste/commander.astro | 112 +++++++++++ src/pages/mentions-legales.astro | 92 +++++++++ 7 files changed, 792 insertions(+), 27 deletions(-) create mode 100644 src/pages/a-propos.astro create mode 100644 src/pages/manifeste/commander.astro create mode 100644 src/pages/mentions-legales.astro diff --git a/src/components/astro/ColJournal.astro b/src/components/astro/ColJournal.astro index 98e8a3a..b067d8a 100644 --- a/src/components/astro/ColJournal.astro +++ b/src/components/astro/ColJournal.astro @@ -1,14 +1,104 @@ --- -// Placeholder Journal — PC6 remplit avec entries chrono +// ColJournal - colonne gauche : CTA Manifeste + Hashtags accordeon + Journal skeleton +// 7 hashtags = 7 plateformes (cf delta 15 du PILOTE-PC.md) +const hashtags = [ + { tag: '#manifeste', plateforme: 'Blog trans-former.fr', canal: 'ecriture longue' }, + { tag: '#building-public', plateforme: 'LinkedIn', canal: 'journal pro' }, + { tag: '#politique', plateforme: 'Substack', canal: 'pensee AEP' }, + { tag: '#aep-politique', plateforme: 'Insta @aep.politique', canal: 'carrousels manifeste' }, + { tag: '#peinture', plateforme: 'Insta @julesneny', canal: 'art / poesie / Corse' }, + { tag: '#podcast', plateforme: 'Castopod', canal: 'podcast.trans-former.fr' }, + { tag: '#stack', plateforme: 'GitHub', canal: 'open source' }, +]; --- -
-

Journal

-
    -
  • Entry placeholder 1
  • -
  • Entry placeholder 2
  • -
  • Entry placeholder 3
  • -
  • Entry placeholder 4
  • -
  • Entry placeholder 5
  • -
-

Nav latérale + manifeste CTA — PC2

-
+
+ + + Lire le manifeste → + + + +
+ + Hashtags + 7 plateformes + +
    + {hashtags.map(({ tag, plateforme, canal }) => ( +
  • + +
  • + ))} +
+
+ + +
+

+ Journal + chrono +

+
+ +

+ Chargement du journal... +

+
+
+
+ + diff --git a/src/components/astro/HamburgerMenu.astro b/src/components/astro/HamburgerMenu.astro index 208fbd4..0927d27 100644 --- a/src/components/astro/HamburgerMenu.astro +++ b/src/components/astro/HamburgerMenu.astro @@ -1,12 +1,103 @@ --- -// Placeholder hamburger menu — PC2 ajoute liens nav +// HamburgerMenu - drawer slide-in left avec liens nav (PC2) +// Astro vanilla + script inline, pas besoin d'island Vue --- + + + + diff --git a/src/components/astro/PopupOnboarding.astro b/src/components/astro/PopupOnboarding.astro index 5dda8b5..ab25138 100644 --- a/src/components/astro/PopupOnboarding.astro +++ b/src/components/astro/PopupOnboarding.astro @@ -1,11 +1,101 @@ --- -// Placeholder popup onboarding — PC2 fait l'animation et le contenu +// PopupOnboarding - micro-resume premier visit, localStorage flag tf-onboarded +// Le micro-resume est une PROPOSITION Opus a iterer avec Jules. +// Stocke en constante en tete pour iteration facile. +const microResume = `Architecture, ecologie, politique : un commun a construire ensemble. +Reapproprions-nous nos infrastructures vitales en repartant de l'existant. +Ce site, c'est le journal vivant de cette bifurcation.`; --- diff --git a/src/components/astro/IframeCarteAEP.astro b/src/components/astro/IframeCarteAEP.astro new file mode 100644 index 0000000..66003a9 --- /dev/null +++ b/src/components/astro/IframeCarteAEP.astro @@ -0,0 +1,55 @@ +--- +// PC4 - iframe carte AEP (cartobifurcation) avec skeleton loader + fallback timeout 8s. +// Route confirmee 200 sans X-Frame-Options ni frame-ancestors restrictifs (preflight 2026-05-08). +const CARTE_URL = 'https://aep.trans-former.fr/agences'; +--- +
+ +
+
Chargement de la carte AEP...
+
+ + +
+ + diff --git a/src/components/astro/ScrollArticles.astro b/src/components/astro/ScrollArticles.astro new file mode 100644 index 0000000..c7b2e6d --- /dev/null +++ b/src/components/astro/ScrollArticles.astro @@ -0,0 +1,72 @@ +--- +// PC4 - Liste articles Substack en scroll sous l'iframe carte. +// V1 placeholder data en dur ; PC6 (journal n8n) remplacera par fetch journal.json filtre tag #politique. +const articles = [ + { + date: '2026-04-28', + titre: 'Cap sur l\'autonomie : retour sur 6 mois de chantier', + url: 'https://transformations.substack.com/p/cap-autonomie', + }, + { + date: '2026-04-15', + titre: 'Le commun comme infrastructure', + url: 'https://transformations.substack.com/p/commun-infrastructure', + }, + { + date: '2026-03-30', + titre: 'Architecte ou operateur de bifurcation ?', + url: 'https://transformations.substack.com/p/architecte-bifurcation', + }, + { + date: '2026-03-12', + titre: 'Reseaux AEP : pourquoi la coordination est politique', + url: 'https://transformations.substack.com/p/aep-coordination', + }, + { + date: '2026-02-26', + titre: 'Sortir du sauveur, entrer dans le compagnon', + url: 'https://transformations.substack.com/p/sauveur-compagnon', + }, + { + date: '2026-02-08', + titre: 'Petit manifeste contre l\'expert isole', + url: 'https://transformations.substack.com/p/contre-expert-isole', + }, + { + date: '2026-01-22', + titre: 'Ce que la commande publique fait a la pensee', + url: 'https://transformations.substack.com/p/commande-publique', + }, + // TODO PC6 : remplacer par fetch journal.json filtre tag #politique. +]; +--- +
+

+ Derniers articles ; Substack +

+ + + Voir tous les articles → + +
From e22dd6654ad3b1e7de253e5e660bdc72a3c57cbf Mon Sep 17 00:00:00 2001 From: Jules Neny Date: Sat, 9 May 2026 01:13:51 +0200 Subject: [PATCH 06/36] feat: PC6 journal unifie + n8n workflow agregateur (V1 MVP) Composant Vue JournalList : - fetch PUBLIC_JOURNAL_URL (defaut data.trans-former.fr/journal.json) - ecoute event 'hashtag-filter-change' emis par ColJournal (PC2) - filtre par hashtag actif, tri desc respecte (n8n cote serveur) - fallback gracieux : loading / errored / empty / no-match Cabling : - ColJournal.astro importe et rend - placeholder remplace par le composant Vue Workflow n8n (docs/n8n-workflow-journal-aggregate.json) : - Schedule trigger cron 0 3 * * * - Fetch Gitea Atom (jules.atom) + Behold AEP + Behold julesneny (skip si feed IDs absents) - Code Node normalisation 3 sources -> format JSON commun - Tri desc + cap top 100 - Write Binary File vers /home/node/.n8n/journal/journal.json (volume Docker partage) Sources V1 actives : - Gitea Atom (#stack) - active, 200 OK confirme - Behold @aep (#aep-politique) - conditionnel feed ID - Behold @julesneny (#peinture) - conditionnel feed ID Sources skipped (V1.5/V2) : - GitHub.com : username 'julesneny' n'existe pas (HTTP 404), pivot Gitea - Substack 'transformations' : pris par 'WoodHorse' (pas Jules), handle a confirmer - LinkedIn, Castopod, Blog : V2 Mock journal.json en public/data/ pour dev local (fallback si data.trans-former.fr indisponible). Setup VPS prepare (cf docs/PC6-JOURNAL-N8N-SETUP.md) : - Caddyfile bloc data.trans-former.fr ajoute en commentaire (active apres DNS) - Dossier /var/lib/docker/volumes/vps-kit_n8n_data/_data/journal/ cree - journal.json initial deploye - Caddy reload OK valide (config valide) - Workflow JSON copie sur VPS /tmp/n8n-workflow-journal-aggregate.json (import manuel UI) Checkpoint Jules requis : - Ajout DNS A 'data' -> 178.104.106.195 (OVH) - Decommenter bloc Caddy + reload - Import workflow n8n via UI (creds basic auth deprecies, login email user) - Run manuel + activation cron Co-Authored-By: Claude Opus 4.7 (1M context) --- .env.example | 4 + docs/PC6-JOURNAL-N8N-SETUP.md | 147 ++++++++++++++++++++ docs/n8n-workflow-journal-aggregate.json | 152 +++++++++++++++++++++ public/data/journal.json | 52 +++++++ src/components/astro/ColJournal.astro | 9 +- src/components/vue/JournalList.vue | 165 ++++++++++++++++++++++- 6 files changed, 521 insertions(+), 8 deletions(-) create mode 100644 docs/PC6-JOURNAL-N8N-SETUP.md create mode 100644 docs/n8n-workflow-journal-aggregate.json create mode 100644 public/data/journal.json diff --git a/.env.example b/.env.example index 15ce499..031e98f 100644 --- a/.env.example +++ b/.env.example @@ -4,3 +4,7 @@ # 3) Recupere les feed IDs et copie ce fichier vers .env.local puis remplis ci-dessous PUBLIC_BEHOLD_AEP= PUBLIC_BEHOLD_JULESNENY= + +# Journal unifie (PC6) - URL JSON agrege par n8n cron nocturne +# Override en local : pointer vers un mock /public/data/journal.json par exemple +PUBLIC_JOURNAL_URL=https://data.trans-former.fr/journal.json diff --git a/docs/PC6-JOURNAL-N8N-SETUP.md b/docs/PC6-JOURNAL-N8N-SETUP.md new file mode 100644 index 0000000..47448a7 --- /dev/null +++ b/docs/PC6-JOURNAL-N8N-SETUP.md @@ -0,0 +1,147 @@ +# PC6 — Setup journal unifié + n8n workflow + +Spec : `0 INBOX/PROMPTS/page-cerveau-build/PROMPT-PC6-journal-n8n.md` +Pilote : `0 INBOX/PROMPTS/page-cerveau-build/PILOTE-PC.md` (delta 5, delta 15) + +## TL;DR + +Le journal de la colonne G est alimenté par un cron n8n nocturne qui agrège plusieurs sources publiques (Gitéa Atom, Behold @aep, Behold @julesneny) et écrit un JSON statique servi par Caddy sur `data.trans-former.fr/journal.json`. + +``` +n8n cron (3h00 UTC) + -> fetch Gitéa Atom + Behold @aep + Behold @julesneny + -> normalisation Code Node + -> tri desc + top 100 + -> écrit /home/node/.n8n/journal/journal.json (volume Docker) + -> Caddy data.trans-former.fr file_server expose ce fichier + -> JournalList.vue fetch côté client (no rebuild Astro requis) +``` + +## Sources V1 actives + +| Plateforme | Hashtag | URL feed | Statut | +|---|---|---|---| +| Gitéa | `#stack` | `https://git.trans-former.fr/jules.atom` | ACTIF | +| Behold @aep | `#aep-politique` | `https://feeds.behold.so/{PUBLIC_BEHOLD_AEP}` | conditionnel (skip si feed ID absent) | +| Behold @julesneny | `#peinture` | `https://feeds.behold.so/{PUBLIC_BEHOLD_JULESNENY}` | conditionnel (skip si feed ID absent) | + +## Sources skipped (V1 -> V1.5/V2) + +| Plateforme | Hashtag | Raison | +|---|---|---| +| GitHub.com | `#stack` | username `julesneny` n'existe pas (HTTP 404). Pivot Gitéa pour le MVP. À reconfirmer si Jules a un autre handle GitHub public. | +| Substack | `#politique` | `transformations.substack.com` est pris par "WoodHorse" (pas Jules). Handle Substack à confirmer avant V1.5. | +| LinkedIn | `#building-public` | V2 (RSS via service tiers ou scrape) | +| Castopod | `#podcast` | V2 (Castopod RSS prêt mais hors scope MVP) | +| Blog `trans-former.fr` | `#manifeste` | V2 (post-PC8 deploy) | + +## Format JSON + +```json +{ + "generatedAt": "2026-05-09T03:00:00Z", + "items": [ + { + "id": "gitea-2026-05-09-pc6", + "platform": "gitea", + "hashtag": "#stack", + "date": "2026-05-09T01:01:00Z", + "titre": "PC6 journal unifié + n8n agrégateur", + "extrait": "...", + "url": "https://git.trans-former.fr/jules/astro-site-cerveau/commit/...", + "thumbnail": null + } + ], + "counts": { "total": N, "gitea": N, "instagram": N } +} +``` + +## Composant Vue + +`src/components/vue/JournalList.vue` : +- fetch `import.meta.env.PUBLIC_JOURNAL_URL` (défaut `https://data.trans-former.fr/journal.json`) +- écoute `window.addEventListener('hashtag-filter-change', ...)` émis par ColJournal.astro +- filtre par hashtag (vide ou tous cochés -> tout afficher ; tous décochés -> rien) +- tri desc déjà fait côté n8n, le composant respecte l'ordre + +Cabling : `src/components/astro/ColJournal.astro` importe et rend `` dans `#journal-list`. + +## Variable d'env + +`PUBLIC_JOURNAL_URL=https://data.trans-former.fr/journal.json` (`.env.example`) + +Override possible en local pointant vers `/data/journal.json` (mock fourni dans `public/data/journal.json`). + +## Setup VPS — étapes (ops, à valider Jules) + +### 1. DNS + +Dans OVH zone DNS `trans-former.fr` : + +``` +data A 178.104.106.195 TTL 600 +``` + +Attendre propagation (~5min). + +### 2. Volume Docker partagé n8n -> Caddy + +Le container n8n monte `vps-kit_n8n_data:/home/node/.n8n`. On va simplement lire un fichier dans ce volume depuis Caddy. + +Path source : `/var/lib/docker/volumes/vps-kit_n8n_data/_data/journal/journal.json` + +```bash +ssh vps-hetzner "mkdir -p /var/lib/docker/volumes/vps-kit_n8n_data/_data/journal && \ + chown 1000:1000 /var/lib/docker/volumes/vps-kit_n8n_data/_data/journal" +``` + +### 3. Caddyfile bloc + +Ajouter dans le Caddyfile (probablement `/etc/caddy/Caddyfile` ou `/opt/vps-kit/configs/Caddyfile`) : + +```caddy +data.trans-former.fr { + root * /var/lib/docker/volumes/vps-kit_n8n_data/_data/journal + file_server { + index journal.json + } + encode gzip + header { + Cache-Control "public, max-age=300" + Access-Control-Allow-Origin "https://trans-former.fr" + } + log { + output file /var/log/caddy/data.log + } +} +``` + +Backup + reload : +```bash +ssh vps-hetzner "cp /etc/caddy/Caddyfile /etc/caddy/Caddyfile.bak.$(date +%Y%m%d-%H%M%S) && \ + systemctl reload caddy && \ + systemctl status caddy --no-pager | head -10" +``` + +### 4. Workflow n8n + +Importer `docs/n8n-workflow-journal-aggregate.json` dans https://automate.trans-former.fr (UI -> Import from file). + +Activer le toggle, vérifier le cron (`0 3 * * *`). + +Configurer les credentials env n8n si besoin (Behold feed IDs) -> non bloquants si absents (workflow skip). + +### 5. Smoke test + +```bash +# Run manuel (UI n8n -> Execute Workflow) +ssh vps-hetzner "ls -la /var/lib/docker/volumes/vps-kit_n8n_data/_data/journal/" +curl -sf https://data.trans-former.fr/journal.json | jq '.counts' +``` + +## Backlog (hors scope PC6) + +- Trigger rebuild Astro Coolify webhook (PC8) +- Sources V2 : LinkedIn, Castopod, Blog, Substack (post handle confirmé) +- Storage archivage long-terme (V1 = écrasement quotidien) +- Real-time updates (V3) diff --git a/docs/n8n-workflow-journal-aggregate.json b/docs/n8n-workflow-journal-aggregate.json new file mode 100644 index 0000000..f9451df --- /dev/null +++ b/docs/n8n-workflow-journal-aggregate.json @@ -0,0 +1,152 @@ +{ + "name": "journal-aggregate", + "nodes": [ + { + "parameters": { + "rule": { + "interval": [ + { + "field": "cronExpression", + "expression": "0 3 * * *" + } + ] + } + }, + "id": "schedule-trigger", + "name": "Cron-3h", + "type": "n8n-nodes-base.scheduleTrigger", + "typeVersion": 1.1, + "position": [240, 320] + }, + { + "parameters": { + "url": "https://git.trans-former.fr/jules.atom", + "options": { + "response": { + "response": { + "responseFormat": "text" + } + }, + "timeout": 15000 + } + }, + "id": "fetch-gitea", + "name": "Fetch-gitea", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.1, + "position": [460, 200] + }, + { + "parameters": { + "url": "=https://feeds.behold.so/{{ $env.PUBLIC_BEHOLD_AEP || 'NOT_SET' }}", + "options": { + "response": { + "response": { + "neverError": true, + "responseFormat": "json" + } + }, + "timeout": 15000 + } + }, + "id": "fetch-behold-aep", + "name": "Fetch-behold-aep", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.1, + "position": [460, 320] + }, + { + "parameters": { + "url": "=https://feeds.behold.so/{{ $env.PUBLIC_BEHOLD_JULESNENY || 'NOT_SET' }}", + "options": { + "response": { + "response": { + "neverError": true, + "responseFormat": "json" + } + }, + "timeout": 15000 + } + }, + "id": "fetch-behold-julesneny", + "name": "Fetch-behold-julesneny", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.1, + "position": [460, 440] + }, + { + "parameters": { + "jsCode": "// Normalisation des 3 sources V1 vers le format JSON unifié\n// Sources : Gitéa Atom (XML), Behold @aep (JSON), Behold @julesneny (JSON)\n\nconst items = [];\n\n// ---- Helper parse Atom XML (Gitéa) ----\nfunction parseAtomGitea(xml) {\n const out = [];\n if (!xml || typeof xml !== 'string') return out;\n const entries = xml.split(//i).slice(1);\n for (const raw of entries) {\n const block = '' + raw.split(/<\\/entry>/i)[0] + '';\n const title = (block.match(/([\\s\\S]*?)<\\/title>/i) || [])[1] || '';\n const updated = (block.match(/<updated>([^<]+)<\\/updated>/i) || [])[1] || '';\n const link = (block.match(/<link[^>]*href=\"([^\"]+)\"/i) || [])[1] || '';\n const summary = (block.match(/<summary[^>]*>([\\s\\S]*?)<\\/summary>/i) || [])[1] || '';\n const id = (block.match(/<id>([^<]+)<\\/id>/i) || [])[1] || link || updated;\n if (!updated) continue;\n // Decode HTML entities basique + strip tags\n const decode = (s) => s\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/"/g, '\"')\n .replace(/"/g, '\"')\n .replace(/ /g, ' ')\n .replace(/&/g, '&')\n .replace(/<[^>]+>/g, ' ')\n .replace(/\\s+/g, ' ')\n .trim();\n const titreClean = decode(title);\n const extrait = decode(summary).slice(0, 280);\n out.push({\n id: 'gitea-' + (id.slice(0, 80) || updated),\n platform: 'gitea',\n hashtag: '#stack',\n date: new Date(updated).toISOString(),\n titre: titreClean.slice(0, 140),\n extrait,\n url: link,\n thumbnail: null,\n });\n }\n return out;\n}\n\n// ---- Récupère payloads depuis les 3 nodes amont ----\nlet giteaXml = '';\ntry {\n const giteaItems = $('Fetch-gitea').all();\n if (giteaItems.length && giteaItems[0].json) {\n // HTTP node typeVersion 4.1 met le body brut dans .data si responseFormat=text\n giteaXml = giteaItems[0].json.data || giteaItems[0].json.body || '';\n if (typeof giteaXml !== 'string') giteaXml = String(giteaXml);\n }\n} catch (e) {\n console.log('Gitéa fetch missing:', e.message);\n}\n\nlet beholdAep = [];\ntry {\n const aep = $('Fetch-behold-aep').all();\n if (aep.length && aep[0].json && Array.isArray(aep[0].json.posts)) {\n beholdAep = aep[0].json.posts;\n } else if (aep.length && Array.isArray(aep[0].json)) {\n beholdAep = aep[0].json;\n }\n} catch (e) {\n console.log('Behold AEP fetch missing:', e.message);\n}\n\nlet beholdJulesneny = [];\ntry {\n const j = $('Fetch-behold-julesneny').all();\n if (j.length && j[0].json && Array.isArray(j[0].json.posts)) {\n beholdJulesneny = j[0].json.posts;\n } else if (j.length && Array.isArray(j[0].json)) {\n beholdJulesneny = j[0].json;\n }\n} catch (e) {\n console.log('Behold Julesneny fetch missing:', e.message);\n}\n\n// ---- Normalisation ----\nfor (const it of parseAtomGitea(giteaXml)) items.push(it);\n\nfor (const post of beholdAep) {\n if (!post || !post.id) continue;\n const ts = post.timestamp || post.taken_at || post.date;\n const caption = (post.caption || post.captionWithEmojis || '').toString();\n items.push({\n id: 'insta-aep-' + post.id,\n platform: 'instagram',\n hashtag: '#aep-politique',\n date: ts ? new Date(ts).toISOString() : new Date().toISOString(),\n titre: caption.slice(0, 100) || '@aep.politique',\n extrait: caption.slice(0, 280),\n url: post.permalink || post.url || 'https://instagram.com/aep.politique',\n thumbnail: post.thumbnailUrl || post.mediaUrl || (post.sizes && post.sizes.medium && post.sizes.medium.mediaUrl) || null,\n });\n}\n\nfor (const post of beholdJulesneny) {\n if (!post || !post.id) continue;\n const ts = post.timestamp || post.taken_at || post.date;\n const caption = (post.caption || post.captionWithEmojis || '').toString();\n items.push({\n id: 'insta-julesneny-' + post.id,\n platform: 'instagram',\n hashtag: '#peinture',\n date: ts ? new Date(ts).toISOString() : new Date().toISOString(),\n titre: caption.slice(0, 100) || '@julesneny',\n extrait: caption.slice(0, 280),\n url: post.permalink || post.url || 'https://instagram.com/julesneny',\n thumbnail: post.thumbnailUrl || post.mediaUrl || (post.sizes && post.sizes.medium && post.sizes.medium.mediaUrl) || null,\n });\n}\n\n// ---- Tri desc + cap top 100 ----\nitems.sort((a, b) => b.date.localeCompare(a.date));\nconst top = items.slice(0, 100);\n\nconst counts = {\n total: top.length,\n gitea: top.filter((i) => i.platform === 'gitea').length,\n instagram: top.filter((i) => i.platform === 'instagram').length,\n};\n\nreturn [\n {\n json: {\n generatedAt: new Date().toISOString(),\n items: top,\n counts,\n },\n },\n];" + }, + "id": "normalise", + "name": "Normalise", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [720, 320] + }, + { + "parameters": { + "operation": "toJson", + "fieldName": "data", + "options": { + "format": true + } + }, + "id": "to-json", + "name": "To-json-string", + "type": "n8n-nodes-base.set", + "typeVersion": 3.4, + "position": [940, 320], + "notesInFlow": false, + "notes": "Transforme l'objet JS en string JSON pour Write Binary File" + }, + { + "parameters": { + "operation": "write", + "fileName": "/home/node/.n8n/journal/journal.json", + "dataPropertyName": "data", + "options": {} + }, + "id": "write-file", + "name": "Write-journal-json", + "type": "n8n-nodes-base.readWriteFile", + "typeVersion": 1, + "position": [1160, 320] + } + ], + "connections": { + "Cron-3h": { + "main": [ + [ + { "node": "Fetch-gitea", "type": "main", "index": 0 }, + { "node": "Fetch-behold-aep", "type": "main", "index": 0 }, + { "node": "Fetch-behold-julesneny", "type": "main", "index": 0 } + ] + ] + }, + "Fetch-gitea": { + "main": [[{ "node": "Normalise", "type": "main", "index": 0 }]] + }, + "Fetch-behold-aep": { + "main": [[{ "node": "Normalise", "type": "main", "index": 0 }]] + }, + "Fetch-behold-julesneny": { + "main": [[{ "node": "Normalise", "type": "main", "index": 0 }]] + }, + "Normalise": { + "main": [[{ "node": "To-json-string", "type": "main", "index": 0 }]] + }, + "To-json-string": { + "main": [[{ "node": "Write-journal-json", "type": "main", "index": 0 }]] + } + }, + "settings": { + "executionOrder": "v1", + "saveExecutionProgress": true, + "saveManualExecutions": true + }, + "tags": [ + { "name": "page-cerveau" }, + { "name": "PC6" } + ] +} diff --git a/public/data/journal.json b/public/data/journal.json new file mode 100644 index 0000000..35e044f --- /dev/null +++ b/public/data/journal.json @@ -0,0 +1,52 @@ +{ + "generatedAt": "2026-05-09T01:00:00Z", + "fallback": true, + "note": "Mock local — agrégateur n8n live pousse sur data.trans-former.fr/journal.json. Ce fichier sert de fallback dev tant que data.trans-former.fr DNS/Caddy ne sont pas en place.", + "items": [ + { + "id": "gitea-mock-pc6-feat", + "platform": "gitea", + "hashtag": "#stack", + "date": "2026-05-09T01:01:00Z", + "titre": "PC6 journal unifié + n8n agrégateur (mock)", + "extrait": "Composant JournalList Vue + workflow n8n cron 3h00. Sources V1 : Gitéa Atom + Behold @aep + Behold @julesneny.", + "url": "https://git.trans-former.fr/jules/astro-site-cerveau", + "thumbnail": null + }, + { + "id": "gitea-mock-pc3-mindmap", + "platform": "gitea", + "hashtag": "#stack", + "date": "2026-05-09T00:59:41Z", + "titre": "PC3 mindmap Carte O (D3 force-directed)", + "extrait": "Scrape AEP/Articles + tabs centre HAUT.", + "url": "https://git.trans-former.fr/jules/astro-site-cerveau/commit/32bdc9a", + "thumbnail": null + }, + { + "id": "insta-mock-aep-1", + "platform": "instagram", + "hashtag": "#aep-politique", + "date": "2026-05-07T18:30:00Z", + "titre": "Mock carrousel @aep.politique", + "extrait": "Placeholder carrousel manifeste écologie politique. Cron n8n live remplace ce mock par la vraie API Behold.", + "url": "https://instagram.com/aep.politique", + "thumbnail": null + }, + { + "id": "insta-mock-julesneny-1", + "platform": "instagram", + "hashtag": "#peinture", + "date": "2026-05-05T14:00:00Z", + "titre": "Mock peinture @julesneny", + "extrait": "Placeholder art / poésie / Corse. Cron n8n live remplace ce mock par la vraie API Behold.", + "url": "https://instagram.com/julesneny", + "thumbnail": null + } + ], + "counts": { + "total": 4, + "gitea": 2, + "instagram": 2 + } +} diff --git a/src/components/astro/ColJournal.astro b/src/components/astro/ColJournal.astro index b067d8a..ccbc089 100644 --- a/src/components/astro/ColJournal.astro +++ b/src/components/astro/ColJournal.astro @@ -1,6 +1,8 @@ --- -// ColJournal - colonne gauche : CTA Manifeste + Hashtags accordeon + Journal skeleton +// ColJournal - colonne gauche : CTA Manifeste + Hashtags accordeon + Journal (PC6) // 7 hashtags = 7 plateformes (cf delta 15 du PILOTE-PC.md) +import JournalList from '../vue/JournalList.vue'; + const hashtags = [ { tag: '#manifeste', plateforme: 'Blog trans-former.fr', canal: 'ecriture longue' }, { tag: '#building-public', plateforme: 'LinkedIn', canal: 'journal pro' }, @@ -55,10 +57,7 @@ const hashtags = [ <span class="text-xs text-neutral-400 font-normal">chrono</span> </h2> <div id="journal-list" class="space-y-3 text-sm"> - <!-- PC6 remplit ce slot via <JournalList client:visible /> --> - <p class="text-neutral-400 italic text-xs"> - Chargement du journal... - </p> + <JournalList client:visible /> </div> </section> </div> diff --git a/src/components/vue/JournalList.vue b/src/components/vue/JournalList.vue index fe3f4be..2e3a9f5 100644 --- a/src/components/vue/JournalList.vue +++ b/src/components/vue/JournalList.vue @@ -1,9 +1,168 @@ <script setup lang="ts"> -// Placeholder journal list — PC6 lit public/data/journal.json +import { ref, computed, onMounted, onUnmounted } from 'vue' + +interface JournalItem { + id: string + platform: 'substack' | 'gitea' | 'github' | 'instagram' | 'castopod' | 'blog' | 'linkedin' + hashtag: string + date: string + titre: string + extrait: string + url: string + thumbnail: string | null +} + +interface JournalPayload { + generatedAt: string + items: JournalItem[] + counts?: Record<string, number> +} + +const items = ref<JournalItem[]>([]) +const filters = ref<Record<string, boolean>>({}) +const loading = ref(true) +const errored = ref(false) +const empty = ref(false) + +const JOURNAL_URL = + (import.meta as unknown as { env: Record<string, string | undefined> }).env + .PUBLIC_JOURNAL_URL || 'https://data.trans-former.fr/journal.json' + +const STORAGE_KEY = 'tf-hashtag-filters' + +const loadFilters = () => { + try { + filters.value = JSON.parse(localStorage.getItem(STORAGE_KEY) || '{}') + } catch { + filters.value = {} + } +} + +const onFilterChange = (e: Event) => { + const ce = e as CustomEvent + if (ce.detail && typeof ce.detail === 'object') { + filters.value = { ...ce.detail } + } +} + +onMounted(async () => { + loadFilters() + window.addEventListener('hashtag-filter-change', onFilterChange as EventListener) + try { + const res = await fetch(JOURNAL_URL, { cache: 'no-store' }) + if (!res.ok) { + errored.value = true + return + } + const data = (await res.json()) as JournalPayload + items.value = Array.isArray(data.items) ? data.items : [] + if (items.value.length === 0) empty.value = true + } catch (e) { + errored.value = true + console.error('JournalList fetch failed', e) + } finally { + loading.value = false + } +}) + +onUnmounted(() => { + window.removeEventListener('hashtag-filter-change', onFilterChange as EventListener) +}) + +const visibleItems = computed(() => { + // Tous les filtres faux = aucun affiche ; tous true ou aucune cle = tout afficher + const keys = Object.keys(filters.value) + if (keys.length === 0) return items.value + const activeKeys = keys.filter((k) => filters.value[k]) + // Si l'utilisateur a explicitement décoché tous les hashtags, on n'affiche rien + if (activeKeys.length === 0 && keys.some((k) => filters.value[k] === false)) return [] + if (activeKeys.length === 0) return items.value + return items.value.filter((it) => activeKeys.includes(it.hashtag)) +}) + +const formatDate = (iso: string) => { + try { + const d = new Date(iso) + if (isNaN(d.getTime())) return '' + return `${String(d.getDate()).padStart(2, '0')}/${String(d.getMonth() + 1).padStart(2, '0')}` + } catch { + return '' + } +} + +const platformLabel = (p: string) => { + switch (p) { + case 'gitea': + case 'github': + return 'code' + case 'substack': + return 'substack' + case 'instagram': + return 'insta' + case 'castopod': + return 'pod' + case 'blog': + return 'blog' + case 'linkedin': + return 'linkedin' + default: + return p + } +} </script> <template> - <div class="h-full w-full flex items-center justify-center text-sm text-neutral-400"> - Journal list placeholder (PC6) + <div class="space-y-3"> + <p v-if="loading" class="text-neutral-400 italic text-xs"> + Chargement du journal... + </p> + <p v-else-if="errored" class="text-neutral-400 italic text-xs"> + Journal en cours d'agregation. Reviens dans quelques heures. + </p> + <p v-else-if="empty" class="text-neutral-400 italic text-xs"> + Aucun item agrege. Le cron nocturne tourne a 3h. + </p> + <p + v-else-if="!visibleItems.length" + class="text-neutral-400 italic text-xs" + > + Aucun item ; ajuste les filtres. + </p> + <article + v-for="item in visibleItems" + :key="item.id" + class="border-b border-neutral-100 pb-3 last:border-b-0" + > + <a + :href="item.url" + target="_blank" + rel="noopener noreferrer" + class="block group" + > + <div class="flex items-baseline gap-2 text-[11px] text-neutral-500 mb-1"> + <time>{{ formatDate(item.date) }}</time> + <span class="font-mono text-neutral-700">{{ item.hashtag }}</span> + <span class="text-neutral-400">{{ platformLabel(item.platform) }}</span> + </div> + <h4 + class="text-sm text-neutral-900 group-hover:text-neutral-600 leading-snug" + > + {{ item.titre }} + </h4> + <p + v-if="item.extrait" + class="text-xs text-neutral-500 mt-1 line-clamp-2" + > + {{ item.extrait }} + </p> + <img + v-if="item.thumbnail" + :src="item.thumbnail" + :alt="item.titre" + loading="lazy" + class="mt-2 w-full max-h-32 object-cover rounded" + /> + </a> + </article> </div> </template> From be7fc09085572768cd03bd4cdadcf34fcfb334f1 Mon Sep 17 00:00:00 2001 From: Jules Neny <jules@trans-former.fr> Date: Sat, 9 May 2026 01:22:01 +0200 Subject: [PATCH 07/36] feat: PC7 chatbot V1 onglet centre HAUT + endpoint Astro proxy SSR - ChatbotV2.vue : Vue island, thread chat (input + messages bot/user), persistance sessionStorage, bandeau beta '120 fiches AEP, RAG-PE bientot', gestion erreurs 429/502/504 ; pas de streaming ni markdown V1 - /api/chatbot.ts : endpoint Astro server proxy POST vers CHATBOT_UPSTREAM (default https://aep.trans-former.fr/api/chatbot), timeout 25s, body { question, history } -> upstream classique chatbot AEP Mistral Small - astro.config.mjs : output 'server' + adapter @astrojs/node standalone (Astro 6 a supprime mode hybrid ; on opt-in prerender sur les pages) - Toutes les pages publiques (index, manifeste, manifeste/commander, a-propos, mentions-legales) ont 'export const prerender = true' - ColCentre.astro : remplace ChatbotPlaceholder par ChatbotV2 dans le tab - .env.example : ajoute CHATBOT_UPSTREAM (V1.5 = switch LightRAG-PE 1 ligne) Decision V1 : endpoint AEP /api/chatbot (classique, repond bien) au lieu de /api/chatbot-v2 qui retourne v2_ready=false ('base vectorielle en cours'). Bandeau beta reste valide ; switch v2 quand ready cote AEP via env var. Note PC8 deploy : Coolify doit booter avec 'node ./dist/server/entry.mjs' (SSR Node standalone) au lieu de servir dist/client/ static. Test end-to-end OK : SSR boot port 4399 + curl POST /api/chatbot -> reponse_texte 800+ chars de l'AEP backend. --- .env.example | 5 + astro.config.mjs | 7 +- package-lock.json | 191 ++++++++++ package.json | 1 + public/data/carte-o.json | 502 +++++++++++++-------------- src/components/astro/ColCentre.astro | 6 +- src/components/vue/ChatbotV2.vue | 187 ++++++++++ src/pages/a-propos.astro | 2 + src/pages/api/chatbot.ts | 65 ++++ src/pages/index.astro | 2 + src/pages/manifeste.astro | 2 + src/pages/manifeste/commander.astro | 2 + src/pages/mentions-legales.astro | 2 + 13 files changed, 719 insertions(+), 255 deletions(-) create mode 100644 src/components/vue/ChatbotV2.vue create mode 100644 src/pages/api/chatbot.ts diff --git a/.env.example b/.env.example index 031e98f..a997cf8 100644 --- a/.env.example +++ b/.env.example @@ -8,3 +8,8 @@ PUBLIC_BEHOLD_JULESNENY= # Journal unifie (PC6) - URL JSON agrege par n8n cron nocturne # Override en local : pointer vers un mock /public/data/journal.json par exemple PUBLIC_JOURNAL_URL=https://data.trans-former.fr/journal.json + +# Chatbot upstream (PC7) - URL backend chatbot AEP +# V1 : chatbot AEP classique (Mistral Small + 120 fiches) +# V1.5 : switch vers LightRAG-PE (1 ligne) +CHATBOT_UPSTREAM=https://aep.trans-former.fr/api/chatbot diff --git a/astro.config.mjs b/astro.config.mjs index dac3789..6a9ed12 100644 --- a/astro.config.mjs +++ b/astro.config.mjs @@ -1,10 +1,15 @@ // @ts-check import { defineConfig } from 'astro/config'; import vue from '@astrojs/vue'; +import node from '@astrojs/node'; import tailwindcss from '@tailwindcss/vite'; -// https://astro.build/config +// PC7 — bascule SSR (mode 'server' Astro 6) pour endpoint /api/chatbot proxy. +// Toutes les pages publiques restent statiques via `export const prerender = true`. +// Coolify deploy (PC8) : `node ./dist/server/entry.mjs` (Node adapter standalone). export default defineConfig({ + output: 'server', + adapter: node({ mode: 'standalone' }), integrations: [vue()], vite: { plugins: [tailwindcss()], diff --git a/package-lock.json b/package-lock.json index f028bf1..5d06892 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,7 @@ "name": "astro-site-cerveau", "version": "0.0.1", "dependencies": { + "@astrojs/node": "^10.1.0", "@astrojs/vue": "^6.0.1", "@tailwindcss/vite": "^4.2.4", "@types/d3": "^7.4.3", @@ -67,6 +68,20 @@ "vfile": "^6.0.3" } }, + "node_modules/@astrojs/node": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/@astrojs/node/-/node-10.1.0.tgz", + "integrity": "sha512-4/2oqUTQ71UQ8+xX249T4l/d0/YkC5ssOVl4R2yQO7Wg4mOnvsq9Z9iaTkWAyElg3lqZq7XRNCEXCmDNiYcW1A==", + "license": "MIT", + "dependencies": { + "@astrojs/internal-helpers": "0.9.0", + "send": "^1.2.1", + "server-destroy": "^1.0.1" + }, + "peerDependencies": { + "astro": "^6.3.0" + } + }, "node_modules/@astrojs/prism": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/@astrojs/prism/-/prism-4.0.1.tgz", @@ -3853,6 +3868,15 @@ "robust-predicates": "^3.0.2" } }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/dequal": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", @@ -3981,6 +4005,12 @@ "node": ">=4" } }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, "node_modules/electron-to-chromium": { "version": "1.5.352", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.352.tgz", @@ -4015,6 +4045,15 @@ "vue": "^3.2.37" } }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/enhanced-resolve": { "version": "5.21.2", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.21.2.tgz", @@ -4105,6 +4144,12 @@ "node": ">=6" } }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, "node_modules/escape-string-regexp": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", @@ -4136,6 +4181,15 @@ "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", "license": "MIT" }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/eventemitter3": { "version": "5.0.4", "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", @@ -4268,6 +4322,15 @@ "node": ">=20" } }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -4609,6 +4672,26 @@ "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", "license": "BSD-2-Clause" }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/iconv-lite": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", @@ -4630,6 +4713,12 @@ "node": ">= 4" } }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, "node_modules/internmap": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", @@ -5972,6 +6061,31 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/mrmime": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", @@ -6093,6 +6207,18 @@ "integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==", "license": "MIT" }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/oniguruma-parser": { "version": "0.12.2", "resolved": "https://registry.npmjs.org/oniguruma-parser/-/oniguruma-parser-0.12.2.tgz", @@ -6316,6 +6442,15 @@ "integrity": "sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA==", "license": "MIT" }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/readdirp": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz", @@ -6712,6 +6847,44 @@ "node": ">=10" } }, + "node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/server-destroy": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/server-destroy/-/server-destroy-1.0.1.tgz", + "integrity": "sha512-rb+9B5YBIEzYcD6x2VKidaa+cqYBJQKnU4oe4E3ANwRRN56yk/ua1YCJT1n21NTS8w6CcOclAKNP3PhdCXKYtQ==", + "license": "ISC" + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, "node_modules/sharp": { "version": "0.34.5", "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", @@ -6845,6 +7018,15 @@ "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", "license": "BSD-3-Clause" }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/stringify-entities": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", @@ -6964,6 +7146,15 @@ "node": ">=8.0" } }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, "node_modules/totalist": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", diff --git a/package.json b/package.json index 27fe719..8ebab62 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "build:carte-o": "node scripts/build-carte-o.js" }, "dependencies": { + "@astrojs/node": "^10.1.0", "@astrojs/vue": "^6.0.1", "@tailwindcss/vite": "^4.2.4", "@types/d3": "^7.4.3", diff --git a/public/data/carte-o.json b/public/data/carte-o.json index 481c7e2..1389295 100644 --- a/public/data/carte-o.json +++ b/public/data/carte-o.json @@ -1,6 +1,6 @@ { "meta": { - "generated": "2026-05-08T22:56:53.553Z", + "generated": "2026-05-08T23:20:14.828Z", "source": "AEP/Articles", "nodeCount": 84, "edgeCount": 94, @@ -25,19 +25,19 @@ "TR - renovation-energetique - website pro.md": 1, "AEP ARTICLES, BROUILLON": 6, "Livre - le nouveau contrat social": 2, + "AEP agriculture": 1, "AEP archi": 4, "AEP déconstruction": 9, - "AEP agriculture": 1, - "AEP géopolitique": 3, "AEP education": 4, - "AEP justice": 3, - "AEP IA": 14, - "AEP nouveaux récits": 9, "AEP histoire": 7, + "AEP IA": 14, + "AEP géopolitique": 3, + "AEP justice": 3, "AEP piraterie (script formation)": 2, + "AEP nouveaux récits": 9, "AEP politique": 3, - "AEP santé": 6, "AEP regénération": 3, + "AEP santé": 6, "AEP spiritualité": 1, "AEP système économique": 4 } @@ -142,6 +142,15 @@ "theme": "Livre - le nouveau contrat social", "path": "Livre - le nouveau contrat social/AEP - article fiscalite contrat social.md" }, + { + "id": "aep-agriculture", + "label": "AEP Agriculture", + "family": "methode", + "intention": "Contrat social : les agriculteurs, àvalorises par autre chose que les produits qu’il créent. Besoin que ça soit le moins cher possible", + "slug": "aep-agriculture", + "theme": "AEP agriculture", + "path": "AEP ARTICLES, BROUILLON/AEP agriculture/AEP Agriculture.md" + }, { "id": "aep-beton-critique", "label": "AEP Béton - critique", @@ -223,42 +232,6 @@ "theme": "AEP déconstruction", "path": "AEP ARTICLES, BROUILLON/AEP déconstruction/AEP humour - manifeste brouillon.md" }, - { - "id": "aep-agriculture", - "label": "AEP Agriculture", - "family": "methode", - "intention": "Contrat social : les agriculteurs, àvalorises par autre chose que les produits qu’il créent. Besoin que ça soit le moins cher possible", - "slug": "aep-agriculture", - "theme": "AEP agriculture", - "path": "AEP ARTICLES, BROUILLON/AEP agriculture/AEP Agriculture.md" - }, - { - "id": "2025-07-05-juan-branco-thinker-view-ep-2-30-06-2025", - "label": "2025-07-05 Juan branco - thinker view - ep 2 30-06-2025", - "family": "penseur", - "intention": "Juan branco - thinker view - ep 2, 30/06/2025", - "slug": "2025-07-05-juan-branco-thinker-view-ep-2-30-06-2025", - "theme": "AEP géopolitique", - "path": "AEP ARTICLES, BROUILLON/AEP géopolitique/2025-07-05 Juan branco - thinker view - ep 2 30-06-2025.md" - }, - { - "id": "aep-israel-palestine-conflit-brut", - "label": "AEP Israel - Palestine, conflit (brut)", - "family": "penseur", - "intention": "Depuis l’attaque du 7 octobre 2023 par le Hamas, qui a fait environ 1 200 morts israéliens, l'État d’Israël, dirigé par Benyamin Netanyahou, a lancé une campagne militaire d’une intensité sans précédent contre la bande d", - "slug": "aep-israel-palestine-conflit-brut", - "theme": "AEP géopolitique", - "path": "AEP ARTICLES, BROUILLON/AEP géopolitique/AEP Israel - Palestine, conflit (brut).md" - }, - { - "id": "aep-politique-energie-fr", - "label": "AEP Politique Énergie FR", - "family": "methode", - "intention": "MOC : Architecture technique d'ATISPOLITIQUETRANSMETTRE Source : Discussion Álvaro Projet: PFE - Infolettre AEP! AEP émerger une nouvelle pratiqueAEP Politique Énergie FR Tags : #contenu/infolettre #contenu/manifeste #co", - "slug": "aep-politique-energie-fr", - "theme": "AEP géopolitique", - "path": "AEP ARTICLES, BROUILLON/AEP géopolitique/AEP Politique Énergie FR.md" - }, { "id": "aep-cooperation", "label": "AEP Coopération", @@ -295,123 +268,6 @@ "theme": "AEP education", "path": "AEP ARTICLES, BROUILLON/AEP education/Mémorisation - discussion train.md" }, - { - "id": "aep-justice-securite", - "label": "AEP justice & sécurité", - "family": "penseur", - "intention": "MOC : POLITIQUE-AEP 1 Source : divers Projets : AEP articles, idées en gestation-AEP tableau des articles Tags : #contenu/article Date : 2025-10-31 Justice/droit Juan Branco réformes structurelles https://www.youtube.com", - "slug": "aep-justice-securite", - "theme": "AEP justice", - "path": "AEP ARTICLES, BROUILLON/AEP justice/AEP justice & sécurité.md" - }, - { - "id": "crepuscule-juan-branco-itw", - "label": "Crépuscule - Juan branco - ITW", - "family": "penseur", - "intention": "Crépuscule - Juan branco - podcast - thinkerview", - "slug": "crepuscule-juan-branco-itw", - "theme": "AEP justice", - "path": "AEP ARTICLES, BROUILLON/AEP justice/Crépuscule - Juan branco - ITW.md" - }, - { - "id": "narcotrafic", - "label": "Narcotrafic", - "family": "collectif", - "intention": "Ces frontières sont valorisées dans les médias par la dramaturgie de postures martiales mettant en valeur des politiciens, doublées de menaces a l'ordre social, et a la stigmatisation de groupes sociaux (racailles, bande", - "slug": "narcotrafic", - "theme": "AEP justice", - "path": "AEP ARTICLES, BROUILLON/AEP justice/Narcotrafic.md" - }, - { - "id": "2026-03-11-aep-ia-usage-sage-intention", - "label": "2026-03-11 AEP IA usage sage intention", - "family": "ressource", - "intention": "MOC : Transcriptions Projets : AEP - IA - usage sage Date : 2026-03-11 Durée : 9m 18s · groq récap de la conversation sur l'IA super cool qu'on a eue avec Issa, envie de poser ça comme base de la future formation IA de N", - "slug": "2026-03-11-aep-ia-usage-sage-intention", - "theme": "AEP IA", - "path": "AEP ARTICLES, BROUILLON/AEP IA/2026-03-11 AEP IA usage sage intention.md" - }, - { - "id": "aep-ia-usage-sage", - "label": "AEP - IA - usage sage", - "family": "methode", - "intention": "MOC : ORGANISATION-PFE - Infolettre AEP Source : Ines Lee [\"oops i become codependent on IA\"](https://ineslee.substack.com/p/ai-codependence) + Tyler Austin Harper, The Atlantic, \"[what happens when people don't undersan", - "slug": "aep-ia-usage-sage", - "theme": "AEP IA", - "path": "AEP ARTICLES, BROUILLON/AEP IA/AEP - IA - usage sage.md" - }, - { - "id": "anthropic-revolution-ia-2026", - "label": "Anthropic, révolution IA 2026", - "family": "concept", - "intention": "révolution d'anthropic via cowork, claude code & opus.", - "slug": "anthropic-revolution-ia-2026", - "theme": "AEP IA", - "path": "AEP ARTICLES, BROUILLON/AEP IA/Anthropic, révolution IA 2026.md" - }, - { - "id": "ia-usage-sage-sven-et-lucie", - "label": "IA usage sage Sven et Lucie", - "family": "ressource", - "intention": "Voici la retranscription d'un article que j'aimerais écrire sur l'usage de l'IA. Et c'était un rendez-vous la semaine dernière avec Zden et Lucie, qui nous a permis pendant deux heures de traverser des sujets importants ", - "slug": "ia-usage-sage-sven-et-lucie", - "theme": "AEP IA", - "path": "AEP ARTICLES, BROUILLON/AEP IA/IA usage sage Sven et Lucie.md" - }, - { - "id": "manifeste-anti-iag", - "label": "Manifeste anti-IAg", - "family": "ressource", - "intention": "[Vous pouvez signer ce manifeste ([version pdf ici](https://atecopol.hypotheses.org/files/2025/12/ManifesteObjectionConscienceIAg.pdf)) dans [ce formulaire](https://framaforms.org/face-a-lia-generative-lobjection-de-cons", - "slug": "manifeste-anti-iag", - "theme": "AEP IA", - "path": "AEP ARTICLES, BROUILLON/AEP IA/Manifeste anti-IAg.md" - }, - { - "id": "aep-culture-dominante-analyse-series", - "label": "AEP culture dominante (analyse séries)", - "family": "concept", - "intention": "\"grands récits\" - star wars - le seigneur des anneaux - dune !Post Dune- avis critique.m4a - harry potter - eragon (le livre plus que le film) - one piece - naruto - Arcane : !Arcanes feedback série.m4a = parallèle avec ", - "slug": "aep-culture-dominante-analyse-series", - "theme": "AEP nouveaux récits", - "path": "AEP ARTICLES, BROUILLON/AEP nouveaux récits/AEP culture dominante (analyse séries).md" - }, - { - "id": "aep-medias-la-pensee-critique", - "label": "AEP médias & la pensée critique", - "family": "methode", - "intention": "MOC : POLITIQUE-PFE - Infolettre AEP Source : https://mythodologie.fr/ Projets : NAAV - apprendre a apprendre-cours de transformation urbaine- Tags : Date : 2025-06-19 une note expliquant les bases de l'intelligence crit", - "slug": "aep-medias-la-pensee-critique", - "theme": "AEP nouveaux récits", - "path": "AEP ARTICLES, BROUILLON/AEP nouveaux récits/AEP médias & la pensée critique.md" - }, - { - "id": "aep-prospective", - "label": "AEP prospective", - "family": "penseur", - "intention": "En lien avec Tout pour tout le monde - ITW - prospective", - "slug": "aep-prospective", - "theme": "AEP nouveaux récits", - "path": "AEP ARTICLES, BROUILLON/AEP nouveaux récits/AEP prospective.md" - }, - { - "id": "aep-ecrire-de-nouveaux-recits", - "label": "AEP écrire de nouveaux récits", - "family": "ressource", - "intention": "MOC : POLITIQUETRANSMETTRE- ART-FACILITATION Projets : Stratégie création de contenu PFE - Infolettre AEP AEP éducation-L'art du récit (storytelling) Tags : #contenu/article Date : 2024-03-23 # Projets d'écriture 1. LIVR", - "slug": "aep-ecrire-de-nouveaux-recits", - "theme": "AEP nouveaux récits", - "path": "AEP ARTICLES, BROUILLON/AEP nouveaux récits/AEP écrire de nouveaux récits.md" - }, - { - "id": "archetypes-eneagramme", - "label": "Archétypes - énéagramme", - "family": "collectif", - "intention": "MOC : LITTERATURE-écrivain-ART-SPIRITUALITE Source : Annif Flo 44 ans - orga Projets : AEP écrire de nouveaux récits-LIVRE - le nouveau contrat social Tags : Date : 2025-10-31 note sur l'étude des personnages d'un récit/", - "slug": "archetypes-eneagramme", - "theme": "AEP nouveaux récits", - "path": "AEP ARTICLES, BROUILLON/AEP nouveaux récits/Archétypes - énéagramme.md" - }, { "id": "aep-histoire", "label": "AEP Histoire", @@ -475,6 +331,105 @@ "theme": "AEP histoire", "path": "AEP ARTICLES, BROUILLON/AEP histoire/L’empire n’a jamais pris fin 6 -.md" }, + { + "id": "2026-03-11-aep-ia-usage-sage-intention", + "label": "2026-03-11 AEP IA usage sage intention", + "family": "ressource", + "intention": "MOC : Transcriptions Projets : AEP - IA - usage sage Date : 2026-03-11 Durée : 9m 18s · groq récap de la conversation sur l'IA super cool qu'on a eue avec Issa, envie de poser ça comme base de la future formation IA de N", + "slug": "2026-03-11-aep-ia-usage-sage-intention", + "theme": "AEP IA", + "path": "AEP ARTICLES, BROUILLON/AEP IA/2026-03-11 AEP IA usage sage intention.md" + }, + { + "id": "aep-ia-usage-sage", + "label": "AEP - IA - usage sage", + "family": "methode", + "intention": "MOC : ORGANISATION-PFE - Infolettre AEP Source : Ines Lee [\"oops i become codependent on IA\"](https://ineslee.substack.com/p/ai-codependence) + Tyler Austin Harper, The Atlantic, \"[what happens when people don't undersan", + "slug": "aep-ia-usage-sage", + "theme": "AEP IA", + "path": "AEP ARTICLES, BROUILLON/AEP IA/AEP - IA - usage sage.md" + }, + { + "id": "anthropic-revolution-ia-2026", + "label": "Anthropic, révolution IA 2026", + "family": "concept", + "intention": "révolution d'anthropic via cowork, claude code & opus.", + "slug": "anthropic-revolution-ia-2026", + "theme": "AEP IA", + "path": "AEP ARTICLES, BROUILLON/AEP IA/Anthropic, révolution IA 2026.md" + }, + { + "id": "ia-usage-sage-sven-et-lucie", + "label": "IA usage sage Sven et Lucie", + "family": "ressource", + "intention": "Voici la retranscription d'un article que j'aimerais écrire sur l'usage de l'IA. Et c'était un rendez-vous la semaine dernière avec Zden et Lucie, qui nous a permis pendant deux heures de traverser des sujets importants ", + "slug": "ia-usage-sage-sven-et-lucie", + "theme": "AEP IA", + "path": "AEP ARTICLES, BROUILLON/AEP IA/IA usage sage Sven et Lucie.md" + }, + { + "id": "manifeste-anti-iag", + "label": "Manifeste anti-IAg", + "family": "ressource", + "intention": "[Vous pouvez signer ce manifeste ([version pdf ici](https://atecopol.hypotheses.org/files/2025/12/ManifesteObjectionConscienceIAg.pdf)) dans [ce formulaire](https://framaforms.org/face-a-lia-generative-lobjection-de-cons", + "slug": "manifeste-anti-iag", + "theme": "AEP IA", + "path": "AEP ARTICLES, BROUILLON/AEP IA/Manifeste anti-IAg.md" + }, + { + "id": "2025-07-05-juan-branco-thinker-view-ep-2-30-06-2025", + "label": "2025-07-05 Juan branco - thinker view - ep 2 30-06-2025", + "family": "penseur", + "intention": "Juan branco - thinker view - ep 2, 30/06/2025", + "slug": "2025-07-05-juan-branco-thinker-view-ep-2-30-06-2025", + "theme": "AEP géopolitique", + "path": "AEP ARTICLES, BROUILLON/AEP géopolitique/2025-07-05 Juan branco - thinker view - ep 2 30-06-2025.md" + }, + { + "id": "aep-israel-palestine-conflit-brut", + "label": "AEP Israel - Palestine, conflit (brut)", + "family": "penseur", + "intention": "Depuis l’attaque du 7 octobre 2023 par le Hamas, qui a fait environ 1 200 morts israéliens, l'État d’Israël, dirigé par Benyamin Netanyahou, a lancé une campagne militaire d’une intensité sans précédent contre la bande d", + "slug": "aep-israel-palestine-conflit-brut", + "theme": "AEP géopolitique", + "path": "AEP ARTICLES, BROUILLON/AEP géopolitique/AEP Israel - Palestine, conflit (brut).md" + }, + { + "id": "aep-politique-energie-fr", + "label": "AEP Politique Énergie FR", + "family": "methode", + "intention": "MOC : Architecture technique d'ATISPOLITIQUETRANSMETTRE Source : Discussion Álvaro Projet: PFE - Infolettre AEP! AEP émerger une nouvelle pratiqueAEP Politique Énergie FR Tags : #contenu/infolettre #contenu/manifeste #co", + "slug": "aep-politique-energie-fr", + "theme": "AEP géopolitique", + "path": "AEP ARTICLES, BROUILLON/AEP géopolitique/AEP Politique Énergie FR.md" + }, + { + "id": "aep-justice-securite", + "label": "AEP justice & sécurité", + "family": "penseur", + "intention": "MOC : POLITIQUE-AEP 1 Source : divers Projets : AEP articles, idées en gestation-AEP tableau des articles Tags : #contenu/article Date : 2025-10-31 Justice/droit Juan Branco réformes structurelles https://www.youtube.com", + "slug": "aep-justice-securite", + "theme": "AEP justice", + "path": "AEP ARTICLES, BROUILLON/AEP justice/AEP justice & sécurité.md" + }, + { + "id": "crepuscule-juan-branco-itw", + "label": "Crépuscule - Juan branco - ITW", + "family": "penseur", + "intention": "Crépuscule - Juan branco - podcast - thinkerview", + "slug": "crepuscule-juan-branco-itw", + "theme": "AEP justice", + "path": "AEP ARTICLES, BROUILLON/AEP justice/Crépuscule - Juan branco - ITW.md" + }, + { + "id": "narcotrafic", + "label": "Narcotrafic", + "family": "collectif", + "intention": "Ces frontières sont valorisées dans les médias par la dramaturgie de postures martiales mettant en valeur des politiciens, doublées de menaces a l'ordre social, et a la stigmatisation de groupes sociaux (racailles, bande", + "slug": "narcotrafic", + "theme": "AEP justice", + "path": "AEP ARTICLES, BROUILLON/AEP justice/Narcotrafic.md" + }, { "id": "creation-de-script-template", "label": "Création de script - template", @@ -493,6 +448,51 @@ "theme": "AEP piraterie (script formation)", "path": "AEP ARTICLES, BROUILLON/AEP piraterie (script formation)/methodologie_developpement.md" }, + { + "id": "aep-culture-dominante-analyse-series", + "label": "AEP culture dominante (analyse séries)", + "family": "concept", + "intention": "\"grands récits\" - star wars - le seigneur des anneaux - dune !Post Dune- avis critique.m4a - harry potter - eragon (le livre plus que le film) - one piece - naruto - Arcane : !Arcanes feedback série.m4a = parallèle avec ", + "slug": "aep-culture-dominante-analyse-series", + "theme": "AEP nouveaux récits", + "path": "AEP ARTICLES, BROUILLON/AEP nouveaux récits/AEP culture dominante (analyse séries).md" + }, + { + "id": "aep-medias-la-pensee-critique", + "label": "AEP médias & la pensée critique", + "family": "methode", + "intention": "MOC : POLITIQUE-PFE - Infolettre AEP Source : https://mythodologie.fr/ Projets : NAAV - apprendre a apprendre-cours de transformation urbaine- Tags : Date : 2025-06-19 une note expliquant les bases de l'intelligence crit", + "slug": "aep-medias-la-pensee-critique", + "theme": "AEP nouveaux récits", + "path": "AEP ARTICLES, BROUILLON/AEP nouveaux récits/AEP médias & la pensée critique.md" + }, + { + "id": "aep-prospective", + "label": "AEP prospective", + "family": "penseur", + "intention": "En lien avec Tout pour tout le monde - ITW - prospective", + "slug": "aep-prospective", + "theme": "AEP nouveaux récits", + "path": "AEP ARTICLES, BROUILLON/AEP nouveaux récits/AEP prospective.md" + }, + { + "id": "aep-ecrire-de-nouveaux-recits", + "label": "AEP écrire de nouveaux récits", + "family": "ressource", + "intention": "MOC : POLITIQUETRANSMETTRE- ART-FACILITATION Projets : Stratégie création de contenu PFE - Infolettre AEP AEP éducation-L'art du récit (storytelling) Tags : #contenu/article Date : 2024-03-23 # Projets d'écriture 1. LIVR", + "slug": "aep-ecrire-de-nouveaux-recits", + "theme": "AEP nouveaux récits", + "path": "AEP ARTICLES, BROUILLON/AEP nouveaux récits/AEP écrire de nouveaux récits.md" + }, + { + "id": "archetypes-eneagramme", + "label": "Archétypes - énéagramme", + "family": "collectif", + "intention": "MOC : LITTERATURE-écrivain-ART-SPIRITUALITE Source : Annif Flo 44 ans - orga Projets : AEP écrire de nouveaux récits-LIVRE - le nouveau contrat social Tags : Date : 2025-10-31 note sur l'étude des personnages d'un récit/", + "slug": "archetypes-eneagramme", + "theme": "AEP nouveaux récits", + "path": "AEP ARTICLES, BROUILLON/AEP nouveaux récits/Archétypes - énéagramme.md" + }, { "id": "5-chantiers-mamdani-nyc", "label": "5 chantiers Mamdani NYC", @@ -520,6 +520,33 @@ "theme": "AEP politique", "path": "AEP ARTICLES, BROUILLON/AEP politique/AEP politique - état des lieux.md" }, + { + "id": "aep-regen-fertival", + "label": "AEP - Regen - fertival", + "family": "methode", + "intention": "MOC : FACILITATION-Agroforesterie- Source : Sacha Gouttenoire Projets : Fertival 2025-PFE - Infolettre AEP Tags : Date : 2025-05-02 10.03.23 article phare sur la pratique de la regénération naturelle, une pratique illust", + "slug": "aep-regen-fertival", + "theme": "AEP regénération", + "path": "AEP ARTICLES, BROUILLON/AEP regénération/! AEP - Regen - fertival.md" + }, + { + "id": "aep-emerger-une-nouvelle-pratique", + "label": "AEP émerger une nouvelle pratique", + "family": "methode", + "intention": "MOC : POLITIQUEArchitecture technique d'ATIS Source : Projets : faire émerger une nouvelle pratique de l'architecture Tags : Date : 2024-11-11 projet principal d'énonciation d'une théorie de la transformation ! Structure", + "slug": "aep-emerger-une-nouvelle-pratique", + "theme": "AEP regénération", + "path": "AEP ARTICLES, BROUILLON/AEP regénération/! AEP émerger une nouvelle pratique.md" + }, + { + "id": "aep-mdcs-medecine-du-corps-social", + "label": "AEP mdcs - Médecine du corps social", + "family": "methode", + "intention": "Note capitalisme socialisme 3ème voie ? Communes, local briser pyramide colonialiste, troisième voie, rendre local l'indispensable, et global le spécifique.", + "slug": "aep-mdcs-medecine-du-corps-social", + "theme": "AEP regénération", + "path": "AEP ARTICLES, BROUILLON/AEP regénération/AEP mdcs - Médecine du corps social.md" + }, { "id": "aep-fiscalite-contrat-social", "label": "AEP fiscalité - contrat social", @@ -574,33 +601,6 @@ "theme": "AEP santé", "path": "AEP ARTICLES, BROUILLON/AEP santé/Qualité de vie EU vs USA - art. mindvalley.md" }, - { - "id": "aep-regen-fertival", - "label": "AEP - Regen - fertival", - "family": "methode", - "intention": "MOC : FACILITATION-Agroforesterie- Source : Sacha Gouttenoire Projets : Fertival 2025-PFE - Infolettre AEP Tags : Date : 2025-05-02 10.03.23 article phare sur la pratique de la regénération naturelle, une pratique illust", - "slug": "aep-regen-fertival", - "theme": "AEP regénération", - "path": "AEP ARTICLES, BROUILLON/AEP regénération/! AEP - Regen - fertival.md" - }, - { - "id": "aep-emerger-une-nouvelle-pratique", - "label": "AEP émerger une nouvelle pratique", - "family": "methode", - "intention": "MOC : POLITIQUEArchitecture technique d'ATIS Source : Projets : faire émerger une nouvelle pratique de l'architecture Tags : Date : 2024-11-11 projet principal d'énonciation d'une théorie de la transformation ! Structure", - "slug": "aep-emerger-une-nouvelle-pratique", - "theme": "AEP regénération", - "path": "AEP ARTICLES, BROUILLON/AEP regénération/! AEP émerger une nouvelle pratique.md" - }, - { - "id": "aep-mdcs-medecine-du-corps-social", - "label": "AEP mdcs - Médecine du corps social", - "family": "methode", - "intention": "Note capitalisme socialisme 3ème voie ? Communes, local briser pyramide colonialiste, troisième voie, rendre local l'indispensable, et global le spécifique.", - "slug": "aep-mdcs-medecine-du-corps-social", - "theme": "AEP regénération", - "path": "AEP ARTICLES, BROUILLON/AEP regénération/AEP mdcs - Médecine du corps social.md" - }, { "id": "aep-spiritualite", "label": "AEP spiritualité", @@ -1029,22 +1029,6 @@ "source": "aep-dec-l-inconfort-la-solitude-crise-d-identite", "target": "aep-rite-passage-age-adulte" }, - { - "source": "aep-israel-palestine-conflit-brut", - "target": "aep-ia-usage-sage" - }, - { - "source": "aep-israel-palestine-conflit-brut", - "target": "crepuscule-juan-branco-itw" - }, - { - "source": "aep-israel-palestine-conflit-brut", - "target": "aep-justice-securite" - }, - { - "source": "aep-politique-energie-fr", - "target": "aep-emerger-une-nouvelle-pratique" - }, { "source": "aep-education", "target": "aep-ecrire-de-nouveaux-recits" @@ -1053,46 +1037,6 @@ "source": "aep-education", "target": "aep-medias-la-pensee-critique" }, - { - "source": "crepuscule-juan-branco-itw", - "target": "aep-politique-etat-des-lieux" - }, - { - "source": "crepuscule-juan-branco-itw", - "target": "aep-medias-la-pensee-critique" - }, - { - "source": "2026-03-11-aep-ia-usage-sage-intention", - "target": "aep-ia-usage-sage" - }, - { - "source": "aep-ia-usage-sage", - "target": "aep-medias-la-pensee-critique" - }, - { - "source": "manifeste-anti-iag", - "target": "aep-ia-usage-sage" - }, - { - "source": "aep-culture-dominante-analyse-series", - "target": "aep-ecrire-de-nouveaux-recits" - }, - { - "source": "aep-prospective", - "target": "aep-mdcs-medecine-du-corps-social" - }, - { - "source": "aep-ecrire-de-nouveaux-recits", - "target": "l-empire-n-a-jamais-pris-fin-6" - }, - { - "source": "archetypes-eneagramme", - "target": "aep-ecrire-de-nouveaux-recits" - }, - { - "source": "archetypes-eneagramme", - "target": "livre-le-nouveau-contrat-social" - }, { "source": "aep-histoire", "target": "aep-ecrire-de-nouveaux-recits" @@ -1121,6 +1065,70 @@ "source": "aep-histoire", "target": "l-empire-n-a-jamais-pris-fin-5-cathares" }, + { + "source": "2026-03-11-aep-ia-usage-sage-intention", + "target": "aep-ia-usage-sage" + }, + { + "source": "aep-ia-usage-sage", + "target": "aep-medias-la-pensee-critique" + }, + { + "source": "manifeste-anti-iag", + "target": "aep-ia-usage-sage" + }, + { + "source": "aep-israel-palestine-conflit-brut", + "target": "aep-ia-usage-sage" + }, + { + "source": "aep-israel-palestine-conflit-brut", + "target": "crepuscule-juan-branco-itw" + }, + { + "source": "aep-israel-palestine-conflit-brut", + "target": "aep-justice-securite" + }, + { + "source": "aep-politique-energie-fr", + "target": "aep-emerger-une-nouvelle-pratique" + }, + { + "source": "crepuscule-juan-branco-itw", + "target": "aep-politique-etat-des-lieux" + }, + { + "source": "crepuscule-juan-branco-itw", + "target": "aep-medias-la-pensee-critique" + }, + { + "source": "aep-culture-dominante-analyse-series", + "target": "aep-ecrire-de-nouveaux-recits" + }, + { + "source": "aep-prospective", + "target": "aep-mdcs-medecine-du-corps-social" + }, + { + "source": "aep-ecrire-de-nouveaux-recits", + "target": "l-empire-n-a-jamais-pris-fin-6" + }, + { + "source": "archetypes-eneagramme", + "target": "aep-ecrire-de-nouveaux-recits" + }, + { + "source": "archetypes-eneagramme", + "target": "livre-le-nouveau-contrat-social" + }, + { + "source": "aep-emerger-une-nouvelle-pratique", + "target": "l-empire-n-a-jamais-pris-fin-6" + }, + { + "source": "aep-mdcs-medecine-du-corps-social", + "target": "livre-le-nouveau-contrat-social" + }, { "source": "aep-fiscalite-contrat-social", "target": "livre-le-nouveau-contrat-social" @@ -1137,14 +1145,6 @@ "source": "aep-performance-douce", "target": "aep-sante-mentale-burn-out" }, - { - "source": "aep-emerger-une-nouvelle-pratique", - "target": "l-empire-n-a-jamais-pris-fin-6" - }, - { - "source": "aep-mdcs-medecine-du-corps-social", - "target": "livre-le-nouveau-contrat-social" - }, { "source": "aep-spiritualite", "target": "aep-histoire" diff --git a/src/components/astro/ColCentre.astro b/src/components/astro/ColCentre.astro index 155e6fc..0158e07 100644 --- a/src/components/astro/ColCentre.astro +++ b/src/components/astro/ColCentre.astro @@ -1,8 +1,8 @@ --- -// Centre - HAUT : tabs (Carte O mindmap | Chatbot RAG placeholder PC7). +// Centre - HAUT : tabs (Carte O mindmap | Chatbot RAG branche PC7). // BAS : iframe carte AEP + scroll articles Substack (PC4). import CarteOWrapper from '../vue/CarteOWrapper.vue'; -import ChatbotPlaceholder from '../vue/ChatbotPlaceholder.vue'; +import ChatbotV2 from '../vue/ChatbotV2.vue'; import IframeCarteAEP from './IframeCarteAEP.astro'; import ScrollArticles from './ScrollArticles.astro'; --- @@ -51,7 +51,7 @@ import ScrollArticles from './ScrollArticles.astro'; data-tab-panel="chatbot" class="absolute inset-0 hidden" > - <ChatbotPlaceholder client:visible /> + <ChatbotV2 client:visible /> </div> </div> </section> diff --git a/src/components/vue/ChatbotV2.vue b/src/components/vue/ChatbotV2.vue new file mode 100644 index 0000000..876992d --- /dev/null +++ b/src/components/vue/ChatbotV2.vue @@ -0,0 +1,187 @@ +<script setup lang="ts"> +// PC7 — Chatbot V1 brancha sur endpoint AEP (proxy Astro server-side). +// Persistance thread sessionStorage. Bandeau beta info ; pas de streaming, pas de markdown V1. + +import { ref, onMounted, nextTick, useTemplateRef } from 'vue' + +interface Msg { + role: 'user' | 'bot' + content: string + ts: number +} + +const props = defineProps<{ apiUrl?: string }>() + +const messages = ref<Msg[]>([]) +const input = ref('') +const sending = ref(false) +const errorBanner = ref('') +const threadEl = useTemplateRef<HTMLElement>('threadEl') + +const STORAGE_KEY = 'tf-chatbot-v2-thread' +const ENDPOINT = props.apiUrl || '/api/chatbot' + +onMounted(() => { + try { + const saved = sessionStorage.getItem(STORAGE_KEY) + if (saved) messages.value = JSON.parse(saved) + } catch { + // sessionStorage indisponible ; on ignore. + } +}) + +const persist = () => { + try { + sessionStorage.setItem(STORAGE_KEY, JSON.stringify(messages.value)) + } catch { + // ignore + } +} + +const scrollToBottom = async () => { + await nextTick() + const el = threadEl.value + if (el) el.scrollTo({ top: el.scrollHeight, behavior: 'smooth' }) +} + +const send = async () => { + const q = input.value.trim() + if (!q || sending.value) return + + messages.value.push({ role: 'user', content: q, ts: Date.now() }) + input.value = '' + sending.value = true + errorBanner.value = '' + persist() + scrollToBottom() + + try { + const res = await fetch(ENDPOINT, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + question: q, + history: messages.value.slice(-10).map((m) => ({ role: m.role, content: m.content })), + }), + }) + + if (!res.ok) { + if (res.status === 429) { + errorBanner.value = 'Limite de questions atteinte ; ressaie plus tard.' + } else if (res.status === 502 || res.status === 504) { + errorBanner.value = "Le service de chat n'est pas joignable ; ressaie dans un instant." + } else { + errorBanner.value = `Erreur ${res.status} ; ressaie ou recharge la page.` + } + sending.value = false + return + } + + const data = await res.json() + const answer = + data.reponse_texte || + data.answer || + data.text || + data.message || + 'Pas de reponse.' + + messages.value.push({ role: 'bot', content: answer, ts: Date.now() }) + } catch (e) { + errorBanner.value = "Probleme reseau ; verifie ta connexion." + messages.value.push({ + role: 'bot', + content: `Erreur : ${(e as Error).message || 'inconnue'}`, + ts: Date.now(), + }) + } finally { + sending.value = false + persist() + scrollToBottom() + } +} + +const reset = () => { + messages.value = [] + errorBanner.value = '' + persist() +} +</script> + +<template> + <div class="h-full flex flex-col bg-white"> + <div class="bg-amber-50 border-b border-amber-200 px-3 py-2 text-xs text-amber-900 leading-snug"> + <span class="font-medium">Beta :</span> 120 fiches AEP indexees (Mistral Small) ; bientot RAG-PE multi-corpus. + </div> + + <div + v-if="errorBanner" + class="bg-rose-50 border-b border-rose-200 px-3 py-2 text-xs text-rose-800" + role="alert" + > + {{ errorBanner }} + </div> + + <div + ref="threadEl" + class="flex-1 overflow-y-auto px-3 py-3 space-y-3" + role="log" + aria-live="polite" + > + <p + v-if="!messages.length" + class="text-neutral-400 text-sm italic text-center mt-8 px-4" + > + Pose une question sur l'AEP ; le manifeste ; les agences locales... + </p> + + <div + v-for="m in messages" + :key="m.ts" + :class="[ + 'max-w-[85%] rounded-lg px-3 py-2 text-sm leading-relaxed whitespace-pre-wrap break-words', + m.role === 'user' + ? 'bg-neutral-900 text-white ml-auto' + : 'bg-neutral-100 text-neutral-800 mr-auto', + ]" + > + {{ m.content }} + </div> + + <div + v-if="sending" + class="bg-neutral-100 max-w-[60%] rounded-lg px-3 py-2 text-sm text-neutral-500 italic mr-auto" + > + Le bot reflechit... + </div> + </div> + + <form + class="border-t border-neutral-200 p-2 flex gap-2 items-stretch" + @submit.prevent="send" + > + <input + v-model="input" + type="text" + placeholder="Ta question..." + :disabled="sending" + class="flex-1 px-3 py-2 border border-neutral-300 rounded-lg text-sm focus:outline-none focus:border-neutral-900 disabled:opacity-60" + aria-label="Saisis ta question" + /> + <button + type="submit" + :disabled="sending || !input.trim()" + class="px-3 py-2 bg-neutral-900 text-white rounded-lg text-sm font-medium disabled:opacity-50" + > + Envoyer + </button> + <button + v-if="messages.length" + type="button" + class="px-2 py-2 text-neutral-500 text-xs hover:text-neutral-900 underline" + @click="reset" + > + Reset + </button> + </form> + </div> +</template> diff --git a/src/pages/a-propos.astro b/src/pages/a-propos.astro index 58644b3..6a836ad 100644 --- a/src/pages/a-propos.astro +++ b/src/pages/a-propos.astro @@ -1,4 +1,6 @@ --- +export const prerender = true; + import BaseLayout from '../layouts/BaseLayout.astro'; import HamburgerMenu from '../components/astro/HamburgerMenu.astro'; --- diff --git a/src/pages/api/chatbot.ts b/src/pages/api/chatbot.ts new file mode 100644 index 0000000..525f42c --- /dev/null +++ b/src/pages/api/chatbot.ts @@ -0,0 +1,65 @@ +// PC7 — Endpoint proxy POST /api/chatbot +// Forward la question vers CHATBOT_UPSTREAM (default : aep.trans-former.fr/api/chatbot). +// Tunnel CORS (l'upstream ne sert pas de header CORS pour trans-former.fr) + timeout 25s. + +import type { APIRoute } from 'astro' + +export const prerender = false + +const UPSTREAM = + import.meta.env.CHATBOT_UPSTREAM || 'https://aep.trans-former.fr/api/chatbot' +const TIMEOUT_MS = 25_000 + +const jsonResponse = (status: number, payload: unknown) => + new Response(JSON.stringify(payload), { + status, + headers: { 'Content-Type': 'application/json; charset=utf-8' }, + }) + +export const POST: APIRoute = async ({ request }) => { + let body: { question?: string; q?: string; history?: unknown } + + try { + body = await request.json() + } catch { + return jsonResponse(400, { error: 'invalid_json' }) + } + + const question = (body.question || body.q || '').toString().trim() + if (!question) { + return jsonResponse(400, { error: 'missing_question' }) + } + + const ctrl = new AbortController() + const timer = setTimeout(() => ctrl.abort(), TIMEOUT_MS) + + try { + const upstream = await fetch(UPSTREAM, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + question, + history: Array.isArray(body.history) ? body.history : [], + }), + signal: ctrl.signal, + }) + clearTimeout(timer) + + const text = await upstream.text() + const contentType = + upstream.headers.get('content-type') || 'application/json; charset=utf-8' + + return new Response(text, { + status: upstream.status, + headers: { 'Content-Type': contentType }, + }) + } catch (e) { + clearTimeout(timer) + const err = e as Error + const aborted = err.name === 'AbortError' + return jsonResponse(aborted ? 504 : 502, { + error: aborted ? 'upstream_timeout' : 'upstream_failed', + detail: err.message || 'unknown', + }) + } +} diff --git a/src/pages/index.astro b/src/pages/index.astro index 844122f..71a5031 100644 --- a/src/pages/index.astro +++ b/src/pages/index.astro @@ -1,4 +1,6 @@ --- +export const prerender = true; + import BaseLayout from '../layouts/BaseLayout.astro'; import ColJournal from '../components/astro/ColJournal.astro'; import ColCentre from '../components/astro/ColCentre.astro'; diff --git a/src/pages/manifeste.astro b/src/pages/manifeste.astro index 6a1b68e..ee64a58 100644 --- a/src/pages/manifeste.astro +++ b/src/pages/manifeste.astro @@ -1,4 +1,6 @@ --- +export const prerender = true; + import BaseLayout from '../layouts/BaseLayout.astro'; import HamburgerMenu from '../components/astro/HamburgerMenu.astro'; --- diff --git a/src/pages/manifeste/commander.astro b/src/pages/manifeste/commander.astro index a8653a0..5006043 100644 --- a/src/pages/manifeste/commander.astro +++ b/src/pages/manifeste/commander.astro @@ -1,4 +1,6 @@ --- +export const prerender = true; + import BaseLayout from '../../layouts/BaseLayout.astro'; import HamburgerMenu from '../../components/astro/HamburgerMenu.astro'; --- diff --git a/src/pages/mentions-legales.astro b/src/pages/mentions-legales.astro index 7967c98..9d1c1fb 100644 --- a/src/pages/mentions-legales.astro +++ b/src/pages/mentions-legales.astro @@ -1,4 +1,6 @@ --- +export const prerender = true; + import BaseLayout from '../layouts/BaseLayout.astro'; import HamburgerMenu from '../components/astro/HamburgerMenu.astro'; --- From 62794459ac1b07ac465e3ed36c7aa5cbe3aac99f Mon Sep 17 00:00:00 2001 From: Jules Neny <jules@trans-former.fr> Date: Mon, 11 May 2026 15:03:45 +0200 Subject: [PATCH 08/36] feat(v11-a): hashtags 4 categories capsules monospace + selecteur Politique Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --- src/components/astro/ColJournal.astro | 300 ++++++++++++++++++++------ src/components/vue/JournalList.vue | 32 ++- 2 files changed, 262 insertions(+), 70 deletions(-) diff --git a/src/components/astro/ColJournal.astro b/src/components/astro/ColJournal.astro index ccbc089..3425e1b 100644 --- a/src/components/astro/ColJournal.astro +++ b/src/components/astro/ColJournal.astro @@ -1,56 +1,101 @@ --- -// ColJournal - colonne gauche : CTA Manifeste + Hashtags accordeon + Journal (PC6) -// 7 hashtags = 7 plateformes (cf delta 15 du PILOTE-PC.md) import JournalList from '../vue/JournalList.vue'; -const hashtags = [ - { tag: '#manifeste', plateforme: 'Blog trans-former.fr', canal: 'ecriture longue' }, - { tag: '#building-public', plateforme: 'LinkedIn', canal: 'journal pro' }, - { tag: '#politique', plateforme: 'Substack', canal: 'pensee AEP' }, - { tag: '#aep-politique', plateforme: 'Insta @aep.politique', canal: 'carrousels manifeste' }, - { tag: '#peinture', plateforme: 'Insta @julesneny', canal: 'art / poesie / Corse' }, - { tag: '#podcast', plateforme: 'Castopod', canal: 'podcast.trans-former.fr' }, - { tag: '#stack', plateforme: 'GitHub', canal: 'open source' }, +const categories = [ + { + id: 'politique', + label: 'Politique', + color: '#1d4ed8', + hashtags: ['#politique', '#aep-politique'], + plateformes: [ + { id: 'instagram', label: '@aep.politique', url: 'https://www.instagram.com/aep.politique/' }, + { id: 'castopod', label: 'Podcast', url: 'https://podcast.trans-former.fr' }, + ], + hasSelector: true, + }, + { + id: 'art', + label: 'Art', + color: '#dc2626', + hashtags: ['#peinture', '#art'], + plateformes: [ + { id: 'instagram', label: '@julesneny', url: 'https://www.instagram.com/julesneny/' }, + ], + hasSelector: false, + }, + { + id: 'outils', + label: 'Outils', + color: '#16a34a', + hashtags: ['#stack', '#building-public'], + plateformes: [ + { id: 'gitea', label: 'Gitea', url: 'https://git.trans-former.fr/jules' }, + ], + hasSelector: false, + }, ]; --- <div class="h-full flex flex-col p-4 pt-20 md:pt-6 gap-5"> - <!-- CTA Manifeste --> - <a - href="/manifeste" - class="block px-4 py-3 bg-neutral-900 text-white rounded-lg font-medium text-center hover:bg-neutral-700 transition-colors shadow-sm" - > - Lire le manifeste → - </a> <!-- Hashtags accordeon --> <details id="hashtags-accordion" class="border-t border-neutral-200 pt-4"> <summary class="font-semibold cursor-pointer select-none flex items-center justify-between"> <span>Hashtags</span> - <span class="text-xs text-neutral-400 font-normal">7 plateformes</span> + <span class="text-xs text-neutral-400 font-normal">3 categories</span> </summary> - <ul class="mt-3 space-y-2 text-sm"> - {hashtags.map(({ tag, plateforme, canal }) => ( - <li> - <label class="flex items-start gap-2 cursor-pointer hover:bg-neutral-50 rounded p-1 -m-1 transition-colors"> - <input - type="checkbox" - data-hashtag={tag} - class="mt-1 accent-neutral-900" - checked - /> - <span class="flex-1"> - <span class="font-mono text-neutral-700 text-[13px]">{tag}</span> - <span class="block text-xs text-neutral-500 leading-snug"> - {plateforme} ; {canal} - </span> - </span> - </label> - </li> + + <div class="mt-3 flex flex-wrap gap-2" id="category-badges"> + {categories.map((cat) => ( + <button + type="button" + data-category-id={cat.id} + data-hashtags={cat.hashtags.join(',')} + data-color={cat.color} + data-has-selector={cat.hasSelector ? 'true' : 'false'} + class="category-badge" + style={`background:${cat.color};color:#fff;font-family:'Courier New',Courier,monospace;font-size:13px;padding:3px 10px;border-radius:4px;cursor:pointer;border:1px solid ${cat.color};`} + > + {cat.label} + </button> ))} - </ul> + </div> + + <!-- Selecteur plateforme Politique --> + <div id="politique-selector" class="mt-2 hidden flex gap-2"> + {categories[0].plateformes.map((p) => ( + <button + type="button" + data-platform-id={p.id} + class="platform-pill" + style="font-family:'Courier New',Courier,monospace;font-size:12px;padding:2px 8px;border-radius:12px;cursor:pointer;border:1px solid #1d4ed8;background:transparent;color:#1d4ed8;" + > + {p.label} + </button> + ))} + </div> + + <!-- Bouton Replier --> + <div class="mt-3 flex justify-end"> + <button + type="button" + id="accordion-close" + class="text-xs text-neutral-400 underline cursor-pointer bg-transparent border-0 p-0" + > + replier + </button> + </div> + + <!-- Bouton Manifeste --> + <a + href="/manifeste" + class="block mt-3 px-4 py-2 bg-neutral-900 text-white font-bold text-sm text-center rounded-lg hover:bg-neutral-700 transition-colors" + style="font-family:'Courier New',Courier,monospace;" + > + Manifeste - Lire + </a> </details> - <!-- Journal chrono (skeleton, slot rempli par PC6) --> + <!-- Journal chrono --> <section class="border-t border-neutral-200 pt-4 flex-1 overflow-y-auto"> <h2 class="font-semibold mb-3 flex items-center justify-between"> <span>Journal</span> @@ -63,41 +108,170 @@ const hashtags = [ </div> <script> - // Hashtags accordeon : ferme par defaut mobile, ouvert desktop (>= 768px) const accordion = document.getElementById('hashtags-accordion') as HTMLDetailsElement | null; if (accordion) { const mql = window.matchMedia('(min-width: 768px)'); - const apply = () => { - accordion.open = mql.matches; - }; + const apply = () => { accordion.open = mql.matches; }; apply(); mql.addEventListener('change', apply); } - // Persistence filtres hashtags (localStorage) - const checkboxes = document.querySelectorAll<HTMLInputElement>('[data-hashtag]'); - const STORAGE_KEY = 'tf-hashtag-filters'; - let stored: Record<string, boolean> = {}; - try { - stored = JSON.parse(localStorage.getItem(STORAGE_KEY) || '{}'); - } catch { - stored = {}; + const closeBtn = document.getElementById('accordion-close'); + if (closeBtn && accordion) { + closeBtn.addEventListener('click', () => { accordion.open = false; }); } - checkboxes.forEach((cb) => { - const tag = cb.dataset.hashtag; - if (!tag) return; - if (tag in stored) cb.checked = stored[tag]; - cb.addEventListener('change', () => { - stored[tag] = cb.checked; - try { - localStorage.setItem(STORAGE_KEY, JSON.stringify(stored)); - } catch { - // mode prive : on continue silencieusement + const STORAGE_KEY = 'tf-hashtag-filters'; + const PLATFORM_KEY = 'tf-platform-filter'; + + // Active state : map categoryId -> boolean + const activeCategories: Record<string, boolean> = { politique: true, art: true, outils: true }; + + // Platform filter : map categoryId -> platformId | null + const platformFilters: Record<string, string | null> = { politique: null }; + + // Load persisted state + try { + const storedHashtags = JSON.parse(localStorage.getItem(STORAGE_KEY) || '{}') as Record<string, boolean>; + // Derive category active state from hashtag values + const politiqueHashtags = ['#politique', '#aep-politique']; + const artHashtags = ['#peinture', '#art']; + const outilsHashtags = ['#stack', '#building-public']; + + const allPolitique = politiqueHashtags.every(h => storedHashtags[h] !== false); + const allArt = artHashtags.every(h => storedHashtags[h] !== false); + const allOutils = outilsHashtags.every(h => storedHashtags[h] !== false); + + if (Object.keys(storedHashtags).length > 0) { + activeCategories['politique'] = allPolitique; + activeCategories['art'] = allArt; + activeCategories['outils'] = allOutils; + } + } catch { /* mode prive */ } + + try { + const storedPlatform = JSON.parse(localStorage.getItem(PLATFORM_KEY) || '{}') as Record<string, string | null>; + if (storedPlatform.politique !== undefined) { + platformFilters['politique'] = storedPlatform.politique; + } + } catch { /* mode prive */ } + + const buildHashtagPayload = () => { + const hashtags: Record<string, boolean> = {}; + const catHashtags: Record<string, string[]> = { + politique: ['#politique', '#aep-politique'], + art: ['#peinture', '#art'], + outils: ['#stack', '#building-public'], + }; + for (const [catId, tags] of Object.entries(catHashtags)) { + const active = activeCategories[catId] ?? true; + for (const tag of tags) { + hashtags[tag] = active; } - window.dispatchEvent( - new CustomEvent('hashtag-filter-change', { detail: { ...stored } }) - ); + } + return hashtags; + }; + + const persist = () => { + try { + localStorage.setItem(STORAGE_KEY, JSON.stringify(buildHashtagPayload())); + localStorage.setItem(PLATFORM_KEY, JSON.stringify(platformFilters)); + } catch { /* mode prive */ } + }; + + const dispatchHashtag = () => { + window.dispatchEvent(new CustomEvent('hashtag-filter-change', { detail: buildHashtagPayload() })); + }; + + const dispatchPlatform = (platform: string | null) => { + window.dispatchEvent(new CustomEvent('platform-filter-change', { detail: { platform } })); + }; + + const updateBadgeStyle = (btn: HTMLElement, active: boolean) => { + const color = btn.dataset.color || '#000'; + if (active) { + btn.style.background = color; + btn.style.color = '#fff'; + btn.style.border = `1px solid ${color}`; + } else { + btn.style.background = 'transparent'; + btn.style.color = color; + btn.style.border = `1px solid ${color}`; + } + }; + + const updatePolitiqueSelector = () => { + const selector = document.getElementById('politique-selector'); + if (!selector) return; + if (activeCategories['politique']) { + selector.classList.remove('hidden'); + selector.style.display = 'flex'; + } else { + selector.classList.add('hidden'); + selector.style.display = 'none'; + } + }; + + const updatePillStyles = () => { + const pills = document.querySelectorAll<HTMLElement>('.platform-pill'); + const active = platformFilters['politique']; + pills.forEach((pill) => { + const pid = pill.dataset.platformId; + if (!active || pid === active) { + pill.style.background = '#1d4ed8'; + pill.style.color = '#fff'; + } else { + pill.style.background = 'transparent'; + pill.style.color = '#1d4ed8'; + } + }); + }; + + // Init badge styles + const badges = document.querySelectorAll<HTMLElement>('.category-badge'); + badges.forEach((btn) => { + const catId = btn.dataset.categoryId || ''; + updateBadgeStyle(btn, activeCategories[catId] ?? true); + + btn.addEventListener('click', () => { + activeCategories[catId] = !(activeCategories[catId] ?? true); + updateBadgeStyle(btn, activeCategories[catId]); + updatePolitiqueSelector(); + if (!activeCategories['politique']) { + platformFilters['politique'] = null; + updatePillStyles(); + dispatchPlatform(null); + } + persist(); + dispatchHashtag(); }); }); + + // Init selector + updatePolitiqueSelector(); + updatePillStyles(); + + // Platform pills + const pills = document.querySelectorAll<HTMLElement>('.platform-pill'); + pills.forEach((pill) => { + pill.addEventListener('click', () => { + const pid = pill.dataset.platformId || null; + // Toggle : click on active pill deselects it + if (platformFilters['politique'] === pid) { + platformFilters['politique'] = null; + } else { + platformFilters['politique'] = pid; + } + updatePillStyles(); + persist(); + dispatchPlatform(platformFilters['politique']); + }); + }); + + // Apply persisted filters on init + persist(); + dispatchHashtag(); + if (platformFilters['politique']) { + dispatchPlatform(platformFilters['politique']); + } </script> diff --git a/src/components/vue/JournalList.vue b/src/components/vue/JournalList.vue index 2e3a9f5..f0e342d 100644 --- a/src/components/vue/JournalList.vue +++ b/src/components/vue/JournalList.vue @@ -20,6 +20,7 @@ interface JournalPayload { const items = ref<JournalItem[]>([]) const filters = ref<Record<string, boolean>>({}) +const platformFilter = ref<string | null>(null) const loading = ref(true) const errored = ref(false) const empty = ref(false) @@ -45,9 +46,15 @@ const onFilterChange = (e: Event) => { } } +const onPlatformChange = (e: Event) => { + const ce = e as CustomEvent + platformFilter.value = ce.detail?.platform ?? null +} + onMounted(async () => { loadFilters() window.addEventListener('hashtag-filter-change', onFilterChange as EventListener) + window.addEventListener('platform-filter-change', onPlatformChange as EventListener) try { const res = await fetch(JOURNAL_URL, { cache: 'no-store' }) if (!res.ok) { @@ -67,17 +74,28 @@ onMounted(async () => { onUnmounted(() => { window.removeEventListener('hashtag-filter-change', onFilterChange as EventListener) + window.removeEventListener('platform-filter-change', onPlatformChange as EventListener) }) const visibleItems = computed(() => { - // Tous les filtres faux = aucun affiche ; tous true ou aucune cle = tout afficher const keys = Object.keys(filters.value) - if (keys.length === 0) return items.value - const activeKeys = keys.filter((k) => filters.value[k]) - // Si l'utilisateur a explicitement décoché tous les hashtags, on n'affiche rien - if (activeKeys.length === 0 && keys.some((k) => filters.value[k] === false)) return [] - if (activeKeys.length === 0) return items.value - return items.value.filter((it) => activeKeys.includes(it.hashtag)) + let filtered: JournalItem[] + if (keys.length === 0) { + filtered = items.value + } else { + const activeKeys = keys.filter((k) => filters.value[k]) + if (activeKeys.length === 0 && keys.some((k) => filters.value[k] === false)) { + filtered = [] + } else if (activeKeys.length === 0) { + filtered = items.value + } else { + filtered = items.value.filter((it) => activeKeys.includes(it.hashtag)) + } + } + if (platformFilter.value) { + filtered = filtered.filter((it) => it.platform === platformFilter.value) + } + return filtered }) const formatDate = (iso: string) => { From 5642690829737b021006cf67ee61d6fdc2791c7e Mon Sep 17 00:00:00 2001 From: Jules Neny <jules@trans-former.fr> Date: Mon, 11 May 2026 15:04:53 +0200 Subject: [PATCH 09/36] feat(v11-b): carte-o YAML source editoriale + build script niveau/nature/statut Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --- package-lock.json | 3 + package.json | 3 + public/data/carte-o-source.yaml | 120 +++ public/data/carte-o.json | 1363 ++++++------------------------- scripts/build-carte-o.js | 340 ++------ 5 files changed, 440 insertions(+), 1389 deletions(-) create mode 100644 public/data/carte-o-source.yaml diff --git a/package-lock.json b/package-lock.json index 5d06892..15bca63 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,6 +20,9 @@ "tailwindcss": "^4.2.4", "vue": "^3.5.34" }, + "devDependencies": { + "js-yaml": "^4.1.1" + }, "engines": { "node": ">=22.12.0" } diff --git a/package.json b/package.json index 8ebab62..3f9eb4a 100644 --- a/package.json +++ b/package.json @@ -26,5 +26,8 @@ "gray-matter": "^4.0.3", "tailwindcss": "^4.2.4", "vue": "^3.5.34" + }, + "devDependencies": { + "js-yaml": "^4.1.1" } } diff --git a/public/data/carte-o-source.yaml b/public/data/carte-o-source.yaml new file mode 100644 index 0000000..a70c2ac --- /dev/null +++ b/public/data/carte-o-source.yaml @@ -0,0 +1,120 @@ +# Carte O AEP - source editoriale manuelle +# Edite manuellement apres chaque publication +# Build: npm run carte-o -> public/data/carte-o.json +# statut: gestation (draft/en cours) | edite (publie) +# nature: essai (texte politique) | projet (projet archi) +# niveau: 0 (centre) | 1 (concepts force) | 2 (thematiques/projets) + +version: "1.1" + +centre: + id: "nouveau-contrat-social" + label: "Nouveau Contrat Social" + niveau: 0 + nature: essai + statut: gestation + resume: "Assemblage de tout ce que j'ecris qui s'entrechoque - inventer un nouveau contrat social." + +concepts_force: + - id: "ncs-politique" + label: "Nouveau contrat social" + niveau: 1 + nature: essai + statut: gestation + resume: "L'ecriture politique d'un futur habitable - essai central AEP." + - id: "medecine-corps-social" + label: "Medecine du corps social" + niveau: 1 + nature: essai + statut: gestation + resume: "Diagnostiquer et soigner les pathologies du corps social - concept AEP mdcs." + +thematiques: + - id: "systemique" + label: "Systemique & complexite" + niveau: 2 + nature: essai + statut: gestation + - id: "pratiques-collectives" + label: "Pratiques collectives" + niveau: 2 + nature: essai + statut: gestation + - id: "art-narration" + label: "Art & narration" + niveau: 2 + nature: essai + statut: gestation + - id: "pouvoir-domination" + label: "Rapport au pouvoir" + niveau: 2 + nature: essai + statut: gestation + - id: "medias-critique" + label: "Medias & pensee critique" + niveau: 2 + nature: essai + statut: gestation + - id: "justice-securite" + label: "Justice & securite" + niveau: 2 + nature: essai + statut: gestation + - id: "sante-globale" + label: "Sante globale" + niveau: 2 + nature: essai + statut: gestation + - id: "agriculture" + label: "Agriculture" + niveau: 2 + nature: essai + statut: gestation + - id: "post-croissance" + label: "Post-croissance" + niveau: 2 + nature: essai + statut: gestation + - id: "anthropocene" + label: "Anthropocene & effondrement" + niveau: 2 + nature: essai + statut: gestation + - id: "education" + label: "Education a la transformation" + niveau: 2 + nature: essai + statut: gestation + - id: "urbanisme" + label: "Urbanisme" + niveau: 2 + nature: essai + statut: gestation + - id: "geopolitique" + label: "Geopolitique & decolonisation" + niveau: 2 + nature: essai + statut: gestation + - id: "ia-technologie" + label: "IA & technologie" + niveau: 2 + nature: essai + statut: gestation + - id: "spiritualite" + label: "Spiritualite" + niveau: 2 + nature: essai + statut: gestation + +projets: + - id: "tmip" + label: "TMIP" + niveau: 2 + nature: projet + statut: gestation + resume: "Transport, mobilite, industrie, politique - projet archi. Exemple de projet archi relie aux thematiques AEP." + liens_thematiques: + - "urbanisme" + - "justice-securite" + - "post-croissance" + - "agriculture" diff --git a/public/data/carte-o.json b/public/data/carte-o.json index 1389295..340867b 100644 --- a/public/data/carte-o.json +++ b/public/data/carte-o.json @@ -1,1181 +1,282 @@ { - "meta": { - "generated": "2026-05-08T23:20:14.828Z", - "source": "AEP/Articles", - "nodeCount": 84, - "edgeCount": 94, - "familyDistribution": { - "methode": 36, - "collectif": 14, - "ressource": 17, - "concept": 10, - "penseur": 7 - }, - "familyColors": { - "penseur": "#3b82f6", - "concept": "#10b981", - "methode": "#f59e0b", - "collectif": "#ef4444", - "ressource": "#8b5cf6" - }, - "themeStats": { - "AEP articles, idées en gestation.md": 1, - "AEP tableau des articles.md": 1, - "LIVRE - le nouveau contrat social.md": 1, - "TR - renovation-energetique - website pro.md": 1, - "AEP ARTICLES, BROUILLON": 6, - "Livre - le nouveau contrat social": 2, - "AEP agriculture": 1, - "AEP archi": 4, - "AEP déconstruction": 9, - "AEP education": 4, - "AEP histoire": 7, - "AEP IA": 14, - "AEP géopolitique": 3, - "AEP justice": 3, - "AEP piraterie (script formation)": 2, - "AEP nouveaux récits": 9, - "AEP politique": 3, - "AEP regénération": 3, - "AEP santé": 6, - "AEP spiritualité": 1, - "AEP système économique": 4 - } - }, + "version": "1.1", + "generatedAt": "2026-05-11T13:04:29.806Z", "nodes": [ { - "id": "aep-articles-idees-en-gestation", - "label": "AEP articles, idées en gestation", - "family": "methode", - "intention": "inventer un nouveau contrat social AEP écrire de nouveaux récits une vue d'ensemble de mes sujets !! a reprendre, mais une super base de départ", - "slug": "aep-articles-idees-en-gestation", - "theme": "AEP articles, idées en gestation.md", - "path": "AEP articles, idées en gestation.md" - }, - { - "id": "aep-tableau-des-articles", - "label": "AEP tableau des articles", - "family": "collectif", - "intention": "", - "slug": "aep-tableau-des-articles", - "theme": "AEP tableau des articles.md", - "path": "AEP tableau des articles.md" - }, - { - "id": "livre-le-nouveau-contrat-social", - "label": "LIVRE - le nouveau contrat social", - "family": "ressource", - "intention": "Contexte : commence chez moi; a Lesponne, fond de critique de la fabrique de la ville", - "slug": "livre-le-nouveau-contrat-social", - "theme": "LIVRE - le nouveau contrat social.md", - "path": "LIVRE - le nouveau contrat social.md" - }, - { - "id": "tr-renovation-energetique-website-pro", - "label": "TR - renovation-energetique - website pro", - "family": "ressource", - "intention": "Priorité 1 — Google Business Profile - [ ] Répondre aux avis clients au fil du temps - [ ] Obtenir des avis Google (te les faire demander toi-même à tes clients)", - "slug": "tr-renovation-energetique-website-pro", - "theme": "TR - renovation-energetique - website pro.md", - "path": "TR - renovation-energetique - website pro.md" - }, - { - "id": "community-organizing-methode-alinsky", - "label": "community-organizing-methode-alinsky", - "family": "methode", - "intention": "Saul Alinsky (1909-1972) est le fondateur du community organizing américain. Sa méthode : tisser des organisations réelles plutôt que des followerships individuels.", - "slug": "community-organizing-methode-alinsky", - "theme": "AEP ARTICLES, BROUILLON", - "path": "AEP ARTICLES, BROUILLON/community-organizing-methode-alinsky.md" - }, - { - "id": "critique-harari-fictions-utiles-depolitisantes", - "label": "critique-harari-fictions-utiles-depolitisantes", - "family": "methode", - "intention": "Harari est un conteur politique puissant, pas un historien académique. Ses apports sont réels, ses angles morts le sont aussi.", - "slug": "critique-harari-fictions-utiles-depolitisantes", - "theme": "AEP ARTICLES, BROUILLON", - "path": "AEP ARTICLES, BROUILLON/critique-harari-fictions-utiles-depolitisantes.md" - }, - { - "id": "eco-marxisme-6-auteurs-cles", - "label": "eco-marxisme-6-auteurs-cles", - "family": "methode", - "intention": "Six auteurs pour articuler capitalisme, écologie, colonialité et racisme — ce qu'Harari n'adresse pas.", - "slug": "eco-marxisme-6-auteurs-cles", - "theme": "AEP ARTICLES, BROUILLON", - "path": "AEP ARTICLES, BROUILLON/eco-marxisme-6-auteurs-cles.md" - }, - { - "id": "ia-medias-critique", - "label": "ia-medias-critique", - "family": "concept", - "intention": "Jules, dans ses notes de travail :", - "slug": "ia-medias-critique", - "theme": "AEP ARTICLES, BROUILLON", - "path": "AEP ARTICLES, BROUILLON/ia-medias-critique.md" - }, - { - "id": "imperialisme-throughline", - "label": "imperialisme-throughline", - "family": "concept", - "intention": "Un fil rouge philosophique qui traverse l'histoire de la domination occidentale.", - "slug": "imperialisme-throughline", - "theme": "AEP ARTICLES, BROUILLON", - "path": "AEP ARTICLES, BROUILLON/imperialisme-throughline.md" - }, - { - "id": "kakistocratie-5-mecanismes", - "label": "kakistocratie-5-mecanismes", - "family": "collectif", - "intention": "La kakistocratie désigne le gouvernement exercé par les plus incompétents. Concept formalisé par James Russell Lowell (1876).", - "slug": "kakistocratie-5-mecanismes", - "theme": "AEP ARTICLES, BROUILLON", - "path": "AEP ARTICLES, BROUILLON/kakistocratie-5-mecanismes.md" - }, - { - "id": "aep-article-fiscalite-contrat-social", - "label": "AEP - article fiscalite contrat social", - "family": "methode", - "intention": "idée d'un article qui est né d'une discussion avec thomas et c'est toujours dans cette idée d'écrire un nouveau contrat social et dans les articles à eepp la de politique je pense que ce serait important d'écrire un arti", - "slug": "aep-article-fiscalite-contrat-social", - "theme": "Livre - le nouveau contrat social", - "path": "Livre - le nouveau contrat social/AEP - article fiscalite contrat social.md" - }, - { - "id": "aep-agriculture", - "label": "AEP Agriculture", - "family": "methode", - "intention": "Contrat social : les agriculteurs, àvalorises par autre chose que les produits qu’il créent. Besoin que ça soit le moins cher possible", - "slug": "aep-agriculture", - "theme": "AEP agriculture", - "path": "AEP ARTICLES, BROUILLON/AEP agriculture/AEP Agriculture.md" - }, - { - "id": "aep-beton-critique", - "label": "AEP Béton - critique", - "family": "methode", - "intention": "MOC : Architecture technique d'ATISPOLITIQUE Source : Projet: PFE - Infolettre AEP! AEP émerger une nouvelle pratique Tags : #contenu/infolettre #contenu/manifeste #contenu/article Date : 2024-11-20 # article béton AEP e", - "slug": "aep-beton-critique", - "theme": "AEP archi", - "path": "AEP ARTICLES, BROUILLON/AEP archi/AEP Béton - critique.md" - }, - { - "id": "aep-rite-passage-age-adulte", - "label": "AEP - rite passage âge adulte", - "family": "concept", - "intention": "Intro : Qu'es ce que le passage a l'âge adulte, émancipation de ses parents, peuples premiers, passage a l'âge adulte ? Nettoyer passé, généalogie, maladies + laisser place a un soi \"véritable\" \"naturel\"", - "slug": "aep-rite-passage-age-adulte", - "theme": "AEP déconstruction", - "path": "AEP ARTICLES, BROUILLON/AEP déconstruction/AEP - rite passage âge adulte.md" - }, - { - "id": "aep-dec-amitie-et-amour", - "label": "AEP DEC - Amitié et amour", - "family": "methode", - "intention": "L’amour que j’ai est donc conditionnel. Si les conditions de sont pas remplies, pas envie de le donner ? Inconfort que ça ne comble pas mes besoins/attentes. (Et que la partie « sauveur » ne puisse pas donner) Inconfort ", - "slug": "aep-dec-amitie-et-amour", - "theme": "AEP déconstruction", - "path": "AEP ARTICLES, BROUILLON/AEP déconstruction/AEP DEC - Amitié et amour.md" - }, - { - "id": "aep-dec-itw-imane-identite-deconstruction", - "label": "AEP DEC - ITW Imane - Identité, déconstruction", - "family": "collectif", - "intention": "!2025-07-08Imaneidentitédéconstruction.ogg Contexte : soirée Imane & Mégane- Sujet ; santé mentale & libération émotionnelle, couches reliées. Puis ça a dévié sur les causes contextuelles, culturelles (système de valeurs", - "slug": "aep-dec-itw-imane-identite-deconstruction", - "theme": "AEP déconstruction", - "path": "AEP ARTICLES, BROUILLON/AEP déconstruction/AEP DEC - ITW Imane - Identité, déconstruction.md" - }, - { - "id": "aep-dec-l-inconfort-la-solitude-crise-d-identite", - "label": "AEP DEC - L'inconfort & la solitude - crise d'identité", - "family": "concept", - "intention": "Tu l’as touché avec justesse : on vit dans un monde qui valorise les certitudes, les poses d’autorité, les identités “stables”. Et ceux qui doutent, qui se déconstruisent, qui cherchent sans cesse la cohérence entre leur", - "slug": "aep-dec-l-inconfort-la-solitude-crise-d-identite", - "theme": "AEP déconstruction", - "path": "AEP ARTICLES, BROUILLON/AEP déconstruction/AEP DEC - L'inconfort & la solitude - crise d'identité.md" - }, - { - "id": "aep-dec-conditionnements-genre-argent-social", - "label": "AEP DEC conditionnements (genre, argent, social)", - "family": "methode", - "intention": "MOC : POLITIQUETRANSMETTRE Source : Discussion avec papa Projets : Démanteler Tags : Date : 2024-12-02 AEP déconstruction Détacher identité ? La connaître d'abord, mettre de la clarté, pour dire aurevoir aux illusions. (", - "slug": "aep-dec-conditionnements-genre-argent-social", - "theme": "AEP déconstruction", - "path": "AEP ARTICLES, BROUILLON/AEP déconstruction/AEP DEC conditionnements (genre, argent, social).md" - }, - { - "id": "aep-gestion-de-conflits", - "label": "AEP Gestion de conflits", - "family": "methode", - "intention": "MOC : POLITIQUE-FACILITATION Source : expérience de la vie Projets : Qui-es-tude - Famille-Colère Tags : Contexte : embrouille/drama Chachou bouillone fin de séminaire 1 quétude Date : 2024-12-22 # Enjeu Vivre avec les a", - "slug": "aep-gestion-de-conflits", - "theme": "AEP déconstruction", - "path": "AEP ARTICLES, BROUILLON/AEP déconstruction/AEP Gestion de conflits.md" - }, - { - "id": "aep-geopolitique-decolonisation-grand-sud", - "label": "AEP Géopolitique - Décolonisation grand sud", - "family": "penseur", - "intention": "MOC : POLITIQUE TRANSMETTREEMPOUVOIRMENT Source : Soirée Barbara & Rafa Projets : PFE - Infolettre AEPDémanteler Tags : #contenu/manifeste #contenu/article Date : 2024-12-01 intention : révéler la realpolitik, ou la mani", - "slug": "aep-geopolitique-decolonisation-grand-sud", - "theme": "AEP déconstruction", - "path": "AEP ARTICLES, BROUILLON/AEP déconstruction/AEP Géopolitique - Décolonisation grand sud.md" - }, - { - "id": "aep-humour-manifeste-brouillon", - "label": "AEP humour - manifeste brouillon", - "family": "methode", - "intention": "Et tu touches là un nœud brûlant pour beaucoup de personnes qui veulent porter une parole politique sans se trahir :", - "slug": "aep-humour-manifeste-brouillon", - "theme": "AEP déconstruction", - "path": "AEP ARTICLES, BROUILLON/AEP déconstruction/AEP humour - manifeste brouillon.md" - }, - { - "id": "aep-cooperation", - "label": "AEP Coopération", - "family": "collectif", - "intention": "pourquoi la coopération ? pour faire passer l'humanité a l'étape d'après trop de connaissance pour un seul individu, besoin de coopérer !! https://unchartedterritories.tomaspueyo.com/p/10-other-places-where-geniuses-hide", - "slug": "aep-cooperation", - "theme": "AEP education", - "path": "AEP ARTICLES, BROUILLON/AEP education/AEP Coopération.md" - }, - { - "id": "aep-education", - "label": "AEP éducation", - "family": "methode", - "intention": "MOC : POLITIQUE Références : Avis de tempête, discussion Aimée et Thomas, Révolution Projets : Stratégie création de contenu AEP écrire de nouveaux récits Tags : Date : 2024-03-25 article \"majeur\" sur le sujet qu'es l'éd", - "slug": "aep-education", - "theme": "AEP education", - "path": "AEP ARTICLES, BROUILLON/AEP education/AEP éducation.md" - }, - { - "id": "itw-imane-orga-pro-perso", - "label": "ITW Imane- ORGA PRO-PERSO", - "family": "methode", - "intention": "!2025-07-08Imaneauto-orga.ogg - Orga phases de vie changeantes : s’adapter à elles - intention : équilibre financier, humain, pro (productif/humain/sens). Vases communicants. - Avant indépendante : une expérience salarié", - "slug": "itw-imane-orga-pro-perso", - "theme": "AEP education", - "path": "AEP ARTICLES, BROUILLON/AEP education/ITW Imane- ORGA PRO-PERSO.md" - }, - { - "id": "memorisation-discussion-train", - "label": "Mémorisation - discussion train", - "family": "methode", - "intention": "Apprendre - moyen de s'émanciper, grandir, évoluer. Ce qui nous a permis de survivre en tant qu'espèce. Monde qui évolue, fondamental ajd. Yuval, révolutions culturelles, apprendre = définit notre rapport au monde.", - "slug": "memorisation-discussion-train", - "theme": "AEP education", - "path": "AEP ARTICLES, BROUILLON/AEP education/Mémorisation - discussion train.md" - }, - { - "id": "aep-histoire", - "label": "AEP Histoire", - "family": "penseur", - "intention": "L’empire n’a jamais pris fin 6 - L’empire n’a jamais pris fin 1 - César L’empire n’a jamais pris fin 2 - Jesus L’empire n’a jamais pris fin 3 - Gnostiques L’empire n’a jamais pris fin 4 - Francs L’empire n’a jamais pris ", - "slug": "aep-histoire", - "theme": "AEP histoire", - "path": "AEP ARTICLES, BROUILLON/AEP histoire/AEP Histoire.md" - }, - { - "id": "l-empire-n-a-jamais-pris-fin-1-cesar", - "label": "L’empire n’a jamais pris fin 1 - César", - "family": "ressource", - "intention": "L’empire n’a jamais pris fin - un nouveau récit national ?", - "slug": "l-empire-n-a-jamais-pris-fin-1-cesar", - "theme": "AEP histoire", - "path": "AEP ARTICLES, BROUILLON/AEP histoire/L’empire n’a jamais pris fin 1 - César.md" - }, - { - "id": "l-empire-n-a-jamais-pris-fin-2-jesus", - "label": "L’empire n’a jamais pris fin 2 - Jesus", - "family": "ressource", - "intention": "Réfuter le fait qu’il n’existe pas. Preuves historiques nombreuses. Colonies romaines perpétuées par tyranneaux, noblesse locale. Judée, “Herode le grand” un boucher. Le grand prêtre du temple de judee, fonction la plus ", - "slug": "l-empire-n-a-jamais-pris-fin-2-jesus", - "theme": "AEP histoire", - "path": "AEP ARTICLES, BROUILLON/AEP histoire/L’empire n’a jamais pris fin 2 - Jesus.md" - }, - { - "id": "l-empire-n-a-jamais-pris-fin-3-gnostiques", - "label": "L’empire n’a jamais pris fin 3 - Gnostiques", - "family": "ressource", - "intention": "Chrétiens - unité doctrinale et politique Gnostiques - deux dieux (dieu unique monotjeiste- incarnation force et autoritarisme. Le démiurge. Une divinité uniquement puissante sur les âmes. Existe au travers de la bonté e", - "slug": "l-empire-n-a-jamais-pris-fin-3-gnostiques", - "theme": "AEP histoire", - "path": "AEP ARTICLES, BROUILLON/AEP histoire/L’empire n’a jamais pris fin 3 - Gnostiques.md" - }, - { - "id": "l-empire-n-a-jamais-pris-fin-4-francs", - "label": "L’empire n’a jamais pris fin 4 - Francs", - "family": "ressource", - "intention": "« France » Veme siècle, empire romain en déclin (deux empereurs), mettent les Francs (à cheval France-Belgique), une petite dynastie germaine pillards à maintenir le pouvoir. (Descendant de Merovée, « requin ») Clovis co", - "slug": "l-empire-n-a-jamais-pris-fin-4-francs", - "theme": "AEP histoire", - "path": "AEP ARTICLES, BROUILLON/AEP histoire/L’empire n’a jamais pris fin 4 - Francs.md" - }, - { - "id": "l-empire-n-a-jamais-pris-fin-5-cathares", - "label": "L’empire n’a jamais pris fin 5 - Cathares", - "family": "methode", - "intention": "Ethym : Katharoi : pur (typologie de \"sans roi\", Novatien) = lien avec les disciples de Mani Pratique religieuse qui revient aux paroles originelles.", - "slug": "l-empire-n-a-jamais-pris-fin-5-cathares", - "theme": "AEP histoire", - "path": "AEP ARTICLES, BROUILLON/AEP histoire/L’empire n’a jamais pris fin 5 - Cathares.md" - }, - { - "id": "l-empire-n-a-jamais-pris-fin-6", - "label": "L’empire n’a jamais pris fin 6 -", - "family": "collectif", - "intention": "Des amour dirigeant 14eme siècle Haine de l’état. Charle 6 enfant - réprime avec violence impôt meurtrier, gaspillé par les seigneurs. Haine refoulée à l’égard du roi. Tradition jamais éteinte.", - "slug": "l-empire-n-a-jamais-pris-fin-6", - "theme": "AEP histoire", - "path": "AEP ARTICLES, BROUILLON/AEP histoire/L’empire n’a jamais pris fin 6 -.md" - }, - { - "id": "2026-03-11-aep-ia-usage-sage-intention", - "label": "2026-03-11 AEP IA usage sage intention", - "family": "ressource", - "intention": "MOC : Transcriptions Projets : AEP - IA - usage sage Date : 2026-03-11 Durée : 9m 18s · groq récap de la conversation sur l'IA super cool qu'on a eue avec Issa, envie de poser ça comme base de la future formation IA de N", - "slug": "2026-03-11-aep-ia-usage-sage-intention", - "theme": "AEP IA", - "path": "AEP ARTICLES, BROUILLON/AEP IA/2026-03-11 AEP IA usage sage intention.md" - }, - { - "id": "aep-ia-usage-sage", - "label": "AEP - IA - usage sage", - "family": "methode", - "intention": "MOC : ORGANISATION-PFE - Infolettre AEP Source : Ines Lee [\"oops i become codependent on IA\"](https://ineslee.substack.com/p/ai-codependence) + Tyler Austin Harper, The Atlantic, \"[what happens when people don't undersan", - "slug": "aep-ia-usage-sage", - "theme": "AEP IA", - "path": "AEP ARTICLES, BROUILLON/AEP IA/AEP - IA - usage sage.md" - }, - { - "id": "anthropic-revolution-ia-2026", - "label": "Anthropic, révolution IA 2026", - "family": "concept", - "intention": "révolution d'anthropic via cowork, claude code & opus.", - "slug": "anthropic-revolution-ia-2026", - "theme": "AEP IA", - "path": "AEP ARTICLES, BROUILLON/AEP IA/Anthropic, révolution IA 2026.md" - }, - { - "id": "ia-usage-sage-sven-et-lucie", - "label": "IA usage sage Sven et Lucie", - "family": "ressource", - "intention": "Voici la retranscription d'un article que j'aimerais écrire sur l'usage de l'IA. Et c'était un rendez-vous la semaine dernière avec Zden et Lucie, qui nous a permis pendant deux heures de traverser des sujets importants ", - "slug": "ia-usage-sage-sven-et-lucie", - "theme": "AEP IA", - "path": "AEP ARTICLES, BROUILLON/AEP IA/IA usage sage Sven et Lucie.md" - }, - { - "id": "manifeste-anti-iag", - "label": "Manifeste anti-IAg", - "family": "ressource", - "intention": "[Vous pouvez signer ce manifeste ([version pdf ici](https://atecopol.hypotheses.org/files/2025/12/ManifesteObjectionConscienceIAg.pdf)) dans [ce formulaire](https://framaforms.org/face-a-lia-generative-lobjection-de-cons", - "slug": "manifeste-anti-iag", - "theme": "AEP IA", - "path": "AEP ARTICLES, BROUILLON/AEP IA/Manifeste anti-IAg.md" - }, - { - "id": "2025-07-05-juan-branco-thinker-view-ep-2-30-06-2025", - "label": "2025-07-05 Juan branco - thinker view - ep 2 30-06-2025", - "family": "penseur", - "intention": "Juan branco - thinker view - ep 2, 30/06/2025", - "slug": "2025-07-05-juan-branco-thinker-view-ep-2-30-06-2025", - "theme": "AEP géopolitique", - "path": "AEP ARTICLES, BROUILLON/AEP géopolitique/2025-07-05 Juan branco - thinker view - ep 2 30-06-2025.md" - }, - { - "id": "aep-israel-palestine-conflit-brut", - "label": "AEP Israel - Palestine, conflit (brut)", - "family": "penseur", - "intention": "Depuis l’attaque du 7 octobre 2023 par le Hamas, qui a fait environ 1 200 morts israéliens, l'État d’Israël, dirigé par Benyamin Netanyahou, a lancé une campagne militaire d’une intensité sans précédent contre la bande d", - "slug": "aep-israel-palestine-conflit-brut", - "theme": "AEP géopolitique", - "path": "AEP ARTICLES, BROUILLON/AEP géopolitique/AEP Israel - Palestine, conflit (brut).md" - }, - { - "id": "aep-politique-energie-fr", - "label": "AEP Politique Énergie FR", - "family": "methode", - "intention": "MOC : Architecture technique d'ATISPOLITIQUETRANSMETTRE Source : Discussion Álvaro Projet: PFE - Infolettre AEP! AEP émerger une nouvelle pratiqueAEP Politique Énergie FR Tags : #contenu/infolettre #contenu/manifeste #co", - "slug": "aep-politique-energie-fr", - "theme": "AEP géopolitique", - "path": "AEP ARTICLES, BROUILLON/AEP géopolitique/AEP Politique Énergie FR.md" - }, - { - "id": "aep-justice-securite", - "label": "AEP justice & sécurité", - "family": "penseur", - "intention": "MOC : POLITIQUE-AEP 1 Source : divers Projets : AEP articles, idées en gestation-AEP tableau des articles Tags : #contenu/article Date : 2025-10-31 Justice/droit Juan Branco réformes structurelles https://www.youtube.com", - "slug": "aep-justice-securite", - "theme": "AEP justice", - "path": "AEP ARTICLES, BROUILLON/AEP justice/AEP justice & sécurité.md" - }, - { - "id": "crepuscule-juan-branco-itw", - "label": "Crépuscule - Juan branco - ITW", - "family": "penseur", - "intention": "Crépuscule - Juan branco - podcast - thinkerview", - "slug": "crepuscule-juan-branco-itw", - "theme": "AEP justice", - "path": "AEP ARTICLES, BROUILLON/AEP justice/Crépuscule - Juan branco - ITW.md" - }, - { - "id": "narcotrafic", - "label": "Narcotrafic", - "family": "collectif", - "intention": "Ces frontières sont valorisées dans les médias par la dramaturgie de postures martiales mettant en valeur des politiciens, doublées de menaces a l'ordre social, et a la stigmatisation de groupes sociaux (racailles, bande", - "slug": "narcotrafic", - "theme": "AEP justice", - "path": "AEP ARTICLES, BROUILLON/AEP justice/Narcotrafic.md" - }, - { - "id": "creation-de-script-template", - "label": "Création de script - template", - "family": "collectif", - "intention": "", - "slug": "creation-de-script-template", - "theme": "AEP piraterie (script formation)", - "path": "AEP ARTICLES, BROUILLON/AEP piraterie (script formation)/Création de script - template.md" - }, - { - "id": "methodologie-developpement", - "label": "methodologie_developpement", - "family": "methode", - "intention": "", - "slug": "methodologie-developpement", - "theme": "AEP piraterie (script formation)", - "path": "AEP ARTICLES, BROUILLON/AEP piraterie (script formation)/methodologie_developpement.md" - }, - { - "id": "aep-culture-dominante-analyse-series", - "label": "AEP culture dominante (analyse séries)", - "family": "concept", - "intention": "\"grands récits\" - star wars - le seigneur des anneaux - dune !Post Dune- avis critique.m4a - harry potter - eragon (le livre plus que le film) - one piece - naruto - Arcane : !Arcanes feedback série.m4a = parallèle avec ", - "slug": "aep-culture-dominante-analyse-series", - "theme": "AEP nouveaux récits", - "path": "AEP ARTICLES, BROUILLON/AEP nouveaux récits/AEP culture dominante (analyse séries).md" - }, - { - "id": "aep-medias-la-pensee-critique", - "label": "AEP médias & la pensée critique", - "family": "methode", - "intention": "MOC : POLITIQUE-PFE - Infolettre AEP Source : https://mythodologie.fr/ Projets : NAAV - apprendre a apprendre-cours de transformation urbaine- Tags : Date : 2025-06-19 une note expliquant les bases de l'intelligence crit", - "slug": "aep-medias-la-pensee-critique", - "theme": "AEP nouveaux récits", - "path": "AEP ARTICLES, BROUILLON/AEP nouveaux récits/AEP médias & la pensée critique.md" - }, - { - "id": "aep-prospective", - "label": "AEP prospective", - "family": "penseur", - "intention": "En lien avec Tout pour tout le monde - ITW - prospective", - "slug": "aep-prospective", - "theme": "AEP nouveaux récits", - "path": "AEP ARTICLES, BROUILLON/AEP nouveaux récits/AEP prospective.md" - }, - { - "id": "aep-ecrire-de-nouveaux-recits", - "label": "AEP écrire de nouveaux récits", - "family": "ressource", - "intention": "MOC : POLITIQUETRANSMETTRE- ART-FACILITATION Projets : Stratégie création de contenu PFE - Infolettre AEP AEP éducation-L'art du récit (storytelling) Tags : #contenu/article Date : 2024-03-23 # Projets d'écriture 1. LIVR", - "slug": "aep-ecrire-de-nouveaux-recits", - "theme": "AEP nouveaux récits", - "path": "AEP ARTICLES, BROUILLON/AEP nouveaux récits/AEP écrire de nouveaux récits.md" - }, - { - "id": "archetypes-eneagramme", - "label": "Archétypes - énéagramme", - "family": "collectif", - "intention": "MOC : LITTERATURE-écrivain-ART-SPIRITUALITE Source : Annif Flo 44 ans - orga Projets : AEP écrire de nouveaux récits-LIVRE - le nouveau contrat social Tags : Date : 2025-10-31 note sur l'étude des personnages d'un récit/", - "slug": "archetypes-eneagramme", - "theme": "AEP nouveaux récits", - "path": "AEP ARTICLES, BROUILLON/AEP nouveaux récits/Archétypes - énéagramme.md" - }, - { - "id": "5-chantiers-mamdani-nyc", - "label": "5 chantiers Mamdani NYC", - "family": "methode", - "intention": "Zohran Mamdani, élu maire de NYC en 2025, construit sa plateforme sur un principe simple : la ville est trop chère, il va la rendre abordable. Analyse de la faisabilité de ses 5 chantiers et du rapport de force instituti", - "slug": "5-chantiers-mamdani-nyc", - "theme": "AEP politique", - "path": "AEP ARTICLES, BROUILLON/AEP politique/5 chantiers Mamdani NYC.md" - }, - { - "id": "aep-beaute-pouvoir", - "label": "AEP beauté-pouvoir", - "family": "methode", - "intention": "= état : brouillon avancé = besoin : 1. besoin de tisser les idées entre elles, 2. prendre le temps de me faire mon avis propre, 3. orienter davantage le \"sens\" 4. & le lier a mon expérience personnelle 5. lier a Comment", - "slug": "aep-beaute-pouvoir", - "theme": "AEP politique", - "path": "AEP ARTICLES, BROUILLON/AEP politique/AEP beauté-pouvoir.md" - }, - { - "id": "aep-politique-etat-des-lieux", - "label": "AEP politique - état des lieux", - "family": "methode", - "intention": "Depuis la monarchie, démocratie est un outil des mains bourgeoises. Renverser l’ordre établi. Les pauvres sont trop « cons » pour gouverner. Il faut des technocrates.", - "slug": "aep-politique-etat-des-lieux", - "theme": "AEP politique", - "path": "AEP ARTICLES, BROUILLON/AEP politique/AEP politique - état des lieux.md" - }, - { - "id": "aep-regen-fertival", - "label": "AEP - Regen - fertival", - "family": "methode", - "intention": "MOC : FACILITATION-Agroforesterie- Source : Sacha Gouttenoire Projets : Fertival 2025-PFE - Infolettre AEP Tags : Date : 2025-05-02 10.03.23 article phare sur la pratique de la regénération naturelle, une pratique illust", - "slug": "aep-regen-fertival", - "theme": "AEP regénération", - "path": "AEP ARTICLES, BROUILLON/AEP regénération/! AEP - Regen - fertival.md" - }, - { - "id": "aep-emerger-une-nouvelle-pratique", - "label": "AEP émerger une nouvelle pratique", - "family": "methode", - "intention": "MOC : POLITIQUEArchitecture technique d'ATIS Source : Projets : faire émerger une nouvelle pratique de l'architecture Tags : Date : 2024-11-11 projet principal d'énonciation d'une théorie de la transformation ! Structure", - "slug": "aep-emerger-une-nouvelle-pratique", - "theme": "AEP regénération", - "path": "AEP ARTICLES, BROUILLON/AEP regénération/! AEP émerger une nouvelle pratique.md" - }, - { - "id": "aep-mdcs-medecine-du-corps-social", - "label": "AEP mdcs - Médecine du corps social", - "family": "methode", - "intention": "Note capitalisme socialisme 3ème voie ? Communes, local briser pyramide colonialiste, troisième voie, rendre local l'indispensable, et global le spécifique.", - "slug": "aep-mdcs-medecine-du-corps-social", - "theme": "AEP regénération", - "path": "AEP ARTICLES, BROUILLON/AEP regénération/AEP mdcs - Médecine du corps social.md" - }, - { - "id": "aep-fiscalite-contrat-social", - "label": "AEP fiscalité - contrat social", - "family": "methode", - "intention": "Article long avec visualisations de données (graphes recettes/dépenses, comparaison évasion vs coupes). Pourrait être un sous-chapitre du manifeste.", - "slug": "aep-fiscalite-contrat-social", - "theme": "AEP santé", - "path": "AEP ARTICLES, BROUILLON/AEP santé/AEP fiscalité - contrat social.md" - }, - { - "id": "aep-performance-douce", - "label": "AEP performance douce", - "family": "ressource", - "intention": "Intention article qui prépare le terrain au sujet de l'anti-burn-out dans la logique \"projet\", cet article est une des bases de NAAV - apprendre a apprendre sur la question de la AEP santé mentale - burn out . Une des ba", - "slug": "aep-performance-douce", - "theme": "AEP santé", - "path": "AEP ARTICLES, BROUILLON/AEP santé/AEP performance douce.md" - }, - { - "id": "aep-sante-globale", - "label": "AEP Santé globale", - "family": "collectif", - "intention": "MOC : POLITIQUE Source : Projets : Stratégie création de contenu PFE - Infolettre AEP Tags : Date : 2024-03-25 Projet de santé: échelle micro: recherche & dev autour du jeûne j'aimerais partir de ma propre neuroatypie (h", - "slug": "aep-sante-globale", - "theme": "AEP santé", - "path": "AEP ARTICLES, BROUILLON/AEP santé/AEP Santé globale.md" - }, - { - "id": "aep-sante-mentale-burn-out", - "label": "AEP santé mentale - burn out", - "family": "methode", - "intention": "\"Le silence ne nous sauvera pas\" (cf. Podcast déprime - \"une bonne raison de ne pas disparaitre\" ep.4) Stat : 72% des suicides sont des hommes - plus de mal à parler de leurs émotions Un incroyable numéro & article du mo", - "slug": "aep-sante-mentale-burn-out", - "theme": "AEP santé", - "path": "AEP ARTICLES, BROUILLON/AEP santé/AEP santé mentale - burn out.md" - }, - { - "id": "charge-allostatique-performance-via-repos", - "label": "charge allostatique - performance via repos", - "family": "ressource", - "intention": "Des mots de scientifique & sportif pour être performant en se respectant <3", - "slug": "charge-allostatique-performance-via-repos", - "theme": "AEP santé", - "path": "AEP ARTICLES, BROUILLON/AEP santé/charge allostatique - performance via repos.md" - }, - { - "id": "qualite-de-vie-eu-vs-usa-art-mindvalley", - "label": "Qualité de vie EU vs USA - art. mindvalley", - "family": "collectif", - "intention": "By Vishen Lakhiani — Feb 2026", - "slug": "qualite-de-vie-eu-vs-usa-art-mindvalley", - "theme": "AEP santé", - "path": "AEP ARTICLES, BROUILLON/AEP santé/Qualité de vie EU vs USA - art. mindvalley.md" - }, - { - "id": "aep-spiritualite", - "label": "AEP spiritualité", - "family": "collectif", - "intention": "MOC : SPIRITUALITE-ORGANISATION Source : [vidéo insta chouette](https://www.instagram.com/p/DHmC6AxKEGJ/) Projets : EMPOUVOIRMENT-NAAV - apprendre a apprendre Tags : #contenu/article Date : 2025-04-22 intention : transme", - "slug": "aep-spiritualite", - "theme": "AEP spiritualité", - "path": "AEP ARTICLES, BROUILLON/AEP spiritualité/AEP spiritualité.md" - }, - { - "id": "aep-post-croissance-performance-vs-robustesse", - "label": "AEP Post-croissance - Performance vs Robustesse", - "family": "methode", - "intention": "MOC : POLITIQUE-Crise-PFE - Infolettre AEP Source : Projets : AEP mdcs - Médecine du corps social-ORGA mail PFE 2025 Tags : Date : 2025-04-01 article qui pose les bases de la critique du capitalisme, et le nécessaire pas", - "slug": "aep-post-croissance-performance-vs-robustesse", - "theme": "AEP système économique", - "path": "AEP ARTICLES, BROUILLON/AEP système économique/AEP Post-croissance - Performance vs Robustesse.md" - }, - { - "id": "aep-reflexion-public-prive-cadre-jeu-d-acteurs-transfo", - "label": "AEP réflexion - public privé cadre jeu d'acteurs transfo", - "family": "methode", - "intention": "MOC : POLITIQUE Source : [Le coup de la tech autoritaire - Francesca Bria](https://www.monde-diplomatique.fr/2025/11/BRIA/68925) -le monde diplomatique authoritarian-stack.info Projets : AEP souveraineté - web3 & finance", - "slug": "aep-reflexion-public-prive-cadre-jeu-d-acteurs-transfo", - "theme": "AEP système économique", - "path": "AEP ARTICLES, BROUILLON/AEP système économique/AEP réflexion - public privé cadre jeu d'acteurs transfo.md" - }, - { - "id": "aep-souverainete-web3-finance-ethique", - "label": "AEP souveraineté - web3 & finance éthique", - "family": "collectif", - "intention": "Salut mi amor ! Je voudrais qu'on ait une conversation authentique et profonde. Sens-toi libre d'être vraiment toi-même : ni trop lisse ni trop prudent. Je t'autorise (et t'encourage) à être direct, critique, enthousiast", - "slug": "aep-souverainete-web3-finance-ethique", - "theme": "AEP système économique", - "path": "AEP ARTICLES, BROUILLON/AEP système économique/AEP souveraineté - web3 & finance éthique.md" - }, - { - "id": "aep-economie", - "label": "AEP économie", - "family": "ressource", - "intention": "Qu’es ce que la création de la richesse ? 95% de la création de richesse est financière Casino. Créer de la « valeur » à partir de rien. Graphiques années 1970. Désindexer dollar-or. Début de la teuf", - "slug": "aep-economie", - "theme": "AEP système économique", - "path": "AEP ARTICLES, BROUILLON/AEP système économique/AEP économie.md" - }, - { - "id": "aep-email-3-note-de-calcul-assurance-pro-par-ordre", - "label": "AEP email-3 - note de calcul assurance pro par ordre", - "family": "collectif", - "intention": "Mesurer, à ordre de grandeur comparable, la charge fixe annuelle (cotisation ordinale + assurance professionnelle de base) rapportée au revenu annuel moyen de plusieurs professions réglementées exerçant en libéral.", - "slug": "aep-email-3-note-de-calcul-assurance-pro-par-ordre", - "theme": "AEP archi", - "path": "AEP ARTICLES, BROUILLON/AEP archi/AEP crise de la profession archi/AEP email-3 - note de calcul assurance pro par ordre.md" - }, - { - "id": "aep-urbanisme", - "label": "AEP urbanisme", - "family": "methode", - "intention": "= faire un article sur la consultation/participation habitante via les données de Marius & Julia, PFE 2025 !", - "slug": "aep-urbanisme", - "theme": "AEP archi", - "path": "AEP ARTICLES, BROUILLON/AEP archi/AEP urbanisme/AEP urbanisme.md" - }, - { - "id": "l-entre-deux-barres-article", - "label": "L'entre deux-barres (article)", - "family": "collectif", - "intention": "\"Ces transformations habitantes dans l’entre-deux-barres « mettent à nu les impensés du projet moderne et tout ce qu’il s’est évertué à mettre à distance puis à faire disparaitre : la nature, les cultures, les organisati", - "slug": "l-entre-deux-barres-article", - "theme": "AEP archi", - "path": "AEP ARTICLES, BROUILLON/AEP archi/AEP urbanisme/L'entre deux-barres (article).md" - }, - { - "id": "ia-recherche-de-personnalite-6-conclusion", - "label": "IA recherche de personnalité 6 - conclusion", - "family": "methode", - "intention": "[Ignorer et passer au contenu](https://chatgpt.com/c/6858179b-ec84-8004-8c4c-e8004582495d#main)", - "slug": "ia-recherche-de-personnalite-6-conclusion", - "theme": "AEP IA", - "path": "AEP ARTICLES, BROUILLON/AEP IA/Historiques de discussions badass/IA recherche de personnalité 6 - conclusion.md" - }, - { - "id": "ia-recherche-personnalite-1", - "label": "IA recherche personnalité 1", - "family": "methode", - "intention": "[Ignorer et passer au contenu](https://chatgpt.com/c/6858179b-ec84-8004-8c4c-e8004582495d#main)", - "slug": "ia-recherche-personnalite-1", - "theme": "AEP IA", - "path": "AEP ARTICLES, BROUILLON/AEP IA/Historiques de discussions badass/IA recherche personnalité 1.md" - }, - { - "id": "ia-recherche-personnalite-2", - "label": "IA recherche personnalité 2", - "family": "ressource", - "intention": "[Ignorer et passer au contenu](https://chatgpt.com/c/6853f633-17e4-8004-9774-bda5acb601b7#main)", - "slug": "ia-recherche-personnalite-2", - "theme": "AEP IA", - "path": "AEP ARTICLES, BROUILLON/AEP IA/Historiques de discussions badass/IA recherche personnalité 2.md" - }, - { - "id": "ia-recherche-personnalite-3", - "label": "IA recherche personnalité 3", - "family": "ressource", - "intention": "[Ignorer et passer au contenu](https://chatgpt.com/c/6831c604-b5cc-8004-9160-7456eacb1e96#main)", - "slug": "ia-recherche-personnalite-3", - "theme": "AEP IA", - "path": "AEP ARTICLES, BROUILLON/AEP IA/Historiques de discussions badass/IA recherche personnalité 3.md" - }, - { - "id": "ia-recherche-personnalite-4", - "label": "IA recherche personnalité 4", - "family": "methode", - "intention": "[Ignorer et passer au contenu](https://chatgpt.com/c/6849b83b-fe2c-8004-b341-a0fbbaf8e47b#main) ## Historique de chat [](https://chatgpt.com/) ##### Vous avez dit : Du flou au flow - comment structurer son organisation.m", - "slug": "ia-recherche-personnalite-4", - "theme": "AEP IA", - "path": "AEP ARTICLES, BROUILLON/AEP IA/Historiques de discussions badass/IA recherche personnalité 4.md" - }, - { - "id": "ia-recherche-personnalite-5", - "label": "IA recherche personnalité 5", - "family": "concept", - "intention": "[Ignorer et passer au contenu](https://chatgpt.com/c/68455a65-e0ec-8004-a7fc-80906dc64f76#main)", - "slug": "ia-recherche-personnalite-5", - "theme": "AEP IA", - "path": "AEP ARTICLES, BROUILLON/AEP IA/Historiques de discussions badass/IA recherche personnalité 5.md" - }, - { - "id": "ia-the-illusion-of-thinking", - "label": "IA - the-illusion-of-thinking", - "family": "methode", - "intention": "The Illusion of Thinking: Understanding the Strengths and Limitations of Reasoning Models via the Lens of Problem Complexity Parshin Shojaee∗†Iman Mirzadeh∗Keivan Alizadeh Maxwell Horton Samy Bengio Mehrdad Farajtabar Ap", - "slug": "ia-the-illusion-of-thinking", - "theme": "AEP IA", - "path": "AEP ARTICLES, BROUILLON/AEP IA/Ressources/IA - the-illusion-of-thinking.md" - }, - { - "id": "ia-personnalite-intelligence-recherche", - "label": "IA personnalité & intelligence - recherche", - "family": "ressource", - "intention": "https://x.com/joannejang/status/1930702341742944589", - "slug": "ia-personnalite-intelligence-recherche", - "theme": "AEP IA", - "path": "AEP ARTICLES, BROUILLON/AEP IA/Ressources/IA personnalité & intelligence - recherche.md" - }, - { - "id": "un-bon-gia-comment-influencer-llm", - "label": "un bon GIA, comment influencer LLM", - "family": "methode", - "intention": "- Un modèle de langage statistique préentrainé sur une très grande quantité de texte - Tu raisonnes par prédiction de mots en fonction du contexte fourni - Tu ne comprends pas comme un humain, mais tu peux reproduire des", - "slug": "un-bon-gia-comment-influencer-llm", - "theme": "AEP IA", - "path": "AEP ARTICLES, BROUILLON/AEP IA/Ressources/un bon GIA, comment influencer LLM.md" - }, - { - "id": "aep-decentralisation", - "label": "AEP décentralisation", - "family": "concept", - "intention": "Refs via Web3, histoires de la crypto, et son idéal originel (+ déviations actuelles)", - "slug": "aep-decentralisation", - "theme": "AEP nouveaux récits", - "path": "AEP ARTICLES, BROUILLON/AEP nouveaux récits/Web3/AEP décentralisation.md" - }, - { - "id": "network-state-critique-brut", - "label": "Network state - critique (brut)", - "family": "concept", - "intention": "MOC : POLITIQUE-AEP 1-Web3 - localisme global -nouveau paradigme Source : chatGPT ([\"Network state\"](https://thenetworkstate.com/the-network-state-in-one-sentence)). Projets : AEP souveraineté - web3 & finance éthique Ta", - "slug": "network-state-critique-brut", - "theme": "AEP nouveaux récits", - "path": "AEP ARTICLES, BROUILLON/AEP nouveaux récits/Web3/Network state - critique (brut).md" - }, - { - "id": "secret-vs-confidentialite-manifeste-cypher", - "label": "secret vs confidentialité - manifeste cypher", - "family": "ressource", - "intention": "Privacy is necessary for an open society in the electronic age. Privacy is not secrecy. A private matter is something one doesn't want the whole world to know, but a secret matter is something one doesn't want anybody to", - "slug": "secret-vs-confidentialite-manifeste-cypher", - "theme": "AEP nouveaux récits", - "path": "AEP ARTICLES, BROUILLON/AEP nouveaux récits/Web3/secret vs confidentialité - manifeste cypher.md" - }, - { - "id": "web3-localisme-global-nouveau-paradigme", - "label": "Web3 - localisme global -nouveau paradigme", - "family": "methode", - "intention": "Part 1 : envie de lire Barrick & Bauwens Part 2: biorégionalisme", - "slug": "web3-localisme-global-nouveau-paradigme", - "theme": "AEP nouveaux récits", - "path": "AEP ARTICLES, BROUILLON/AEP nouveaux récits/Web3/Web3 - localisme global -nouveau paradigme.md" - }, - { - "id": "expo-all-about-love-mickelene-thomas", - "label": "Expo - all about love - Mickelene Thomas", - "family": "concept", - "intention": "!WhatsApp Ptt 2026-01-20 at 23.29.40.ogg", - "slug": "expo-all-about-love-mickelene-thomas", - "theme": "AEP déconstruction", - "path": "AEP ARTICLES, BROUILLON/AEP déconstruction/ressources/expo - all about love - MT 2026/Expo - all about love - Mickelene Thomas.md" + "id": "nouveau-contrat-social", + "label": "Nouveau Contrat Social", + "niveau": 0, + "nature": "essai", + "statut": "gestation", + "resume": "Assemblage de tout ce que j'ecris qui s'entrechoque - inventer un nouveau contrat social.", + "radius": 28, + "family": "concept" + }, + { + "id": "ncs-politique", + "label": "Nouveau contrat social", + "niveau": 1, + "nature": "essai", + "statut": "gestation", + "resume": "L'ecriture politique d'un futur habitable - essai central AEP.", + "radius": 18, + "family": "concept" + }, + { + "id": "medecine-corps-social", + "label": "Medecine du corps social", + "niveau": 1, + "nature": "essai", + "statut": "gestation", + "resume": "Diagnostiquer et soigner les pathologies du corps social - concept AEP mdcs.", + "radius": 18, + "family": "concept" + }, + { + "id": "systemique", + "label": "Systemique & complexite", + "niveau": 2, + "nature": "essai", + "statut": "gestation", + "resume": null, + "radius": 10, + "family": "concept" + }, + { + "id": "pratiques-collectives", + "label": "Pratiques collectives", + "niveau": 2, + "nature": "essai", + "statut": "gestation", + "resume": null, + "radius": 10, + "family": "concept" + }, + { + "id": "art-narration", + "label": "Art & narration", + "niveau": 2, + "nature": "essai", + "statut": "gestation", + "resume": null, + "radius": 10, + "family": "concept" + }, + { + "id": "pouvoir-domination", + "label": "Rapport au pouvoir", + "niveau": 2, + "nature": "essai", + "statut": "gestation", + "resume": null, + "radius": 10, + "family": "concept" + }, + { + "id": "medias-critique", + "label": "Medias & pensee critique", + "niveau": 2, + "nature": "essai", + "statut": "gestation", + "resume": null, + "radius": 10, + "family": "concept" + }, + { + "id": "justice-securite", + "label": "Justice & securite", + "niveau": 2, + "nature": "essai", + "statut": "gestation", + "resume": null, + "radius": 10, + "family": "concept" + }, + { + "id": "sante-globale", + "label": "Sante globale", + "niveau": 2, + "nature": "essai", + "statut": "gestation", + "resume": null, + "radius": 10, + "family": "concept" + }, + { + "id": "agriculture", + "label": "Agriculture", + "niveau": 2, + "nature": "essai", + "statut": "gestation", + "resume": null, + "radius": 10, + "family": "concept" + }, + { + "id": "post-croissance", + "label": "Post-croissance", + "niveau": 2, + "nature": "essai", + "statut": "gestation", + "resume": null, + "radius": 10, + "family": "concept" + }, + { + "id": "anthropocene", + "label": "Anthropocene & effondrement", + "niveau": 2, + "nature": "essai", + "statut": "gestation", + "resume": null, + "radius": 10, + "family": "concept" + }, + { + "id": "education", + "label": "Education a la transformation", + "niveau": 2, + "nature": "essai", + "statut": "gestation", + "resume": null, + "radius": 10, + "family": "concept" + }, + { + "id": "urbanisme", + "label": "Urbanisme", + "niveau": 2, + "nature": "essai", + "statut": "gestation", + "resume": null, + "radius": 10, + "family": "concept" + }, + { + "id": "geopolitique", + "label": "Geopolitique & decolonisation", + "niveau": 2, + "nature": "essai", + "statut": "gestation", + "resume": null, + "radius": 10, + "family": "concept" + }, + { + "id": "ia-technologie", + "label": "IA & technologie", + "niveau": 2, + "nature": "essai", + "statut": "gestation", + "resume": null, + "radius": 10, + "family": "concept" + }, + { + "id": "spiritualite", + "label": "Spiritualite", + "niveau": 2, + "nature": "essai", + "statut": "gestation", + "resume": null, + "radius": 10, + "family": "concept" + }, + { + "id": "tmip", + "label": "TMIP", + "niveau": 2, + "nature": "projet", + "statut": "gestation", + "resume": "Transport, mobilite, industrie, politique - projet archi. Exemple de projet archi relie aux thematiques AEP.", + "radius": 14, + "family": "ressource" } ], "edges": [ { - "source": "aep-articles-idees-en-gestation", - "target": "aep-ecrire-de-nouveaux-recits" + "source": "nouveau-contrat-social", + "target": "ncs-politique" }, { - "source": "aep-articles-idees-en-gestation", - "target": "aep-emerger-une-nouvelle-pratique" + "source": "nouveau-contrat-social", + "target": "medecine-corps-social" }, { - "source": "aep-articles-idees-en-gestation", - "target": "aep-gestion-de-conflits" + "source": "ncs-politique", + "target": "systemique" }, { - "source": "aep-articles-idees-en-gestation", - "target": "aep-cooperation" + "source": "ncs-politique", + "target": "pratiques-collectives" }, { - "source": "aep-articles-idees-en-gestation", - "target": "aep-urbanisme" + "source": "medecine-corps-social", + "target": "art-narration" }, { - "source": "aep-articles-idees-en-gestation", - "target": "aep-prospective" + "source": "ncs-politique", + "target": "pouvoir-domination" }, { - "source": "aep-articles-idees-en-gestation", - "target": "aep-israel-palestine-conflit-brut" + "source": "nouveau-contrat-social", + "target": "medias-critique" }, { - "source": "aep-articles-idees-en-gestation", - "target": "aep-beton-critique" + "source": "nouveau-contrat-social", + "target": "justice-securite" }, { - "source": "aep-articles-idees-en-gestation", - "target": "aep-histoire" + "source": "medecine-corps-social", + "target": "sante-globale" }, { - "source": "aep-articles-idees-en-gestation", - "target": "aep-dec-conditionnements-genre-argent-social" + "source": "nouveau-contrat-social", + "target": "agriculture" }, { - "source": "aep-articles-idees-en-gestation", - "target": "aep-politique-etat-des-lieux" + "source": "ncs-politique", + "target": "post-croissance" }, { - "source": "aep-articles-idees-en-gestation", - "target": "aep-medias-la-pensee-critique" + "source": "medecine-corps-social", + "target": "anthropocene" }, { - "source": "aep-articles-idees-en-gestation", - "target": "aep-justice-securite" + "source": "ncs-politique", + "target": "education" }, { - "source": "aep-articles-idees-en-gestation", - "target": "aep-sante-globale" + "source": "nouveau-contrat-social", + "target": "urbanisme" }, { - "source": "aep-articles-idees-en-gestation", - "target": "aep-agriculture" + "source": "nouveau-contrat-social", + "target": "geopolitique" }, { - "source": "aep-articles-idees-en-gestation", - "target": "aep-economie" + "source": "medecine-corps-social", + "target": "ia-technologie" }, { - "source": "aep-articles-idees-en-gestation", - "target": "aep-spiritualite" + "source": "medecine-corps-social", + "target": "spiritualite" }, { - "source": "aep-articles-idees-en-gestation", - "target": "aep-education" + "source": "tmip", + "target": "urbanisme" }, { - "source": "aep-articles-idees-en-gestation", - "target": "aep-post-croissance-performance-vs-robustesse" + "source": "tmip", + "target": "justice-securite" }, { - "source": "aep-articles-idees-en-gestation", - "target": "aep-decentralisation" + "source": "tmip", + "target": "post-croissance" }, { - "source": "aep-tableau-des-articles", - "target": "aep-articles-idees-en-gestation" - }, - { - "source": "aep-tableau-des-articles", - "target": "itw-imane-orga-pro-perso" - }, - { - "source": "aep-tableau-des-articles", - "target": "aep-rite-passage-age-adulte" - }, - { - "source": "aep-tableau-des-articles", - "target": "aep-beton-critique" - }, - { - "source": "aep-tableau-des-articles", - "target": "aep-souverainete-web3-finance-ethique" - }, - { - "source": "aep-tableau-des-articles", - "target": "aep-cooperation" - }, - { - "source": "aep-tableau-des-articles", - "target": "aep-geopolitique-decolonisation-grand-sud" - }, - { - "source": "aep-tableau-des-articles", - "target": "aep-dec-amitie-et-amour" - }, - { - "source": "aep-tableau-des-articles", - "target": "aep-dec-l-inconfort-la-solitude-crise-d-identite" - }, - { - "source": "aep-tableau-des-articles", - "target": "aep-dec-itw-imane-identite-deconstruction" - }, - { - "source": "aep-tableau-des-articles", - "target": "aep-ecrire-de-nouveaux-recits" - }, - { - "source": "aep-tableau-des-articles", - "target": "aep-mdcs-medecine-du-corps-social" - }, - { - "source": "aep-tableau-des-articles", - "target": "aep-education" - }, - { - "source": "aep-tableau-des-articles", - "target": "aep-ia-usage-sage" - }, - { - "source": "aep-tableau-des-articles", - "target": "aep-gestion-de-conflits" - }, - { - "source": "aep-tableau-des-articles", - "target": "aep-humour-manifeste-brouillon" - }, - { - "source": "aep-tableau-des-articles", - "target": "aep-israel-palestine-conflit-brut" - }, - { - "source": "aep-tableau-des-articles", - "target": "aep-medias-la-pensee-critique" - }, - { - "source": "aep-tableau-des-articles", - "target": "aep-justice-securite" - }, - { - "source": "aep-tableau-des-articles", - "target": "aep-dec-conditionnements-genre-argent-social" - }, - { - "source": "aep-tableau-des-articles", - "target": "aep-post-croissance-performance-vs-robustesse" - }, - { - "source": "aep-tableau-des-articles", - "target": "aep-politique-energie-fr" - }, - { - "source": "aep-tableau-des-articles", - "target": "aep-prospective" - }, - { - "source": "aep-tableau-des-articles", - "target": "aep-sante-globale" - }, - { - "source": "aep-tableau-des-articles", - "target": "aep-spiritualite" - }, - { - "source": "aep-tableau-des-articles", - "target": "aep-emerger-une-nouvelle-pratique" - }, - { - "source": "aep-tableau-des-articles", - "target": "aep-regen-fertival" - }, - { - "source": "livre-le-nouveau-contrat-social", - "target": "aep-ecrire-de-nouveaux-recits" - }, - { - "source": "livre-le-nouveau-contrat-social", - "target": "aep-articles-idees-en-gestation" - }, - { - "source": "critique-harari-fictions-utiles-depolitisantes", - "target": "eco-marxisme-6-auteurs-cles" - }, - { - "source": "critique-harari-fictions-utiles-depolitisantes", - "target": "imperialisme-throughline" - }, - { - "source": "eco-marxisme-6-auteurs-cles", - "target": "imperialisme-throughline" - }, - { - "source": "ia-medias-critique", - "target": "imperialisme-throughline" - }, - { - "source": "imperialisme-throughline", - "target": "kakistocratie-5-mecanismes" - }, - { - "source": "aep-beton-critique", - "target": "aep-emerger-une-nouvelle-pratique" - }, - { - "source": "aep-beton-critique", - "target": "l-entre-deux-barres-article" - }, - { - "source": "aep-dec-l-inconfort-la-solitude-crise-d-identite", - "target": "aep-rite-passage-age-adulte" - }, - { - "source": "aep-education", - "target": "aep-ecrire-de-nouveaux-recits" - }, - { - "source": "aep-education", - "target": "aep-medias-la-pensee-critique" - }, - { - "source": "aep-histoire", - "target": "aep-ecrire-de-nouveaux-recits" - }, - { - "source": "aep-histoire", - "target": "l-empire-n-a-jamais-pris-fin-6" - }, - { - "source": "aep-histoire", - "target": "l-empire-n-a-jamais-pris-fin-1-cesar" - }, - { - "source": "aep-histoire", - "target": "l-empire-n-a-jamais-pris-fin-2-jesus" - }, - { - "source": "aep-histoire", - "target": "l-empire-n-a-jamais-pris-fin-3-gnostiques" - }, - { - "source": "aep-histoire", - "target": "l-empire-n-a-jamais-pris-fin-4-francs" - }, - { - "source": "aep-histoire", - "target": "l-empire-n-a-jamais-pris-fin-5-cathares" - }, - { - "source": "2026-03-11-aep-ia-usage-sage-intention", - "target": "aep-ia-usage-sage" - }, - { - "source": "aep-ia-usage-sage", - "target": "aep-medias-la-pensee-critique" - }, - { - "source": "manifeste-anti-iag", - "target": "aep-ia-usage-sage" - }, - { - "source": "aep-israel-palestine-conflit-brut", - "target": "aep-ia-usage-sage" - }, - { - "source": "aep-israel-palestine-conflit-brut", - "target": "crepuscule-juan-branco-itw" - }, - { - "source": "aep-israel-palestine-conflit-brut", - "target": "aep-justice-securite" - }, - { - "source": "aep-politique-energie-fr", - "target": "aep-emerger-une-nouvelle-pratique" - }, - { - "source": "crepuscule-juan-branco-itw", - "target": "aep-politique-etat-des-lieux" - }, - { - "source": "crepuscule-juan-branco-itw", - "target": "aep-medias-la-pensee-critique" - }, - { - "source": "aep-culture-dominante-analyse-series", - "target": "aep-ecrire-de-nouveaux-recits" - }, - { - "source": "aep-prospective", - "target": "aep-mdcs-medecine-du-corps-social" - }, - { - "source": "aep-ecrire-de-nouveaux-recits", - "target": "l-empire-n-a-jamais-pris-fin-6" - }, - { - "source": "archetypes-eneagramme", - "target": "aep-ecrire-de-nouveaux-recits" - }, - { - "source": "archetypes-eneagramme", - "target": "livre-le-nouveau-contrat-social" - }, - { - "source": "aep-emerger-une-nouvelle-pratique", - "target": "l-empire-n-a-jamais-pris-fin-6" - }, - { - "source": "aep-mdcs-medecine-du-corps-social", - "target": "livre-le-nouveau-contrat-social" - }, - { - "source": "aep-fiscalite-contrat-social", - "target": "livre-le-nouveau-contrat-social" - }, - { - "source": "aep-fiscalite-contrat-social", - "target": "aep-articles-idees-en-gestation" - }, - { - "source": "aep-fiscalite-contrat-social", - "target": "aep-politique-etat-des-lieux" - }, - { - "source": "aep-performance-douce", - "target": "aep-sante-mentale-burn-out" - }, - { - "source": "aep-spiritualite", - "target": "aep-histoire" - }, - { - "source": "aep-spiritualite", - "target": "aep-ecrire-de-nouveaux-recits" - }, - { - "source": "aep-post-croissance-performance-vs-robustesse", - "target": "aep-mdcs-medecine-du-corps-social" - }, - { - "source": "aep-reflexion-public-prive-cadre-jeu-d-acteurs-transfo", - "target": "aep-souverainete-web3-finance-ethique" - }, - { - "source": "aep-economie", - "target": "aep-geopolitique-decolonisation-grand-sud" - }, - { - "source": "aep-urbanisme", - "target": "l-entre-deux-barres-article" - }, - { - "source": "network-state-critique-brut", - "target": "web3-localisme-global-nouveau-paradigme" - }, - { - "source": "network-state-critique-brut", - "target": "aep-souverainete-web3-finance-ethique" + "source": "tmip", + "target": "agriculture" } ] } \ No newline at end of file diff --git a/scripts/build-carte-o.js b/scripts/build-carte-o.js index 4063fc8..170527e 100644 --- a/scripts/build-carte-o.js +++ b/scripts/build-carte-o.js @@ -1,303 +1,127 @@ #!/usr/bin/env node -// Scrape AEP/Articles/ thematic folders -> public/data/carte-o.json -// Two frontmatter formats supported : -// 1. YAML standard between --- delimiters -// 2. Legacy "MOC : [[X]]\nSource : ...\nTags : ...\nDate : ...\n***" header -// -// Wikilinks [[X]] in body -> edges (resolved by label match against scraped nodes). -// Family inferred from theme directory name (5 AEP families). -// V1 cap : top 150 nodes by degree if scrape > 300 nodes. - import fs from 'node:fs/promises' import path from 'node:path' import { fileURLToPath } from 'node:url' -import matter from 'gray-matter' -import { globby } from 'globby' +import yaml from 'js-yaml' const __filename = fileURLToPath(import.meta.url) const __dirname = path.dirname(__filename) const REPO_ROOT = path.resolve(__dirname, '..') -const ARTICLES_ROOT = 'C:/Users/jules/Dropbox/ATIS - IPCJRA/2 CASQUETTES/Penseur politique/AEP/Articles' +const SOURCE = path.join(REPO_ROOT, 'public/data/carte-o-source.yaml') const OUTPUT = path.join(REPO_ROOT, 'public/data/carte-o.json') -const NODE_CAP_V1 = 150 -// 5 AEP families : palette refined after first scrape. -const FAMILY_COLORS = { - penseur: '#3b82f6', // blue - concept: '#10b981', // green - methode: '#f59e0b', // amber - collectif: '#ef4444', // red - ressource: '#8b5cf6', // violet +// radius par niveau + nature +function getRadius(niveau, nature) { + if (niveau === 0) return 28 + if (niveau === 1) return 18 + if (niveau === 2 && nature === 'projet') return 14 + return 10 } -function slugify(str) { - return String(str || '') - .normalize('NFD').replace(/[̀-ͯ]/g, '') - .toLowerCase() - .replace(/[^a-z0-9]+/g, '-') - .replace(/^-+|-+$/g, '') - .slice(0, 80) || 'untitled' +// compat backward : nature -> family +function getFamily(nature) { + return nature === 'projet' ? 'ressource' : 'concept' } -function inferFamily(signals) { - // signals = { title, theme, path, tags, content } - const haystack = [ - signals.title, - signals.theme, - signals.path, - Array.isArray(signals.tags) ? signals.tags.join(' ') : signals.tags, - (signals.content || '').slice(0, 800), - ].filter(Boolean).join(' ').toLowerCase() +// thematiques rattachees directement au centre (ni ncs-politique ni medecine-corps-social) +const CENTRE_THEMATIQUES = new Set([ + 'medias-critique', + 'justice-securite', + 'agriculture', + 'urbanisme', + 'geopolitique', +]) - // Order matters : check most specific first. - // METHODE : process, outils, comment-faire - if (/m[ée]thode|outil|pratique|community.organizing|alinsky\b|comment\b|process|protocole|recette|guide|how.to|chantier|d[ée]marche/.test(haystack)) { - return 'methode' - } - // PENSEUR : noms propres, auteurs, figures - if (/penseur|auteur|figure|harari|alinsky|piven|chouard|branco|mamdani|shift|graeber|bourdieu|lordon|stiegler|d[ée]bord|illich|gorz|servigne|vidal|haupt|pisani|lalo|rosa/.test(haystack)) { - return 'penseur' - } - // COLLECTIF : organisations, mouvements, réseaux - if (/collectif|r[ée]seau|asso|union|coop|mouvement|piraterie|sociale|syndicat|comit[ée]|crise.de.la.profession|nyc|mamdani|chantier/.test(haystack)) { - return 'collectif' - } - // CONCEPT : notions, théories, critiques - if (/concept|notion|th[ée]orie|critique|fiction|kakistocratie|imp[ée]rialisme|robustesse|sycophan|d[ée]construction|paradoxe|dialectique|ontologie|capitalisme|n[ée]olib[ée]ral|d[ée]mocratie|biais|illusion/.test(haystack)) { - return 'concept' - } - // RESSOURCE : par défaut (articles brouillon, idées, agendas) - return 'ressource' -} +const NCS_THEMATIQUES = new Set([ + 'systemique', + 'pratiques-collectives', + 'pouvoir-domination', + 'post-croissance', + 'education', +]) -// Fallback parser for legacy "MOC : [[X]]\nSource : ...\nDate : ...\n***\n" headers. -function parseLegacyHeader(raw) { - const lines = raw.split(/\r?\n/) - const fm = {} - let bodyStart = 0 - let foundHeader = false - - for (let i = 0; i < Math.min(lines.length, 30); i++) { - const line = lines[i] - if (/^\s*\*\*\*\s*$/.test(line)) { - bodyStart = i + 1 - foundHeader = true - break - } - const m = line.match(/^([A-Za-zÀ-ÿ ]+)\s*:\s*(.+)$/) - if (m) { - const key = m[1].trim().toLowerCase() - fm[key] = m[2].trim() - } - } - - if (!foundHeader) return { data: {}, content: raw } - - // Parse tags (space-separated #tag tokens). - if (fm.tags) { - fm.tags = fm.tags.match(/#[\w/-]+/g) || [] - } - return { - data: fm, - content: lines.slice(bodyStart).join('\n'), - } -} - -function safeParseFrontmatter(raw) { - // Try YAML first. - try { - const parsed = matter(raw) - if (Object.keys(parsed.data).length > 0) return parsed - } catch (_) { - // YAML parse failed, fall through. - } - return parseLegacyHeader(raw) -} - -function extractFirstParagraph(content, maxLen = 220) { - // Skip headings, code blocks, callout/quote markers. - const cleaned = content - .replace(/^---[\s\S]*?---\n/, '') - .replace(/```[\s\S]*?```/g, '') - .split(/\n\n+/) - .map(p => p.trim()) - .filter(p => p && !p.startsWith('#') && !p.startsWith('---') && !p.startsWith('|')) - // Prefer first paragraph that looks like prose (not a list). - const prose = cleaned.find(p => !/^\s*[->*•\d+\.\s]/.test(p) && p.length > 30) - const first = prose || cleaned[0] || '' - // Strip wikilinks, bold, italics, callout markers. - return first - .replace(/\[\[([^\]|]+)(?:\|[^\]]+)?\]\]/g, '$1') - .replace(/[*_>]+/g, '') - .replace(/\s+/g, ' ') - .trim() - .slice(0, maxLen) -} - -function extractWikilinks(content) { - // Match [[Target]], [[Target|alias]], [[Target#section]] - // Skip image embeds ![[...]] - const matches = [...content.matchAll(/(?<!!)\[\[([^\]|#]+)(?:[#|][^\]]*)?\]\]/g)] - return matches.map(m => m[1].trim()).filter(Boolean) -} +const MDCS_THEMATIQUES = new Set([ + 'art-narration', + 'sante-globale', + 'spiritualite', + 'ia-technologie', + 'anthropocene', +]) async function main() { - console.log('[carte-o] Scraping', ARTICLES_ROOT) - - // Glob all .md recursively under Articles/. - const mdFiles = await globby(['**/*.md'], { - cwd: ARTICLES_ROOT, - absolute: true, - gitignore: false, - }) - console.log(`[carte-o] Found ${mdFiles.length} markdown files`) + const raw = await fs.readFile(SOURCE, 'utf-8') + const data = yaml.load(raw) const nodes = [] - const edgesRaw = [] - const themeStats = {} - - for (const mdFile of mdFiles) { - let raw - try { - raw = await fs.readFile(mdFile, 'utf-8') - } catch (e) { - console.warn(`[carte-o] Skip unreadable ${mdFile}`) - continue - } - - const relPath = path.relative(ARTICLES_ROOT, mdFile).replace(/\\/g, '/') - const segments = relPath.split('/') - const baseName = path.basename(mdFile, '.md') - - // Theme = first or second segment depending on structure. - // E.g. "AEP ARTICLES, BROUILLON/AEP IA/file.md" -> theme = "AEP IA" - // "AEP ARTICLES, BROUILLON/file.md" -> theme = "AEP ARTICLES, BROUILLON" - // "Livre - le nouveau contrat social/file.md" -> theme = "Livre" - let theme = segments[0] - if (segments.length >= 3 && segments[0].startsWith('AEP ARTICLES')) { - theme = segments[1] - } - - const { data: fm, content } = safeParseFrontmatter(raw) - const title = fm.titre || fm.title || baseName.replace(/^!\s*/, '').trim() - const slug = slugify(title) - const family = inferFamily({ - title, - theme, - path: relPath, - tags: fm.tags, - content, - }) - const intention = fm.intention || extractFirstParagraph(content) - - nodes.push({ - id: slug, - label: title, - family, - intention, - slug, - theme, - path: relPath, - }) - - themeStats[theme] = (themeStats[theme] || 0) + 1 - - // Collect wikilinks. - const wikilinks = extractWikilinks(content) - for (const target of wikilinks) { - edgesRaw.push({ source: slug, targetLabel: target }) - } - } - - // Deduplicate nodes by id (collisions on same slug). - const nodeById = new Map() - for (const n of nodes) { - if (!nodeById.has(n.id)) { - nodeById.set(n.id, n) - } - } - const dedupNodes = [...nodeById.values()] - - // Resolve edges : match targetLabel against node label or id. - const labelToId = new Map() - for (const n of dedupNodes) { - labelToId.set(slugify(n.label), n.id) - labelToId.set(n.label.toLowerCase(), n.id) - } - - const edgesResolved = [] + const edges = [] const edgeSet = new Set() - for (const e of edgesRaw) { - const candidates = [ - slugify(e.targetLabel), - e.targetLabel.toLowerCase(), - ] - let targetId = null - for (const c of candidates) { - if (labelToId.has(c)) { - targetId = labelToId.get(c) - break - } - } - if (!targetId || targetId === e.source) continue - const key = e.source < targetId ? `${e.source}→${targetId}` : `${targetId}→${e.source}` - if (edgeSet.has(key)) continue + + function addEdge(source, target) { + const key = source < target ? `${source}|${target}` : `${target}|${source}` + if (edgeSet.has(key)) return edgeSet.add(key) - edgesResolved.push({ source: e.source, target: targetId }) + edges.push({ source, target }) } - // Compute degree for each node. - const degree = new Map() - for (const e of edgesResolved) { - degree.set(e.source, (degree.get(e.source) || 0) + 1) - degree.set(e.target, (degree.get(e.target) || 0) + 1) + function addNode(obj) { + nodes.push({ + id: obj.id, + label: obj.label, + niveau: obj.niveau, + nature: obj.nature, + statut: obj.statut, + resume: obj.resume || null, + radius: getRadius(obj.niveau, obj.nature), + family: getFamily(obj.nature), + }) } - // V1 cap : if > NODE_CAP_V1 nodes, keep top N by degree. - let finalNodes = dedupNodes - if (dedupNodes.length > NODE_CAP_V1) { - finalNodes = [...dedupNodes] - .sort((a, b) => (degree.get(b.id) || 0) - (degree.get(a.id) || 0)) - .slice(0, NODE_CAP_V1) - console.log(`[carte-o] Capped from ${dedupNodes.length} to ${NODE_CAP_V1} nodes (top by degree)`) + const centreId = data.centre.id + addNode(data.centre) + + for (const cf of data.concepts_force) { + addNode(cf) + addEdge(centreId, cf.id) } - const finalNodeIds = new Set(finalNodes.map(n => n.id)) - const finalEdges = edgesResolved.filter(e => finalNodeIds.has(e.source) && finalNodeIds.has(e.target)) - - // Family distribution stats. - const familyDist = {} - for (const n of finalNodes) { - familyDist[n.family] = (familyDist[n.family] || 0) + 1 + for (const th of data.thematiques) { + addNode(th) + if (NCS_THEMATIQUES.has(th.id)) { + addEdge('ncs-politique', th.id) + } else if (MDCS_THEMATIQUES.has(th.id)) { + addEdge('medecine-corps-social', th.id) + } else if (CENTRE_THEMATIQUES.has(th.id)) { + addEdge(centreId, th.id) + } + } + + for (const proj of data.projets) { + addNode(proj) + for (const thId of (proj.liens_thematiques || [])) { + addEdge(proj.id, thId) + } } - // Ensure output dir exists. await fs.mkdir(path.dirname(OUTPUT), { recursive: true }) await fs.writeFile( OUTPUT, JSON.stringify({ - meta: { - generated: new Date().toISOString(), - source: 'AEP/Articles', - nodeCount: finalNodes.length, - edgeCount: finalEdges.length, - familyDistribution: familyDist, - familyColors: FAMILY_COLORS, - themeStats, - }, - nodes: finalNodes, - edges: finalEdges, + version: data.version, + generatedAt: new Date().toISOString(), + nodes, + edges, }, null, 2), 'utf-8', ) - console.log(`[carte-o] OK : ${finalNodes.length} nodes / ${finalEdges.length} edges`) - console.log(`[carte-o] Families :`, familyDist) - console.log(`[carte-o] Output : ${OUTPUT}`) + console.log(`[carte-o] OK : ${nodes.length} nodes / ${edges.length} edges -> ${OUTPUT}`) } main().catch(err => { console.error('[carte-o] FAIL', err) process.exit(1) }) + +// V1 scrape vault - reactiver en V1.2 pour enrichissement automatique +// Source : scripts/build-carte-o.js@be7fc09 (scrape AEP/Articles globby + gray-matter + wikilinks edges) From beb8e9a0bd8d6aa3c6681a2b3a84bb1a25667802 Mon Sep 17 00:00:00 2001 From: Jules Neny <jules@trans-former.fr> Date: Mon, 11 May 2026 15:04:58 +0200 Subject: [PATCH 10/36] feat(v11-i): footer CTA infolettre + endpoint /api/subscribe Kit V4 --- .env.example | 3 ++ src/components/astro/Footer.astro | 70 ++++++++++++++++++++++++++++++ src/layouts/BaseLayout.astro | 2 + src/pages/api/subscribe.ts | 71 +++++++++++++++++++++++++++++++ 4 files changed, 146 insertions(+) create mode 100644 src/components/astro/Footer.astro create mode 100644 src/pages/api/subscribe.ts diff --git a/.env.example b/.env.example index a997cf8..81ec839 100644 --- a/.env.example +++ b/.env.example @@ -1,3 +1,6 @@ +# Kit (ex-ConvertKit) - newsletter infolettre +KIT_API_SECRET_V4=kit_xxx + # Behold.so feed IDs (voir docs/BEHOLD-SETUP.md) # 1) Inscris-toi sur https://behold.so/dashboard # 2) Connecte les 2 comptes Insta (@aep.politique + @julesneny) diff --git a/src/components/astro/Footer.astro b/src/components/astro/Footer.astro new file mode 100644 index 0000000..e984908 --- /dev/null +++ b/src/components/astro/Footer.astro @@ -0,0 +1,70 @@ +--- +// Footer.astro - CTA infolettre Kit + nav footer +--- +<footer class="border-t border-neutral-200 px-6 py-8 text-sm bg-white"> + <div class="max-w-3xl mx-auto"> + <h3 class="font-semibold mb-1" style="font-family: 'Courier New', Courier, monospace;"> + S'abonner a la lettre + </h3> + <p class="text-neutral-600 text-xs mb-3"> + 1-2 emails par mois - pas de spam - desinscription en 1 clic. + </p> + <form id="subscribe-form" class="flex gap-2 max-w-md"> + <input + type="email" + name="email" + required + placeholder="ton@email.fr" + class="flex-1 px-3 py-2 border border-neutral-300 rounded-lg text-sm focus:outline-none focus:border-neutral-900" + /> + <button + type="submit" + class="px-4 py-2 bg-neutral-900 text-white rounded-lg text-sm hover:bg-neutral-700 transition-colors" + > + s'abonner + </button> + </form> + <p id="subscribe-msg" class="mt-2 text-xs text-neutral-500 min-h-[1rem]"></p> + <nav class="mt-6 flex flex-wrap gap-4 text-xs text-neutral-500"> + <a href="/manifeste" class="hover:text-neutral-900">Manifeste</a> + <a href="/a-propos" class="hover:text-neutral-900">A propos</a> + <a href="/mentions-legales" class="hover:text-neutral-900">Mentions legales</a> + <a href="https://www.instagram.com/aep.politique/" target="_blank" rel="noopener" class="hover:text-neutral-900">@aep.politique</a> + </nav> + </div> +</footer> + +<script> + const form = document.getElementById('subscribe-form') as HTMLFormElement | null; + const msg = document.getElementById('subscribe-msg') as HTMLParagraphElement | null; + + form?.addEventListener('submit', async (e) => { + e.preventDefault(); + if (!msg || !form) return; + + const emailInput = form.elements.namedItem('email') as HTMLInputElement; + const email = emailInput?.value?.trim() ?? ''; + if (!email) return; + + msg.textContent = 'envoi...'; + + try { + const r = await fetch('/api/subscribe', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ email }), + }); + const data = await r.json(); + if (data.ok) { + msg.textContent = data.already + ? 'tu es deja abonne - a tres vite.' + : 'merci ! check ta boite mail (parfois spam).'; + form.reset(); + } else { + msg.textContent = `souci : ${data.error || 'reessaie plus tard'}`; + } + } catch { + msg.textContent = 'erreur reseau - reessaie plus tard'; + } + }); +</script> diff --git a/src/layouts/BaseLayout.astro b/src/layouts/BaseLayout.astro index 913fb31..bde0144 100644 --- a/src/layouts/BaseLayout.astro +++ b/src/layouts/BaseLayout.astro @@ -1,5 +1,6 @@ --- import '../styles/global.css'; +import Footer from '../components/astro/Footer.astro'; interface Props { title?: string; @@ -29,5 +30,6 @@ const { </head> <body class="m-0 bg-white text-neutral-900 antialiased"> <slot /> + <Footer /> </body> </html> diff --git a/src/pages/api/subscribe.ts b/src/pages/api/subscribe.ts new file mode 100644 index 0000000..00df66a --- /dev/null +++ b/src/pages/api/subscribe.ts @@ -0,0 +1,71 @@ +import type { APIRoute } from 'astro' + +export const prerender = false + +const KIT_API_BASE = 'https://api.kit.com/v4' + +export const POST: APIRoute = async ({ request }) => { + const KIT_API_KEY = import.meta.env.KIT_API_SECRET_V4 + if (!KIT_API_KEY) { + return new Response(JSON.stringify({ error: 'config_missing' }), { + status: 500, + headers: { 'Content-Type': 'application/json' }, + }) + } + + let body: { email: string; first_name?: string } + try { + body = await request.json() + } catch { + return new Response(JSON.stringify({ error: 'invalid_json' }), { + status: 400, + headers: { 'Content-Type': 'application/json' }, + }) + } + + const email = (body.email || '').trim().toLowerCase() + if (!email || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) { + return new Response(JSON.stringify({ error: 'invalid_email' }), { + status: 400, + headers: { 'Content-Type': 'application/json' }, + }) + } + + try { + const upstream = await fetch(`${KIT_API_BASE}/subscribers`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-Kit-Api-Key': KIT_API_KEY, + }, + body: JSON.stringify({ + email_address: email, + first_name: body.first_name || undefined, + state: 'active', + }), + signal: AbortSignal.timeout(10000), + }) + + if (upstream.ok) { + return new Response(JSON.stringify({ ok: true }), { + status: 200, + headers: { 'Content-Type': 'application/json' }, + }) + } + if (upstream.status === 422) { + return new Response(JSON.stringify({ ok: true, already: true }), { + status: 200, + headers: { 'Content-Type': 'application/json' }, + }) + } + return new Response( + JSON.stringify({ error: 'kit_failed', status: upstream.status }), + { status: 502, headers: { 'Content-Type': 'application/json' } } + ) + } catch (e) { + return new Response( + JSON.stringify({ error: 'upstream_failed', detail: (e as Error).message }), + { status: 502, headers: { 'Content-Type': 'application/json' } } + ) + } +} From c18328517a79cf8bf8e311361a6f83065425631d Mon Sep 17 00:00:00 2001 From: Jules Neny <jules@trans-former.fr> Date: Mon, 11 May 2026 15:07:58 +0200 Subject: [PATCH 11/36] feat(v11-a): ajout Substack julesneny.substack.com dans selecteur Politique --- src/components/astro/ColJournal.astro | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/astro/ColJournal.astro b/src/components/astro/ColJournal.astro index 3425e1b..1c50096 100644 --- a/src/components/astro/ColJournal.astro +++ b/src/components/astro/ColJournal.astro @@ -10,6 +10,7 @@ const categories = [ plateformes: [ { id: 'instagram', label: '@aep.politique', url: 'https://www.instagram.com/aep.politique/' }, { id: 'castopod', label: 'Podcast', url: 'https://podcast.trans-former.fr' }, + { id: 'substack', label: 'Substack', url: 'https://julesneny.substack.com' }, ], hasSelector: true, }, From d8d3af28a068d773ade5379396504cb236d24a00 Mon Sep 17 00:00:00 2001 From: Jules Neny <jules@trans-former.fr> Date: Mon, 11 May 2026 15:13:46 +0200 Subject: [PATCH 12/36] feat(v11-c): carte-o rendering refonte niveau/nature/statut + contextmenu positionne Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --- src/components/vue/CarteO.vue | 74 +++++--- src/components/vue/CarteOContextMenu.vue | 119 +++++++++++++ src/components/vue/CarteOModal.vue | 215 ----------------------- src/components/vue/CarteOWrapper.vue | 28 ++- 4 files changed, 192 insertions(+), 244 deletions(-) create mode 100644 src/components/vue/CarteOContextMenu.vue delete mode 100644 src/components/vue/CarteOModal.vue diff --git a/src/components/vue/CarteO.vue b/src/components/vue/CarteO.vue index a663434..b055fc9 100644 --- a/src/components/vue/CarteO.vue +++ b/src/components/vue/CarteO.vue @@ -10,6 +10,11 @@ interface CarteNode { id: string label: string family: string + niveau?: number + nature?: 'essai' | 'projet' + statut?: 'gestation' | 'edite' + resume?: string | null + radius?: number intention?: string slug?: string theme?: string @@ -36,7 +41,7 @@ const props = withDefaults(defineProps<{ }) const emit = defineEmits<{ - 'node-click': [node: CarteNode] + 'node-click': [payload: { node: CarteNode; x: number; y: number }] }>() // Refs @@ -59,8 +64,32 @@ let zoomBehavior: d3.ZoomBehavior<SVGSVGElement, unknown> | null = null const isMobile = computed(() => width.value < 600) const nodeRadius = computed(() => isMobile.value ? 10 : 14) -function colorFor(family: string): string { - return props.familyColors[family] || '#9ca3af' +function colorFor(d: SimNode): string { + if (d.nature === 'projet') return '#b45309' + if (d.niveau === 0) return '#1d4ed8' + if (d.niveau === 1) return '#2563eb' + if (d.niveau === 2) return '#60a5fa' + return props.familyColors[d.family] || '#9ca3af' +} + +function getRadius(d: SimNode): number { + return d.radius ?? nodeRadius.value +} + +function strokeFor(d: SimNode): string { + return d.statut === 'edite' ? '#0f172a' : '#94a3b8' +} + +function strokeWidthFor(d: SimNode): number { + return d.statut === 'edite' ? 2.5 : 1 +} + +function labelWeightFor(d: SimNode): string { + return d.statut === 'edite' ? 'bold' : 'normal' +} + +function labelColorFor(d: SimNode): string { + return d.statut === 'edite' ? '#0f172a' : '#6b7280' } function truncate(str: string, max: number): string { @@ -136,7 +165,6 @@ function render() { const simNodes = buildSimNodes() const simLinks = buildSimLinks(simNodes) - const r = nodeRadius.value const fontSize = isMobile.value ? 9 : 11 // Liens @@ -155,49 +183,55 @@ function render() { .join('g') .attr('class', 'node') .style('cursor', 'pointer') - .on('click', (_event, d) => emit('node-click', d)) - .on('mouseover', function () { + .on('click', (_event, d) => emit('node-click', { node: d as CarteNode, x: (d as SimNode).x || 0, y: (d as SimNode).y || 0 })) + .on('mouseover', function (_event, d) { d3.select(this).select('circle') .transition().duration(120) - .attr('stroke-width', 2.5) + .attr('stroke-width', strokeWidthFor(d) + 1.5) }) - .on('mouseout', function () { + .on('mouseout', function (_event, d) { d3.select(this).select('circle') .transition().duration(120) - .attr('stroke-width', 1.5) + .attr('stroke-width', strokeWidthFor(d)) }) - // Cercle (couleur famille) + // Cercle nodeGroups.append('circle') - .attr('r', r) - .attr('fill', d => colorFor(d.family)) - .attr('stroke', '#ffffff') - .attr('stroke-width', 1.5) + .attr('r', d => getRadius(d)) + .attr('fill', d => colorFor(d)) + .attr('stroke', d => strokeFor(d)) + .attr('stroke-width', d => strokeWidthFor(d)) // Label nodeGroups.append('text') .attr('text-anchor', 'start') .attr('dominant-baseline', 'central') - .attr('dx', r + 4) + .attr('dx', d => getRadius(d) + 4) .attr('font-size', fontSize) .attr('font-family', 'system-ui, sans-serif') - .attr('fill', '#1f2937') + .attr('font-weight', d => labelWeightFor(d)) + .attr('fill', d => labelColorFor(d)) .attr('pointer-events', 'none') .text(d => truncate(d.label, isMobile.value ? 18 : 30)) // Tooltip <title> nodeGroups.append('title') - .text(d => `${d.label}\n[${d.family}]\n${truncate(d.intention || '', 200)}`) + .text(d => `${d.label}\n[${d.family}]\n${truncate(d.resume || d.intention || '', 200)}`) - // Simulation force + // Simulation force avec charges differenciees par niveau simulation = d3.forceSimulation<SimNode, SimLink>(simNodes) .force('link', d3.forceLink<SimNode, SimLink>(simLinks) .id(d => d.id) .distance(80) .strength(0.35)) - .force('charge', d3.forceManyBody<SimNode>().strength(-220)) + .force('charge', d3.forceManyBody<SimNode>().strength(d => { + if (d.niveau === 0) return -800 + if (d.niveau === 1) return -400 + if (d.niveau === 2) return -150 + return -220 + })) .force('center', d3.forceCenter(width.value / 2, height.value / 2)) - .force('collide', d3.forceCollide<SimNode>().radius(r + 6)) + .force('collide', d3.forceCollide<SimNode>().radius(d => getRadius(d) + 6)) .alphaDecay(0.03) .on('tick', tick) diff --git a/src/components/vue/CarteOContextMenu.vue b/src/components/vue/CarteOContextMenu.vue new file mode 100644 index 0000000..458ce02 --- /dev/null +++ b/src/components/vue/CarteOContextMenu.vue @@ -0,0 +1,119 @@ +<script setup lang="ts"> +import { computed, onMounted, onUnmounted } from 'vue' + +interface CarteNode { + id: string + label: string + family?: string + nature?: 'essai' | 'projet' + statut?: 'gestation' | 'edite' + resume?: string | null + intention?: string +} + +const props = defineProps<{ + node: CarteNode | null + x: number + y: number +}>() + +const emit = defineEmits<{ close: [] }>() + +function onKeydown(e: KeyboardEvent) { + if (e.key === 'Escape' && props.node) emit('close') +} +function onClickOutside(e: MouseEvent) { + const el = document.getElementById('carte-o-context-menu') + if (el && !el.contains(e.target as Node)) emit('close') +} + +onMounted(() => { + window.addEventListener('keydown', onKeydown) + setTimeout(() => window.addEventListener('click', onClickOutside), 50) +}) +onUnmounted(() => { + window.removeEventListener('keydown', onKeydown) + window.removeEventListener('click', onClickOutside) +}) + +const naturLabel = computed(() => props.node?.nature === 'projet' ? 'Projet archi' : 'Essai politique') +const naturColor = computed(() => props.node?.nature === 'projet' ? '#b45309' : '#1d4ed8') +const texte = computed(() => props.node?.resume || props.node?.intention || 'En cours d\'ecriture.') +</script> + +<template> + <div + v-if="node" + id="carte-o-context-menu" + class="context-menu" + :style="{ left: x + 'px', top: y + 'px' }" + role="dialog" + :aria-label="node.label" + > + <button class="close-btn" type="button" @click="emit('close')" aria-label="Fermer">x</button> + <span class="nature-badge" :style="{ backgroundColor: naturColor }">{{ naturLabel }}</span> + <h3 class="title" :style="{ fontWeight: node.statut === 'edite' ? 'bold' : 'normal' }"> + {{ node.label }} + </h3> + <p class="resume">{{ texte }}</p> + <p v-if="node.statut === 'edite'" class="edite-badge">publie</p> + </div> +</template> + +<style scoped> +.context-menu { + position: absolute; + z-index: 1000; + background: #ffffff; + border: 1px solid #e5e7eb; + border-radius: 8px; + padding: 12px 14px; + width: 220px; + box-shadow: 0 4px 16px rgba(0,0,0,0.12); + font-size: 13px; +} +.close-btn { + position: absolute; + top: 6px; + right: 8px; + background: none; + border: none; + cursor: pointer; + color: #9ca3af; + font-size: 14px; + line-height: 1; + padding: 2px 4px; +} +.close-btn:hover { color: #374151; } +.nature-badge { + display: inline-block; + padding: 2px 7px; + border-radius: 4px; + font-size: 10px; + font-weight: 600; + color: #fff; + letter-spacing: 0.04em; + text-transform: uppercase; + margin-bottom: 6px; + font-family: 'Courier New', Courier, monospace; +} +.title { + font-size: 14px; + color: #1f2937; + margin: 4px 0 6px 0; + line-height: 1.3; +} +.resume { + font-size: 12px; + color: #4b5563; + line-height: 1.5; + margin: 0; +} +.edite-badge { + margin-top: 8px; + font-size: 10px; + color: #059669; + font-family: 'Courier New', Courier, monospace; + font-weight: 600; +} +</style> diff --git a/src/components/vue/CarteOModal.vue b/src/components/vue/CarteOModal.vue deleted file mode 100644 index 9ad7410..0000000 --- a/src/components/vue/CarteOModal.vue +++ /dev/null @@ -1,215 +0,0 @@ -<script setup lang="ts"> -// Modal récap intention pour Carte O. -// Click node -> emit('node-click', n) -> selectedNode.value = n -> ce modal s'affiche. -// Esc + click backdrop ferment. -import { onMounted, onUnmounted, watch } from 'vue' - -interface CarteNode { - id: string - label: string - family: string - intention?: string - slug?: string - theme?: string -} - -const props = defineProps<{ - node: CarteNode | null - familyColors?: Record<string, string> -}>() - -const emit = defineEmits<{ - close: [] -}>() - -function onKeydown(e: KeyboardEvent) { - if (e.key === 'Escape' && props.node) emit('close') -} - -onMounted(() => window.addEventListener('keydown', onKeydown)) -onUnmounted(() => window.removeEventListener('keydown', onKeydown)) - -watch(() => props.node, (n) => { - if (n) { - document.body.style.overflow = 'hidden' - } else { - document.body.style.overflow = '' - } -}) - -function colorFor(family: string): string { - return props.familyColors?.[family] || '#9ca3af' -} -</script> - -<template> - <Teleport to="body"> - <Transition name="modal"> - <div - v-if="node" - class="modal-backdrop" - role="dialog" - aria-modal="true" - :aria-labelledby="`carte-o-modal-title`" - @click.self="emit('close')" - > - <div class="modal-card"> - <button - type="button" - class="close-btn" - aria-label="Fermer" - @click="emit('close')" - >×</button> - - <div class="family-badge" :style="{ backgroundColor: colorFor(node.family) }"> - {{ node.family }} - </div> - - <h2 id="carte-o-modal-title" class="title"> - {{ node.label }} - </h2> - - <p v-if="node.intention" class="intention"> - {{ node.intention }} - </p> - <p v-else class="intention placeholder"> - Pas d'intention extraite. Ouvrez la fiche pour le contenu complet. - </p> - - <div v-if="node.theme" class="theme"> - Thème : <strong>{{ node.theme }}</strong> - </div> - - <a - v-if="node.slug" - :href="`/thematiques/${node.slug}`" - class="cta-link" - > - Lire la fiche → - </a> - </div> - </div> - </Transition> - </Teleport> -</template> - -<style scoped> -.modal-backdrop { - position: fixed; - inset: 0; - z-index: 9999; - background: rgba(0, 0, 0, 0.45); - display: flex; - align-items: center; - justify-content: center; - padding: 1rem; - backdrop-filter: blur(2px); -} - -.modal-card { - position: relative; - background: #ffffff; - border-radius: 14px; - max-width: 32rem; - width: 100%; - padding: 1.75rem; - box-shadow: 0 25px 60px -15px rgba(0, 0, 0, 0.35); -} - -.close-btn { - position: absolute; - top: 0.5rem; - right: 0.5rem; - width: 32px; - height: 32px; - border: none; - background: transparent; - font-size: 24px; - line-height: 1; - color: #9ca3af; - cursor: pointer; - border-radius: 50%; - display: flex; - align-items: center; - justify-content: center; -} - -.close-btn:hover { - background: #f3f4f6; - color: #374151; -} - -.family-badge { - display: inline-block; - padding: 0.2rem 0.6rem; - border-radius: 999px; - font-size: 0.7rem; - font-weight: 600; - letter-spacing: 0.05em; - text-transform: uppercase; - color: #fff; - margin-bottom: 0.65rem; -} - -.title { - font-size: 1.4rem; - font-weight: 600; - color: #1f2937; - margin: 0 0 0.85rem 0; - line-height: 1.3; -} - -.intention { - color: #4b5563; - line-height: 1.55; - margin: 0 0 1rem 0; - font-size: 0.95rem; -} - -.intention.placeholder { - font-style: italic; - color: #9ca3af; -} - -.theme { - font-size: 0.85rem; - color: #6b7280; - margin-bottom: 1.25rem; -} - -.cta-link { - display: inline-block; - padding: 0.55rem 1.1rem; - background: #1f2937; - color: #fff; - border-radius: 8px; - text-decoration: none; - font-size: 0.9rem; - font-weight: 500; - transition: background 0.15s; -} - -.cta-link:hover { - background: #111827; -} - -.modal-enter-active, -.modal-leave-active { - transition: opacity 0.2s ease; -} - -.modal-enter-active .modal-card, -.modal-leave-active .modal-card { - transition: transform 0.2s ease; -} - -.modal-enter-from, -.modal-leave-to { - opacity: 0; -} - -.modal-enter-from .modal-card, -.modal-leave-to .modal-card { - transform: scale(0.96); -} -</style> diff --git a/src/components/vue/CarteOWrapper.vue b/src/components/vue/CarteOWrapper.vue index 2cfd08c..feecad9 100644 --- a/src/components/vue/CarteOWrapper.vue +++ b/src/components/vue/CarteOWrapper.vue @@ -1,14 +1,16 @@ <script setup lang="ts"> -// Wrapper Carte O : fetch /data/carte-o.json + state modal. -// Vue island Astro hydratée client:visible. import { ref, onMounted, computed } from 'vue' import CarteO from './CarteO.vue' -import CarteOModal from './CarteOModal.vue' +import CarteOContextMenu from './CarteOContextMenu.vue' interface CarteNode { id: string label: string family: string + niveau?: number + nature?: 'essai' | 'projet' + statut?: 'gestation' | 'edite' + resume?: string | null intention?: string slug?: string theme?: string @@ -39,6 +41,8 @@ const props = withDefaults(defineProps<{ const data = ref<CarteData | null>(null) const error = ref<string | null>(null) const selectedNode = ref<CarteNode | null>(null) +const contextX = ref(0) +const contextY = ref(0) const isMobileScreen = ref(false) const familyColors = computed(() => @@ -51,6 +55,12 @@ const familyColors = computed(() => } ) +function onNodeClick(payload: { node: CarteNode; x: number; y: number }) { + selectedNode.value = payload.node + contextX.value = payload.x + contextY.value = payload.y +} + onMounted(async () => { isMobileScreen.value = window.innerWidth < 768 try { @@ -62,7 +72,6 @@ onMounted(async () => { error.value = e?.message || 'Erreur de chargement' } - // Update mobile flag on resize. window.addEventListener('resize', () => { isMobileScreen.value = window.innerWidth < 768 }) @@ -87,13 +96,13 @@ onMounted(async () => { </svg> </div> <p class="msg"> - Carte O optimisée desktop. Retournez sur grand écran pour explorer la mindmap interactive. + Carte O optimisee desktop. Retournez sur grand ecran pour explorer la mindmap interactive. </p> </div> <!-- Loading state --> <div v-else-if="!data && !error" class="state"> - <span>Chargement de la Carte O…</span> + <span>Chargement de la Carte O...</span> </div> <!-- Error state --> @@ -107,11 +116,12 @@ onMounted(async () => { :nodes="data.nodes" :edges="data.edges" :family-colors="familyColors" - @node-click="selectedNode = $event" + @node-click="onNodeClick" /> - <CarteOModal + <CarteOContextMenu :node="selectedNode" - :family-colors="familyColors" + :x="contextX" + :y="contextY" @close="selectedNode = null" /> </template> From 79004573f16947407ad594703fe11e44ef340bf0 Mon Sep 17 00:00:00 2001 From: Jules Neny <jules@trans-former.fr> Date: Mon, 11 May 2026 15:14:48 +0200 Subject: [PATCH 13/36] feat(v11-dg): mobile header page active + hamburger top-right + poignee carte-o + polish css Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --- src/components/astro/ColCentre.astro | 47 +++++++++++++++- src/components/astro/HamburgerMenu.astro | 2 +- src/components/astro/MobileTabBar.astro | 71 ++++++++++++++++++++++++ src/components/vue/SwipeContainer.vue | 12 ++++ src/layouts/BaseLayout.astro | 2 +- src/pages/index.astro | 6 +- src/styles/global.css | 22 ++++++++ 7 files changed, 156 insertions(+), 6 deletions(-) create mode 100644 src/components/astro/MobileTabBar.astro diff --git a/src/components/astro/ColCentre.astro b/src/components/astro/ColCentre.astro index 0158e07..1f1f25b 100644 --- a/src/components/astro/ColCentre.astro +++ b/src/components/astro/ColCentre.astro @@ -6,9 +6,9 @@ import ChatbotV2 from '../vue/ChatbotV2.vue'; import IframeCarteAEP from './IframeCarteAEP.astro'; import ScrollArticles from './ScrollArticles.astro'; --- -<div class="h-full grid grid-rows-2 gap-2 p-2"> +<div id="col-centre-grid" class="h-full grid grid-rows-2 gap-2 p-2"> <!-- HAUT 50% : tabs Carte O / Chatbot --> - <section class="border border-neutral-200 rounded flex flex-col overflow-hidden bg-white"> + <section id="col-centre-haut" class="border border-neutral-200 rounded flex flex-col overflow-hidden bg-white"> <nav role="tablist" aria-label="Vues centrales" class="flex border-b border-neutral-200 px-1 pt-1"> <button type="button" @@ -56,6 +56,16 @@ import ScrollArticles from './ScrollArticles.astro'; </div> </section> + <!-- Poignee repli zone HAUT - mobile only --> + <button + id="col-centre-poignee" + type="button" + aria-label="Replier ou deployer la Carte O" + class="md:hidden flex items-center justify-center h-6 bg-neutral-100 border-y border-neutral-200 cursor-pointer w-full -mt-2 -mb-2 hover:bg-neutral-200 transition-colors" + > + <span class="block w-8 h-0.5 bg-neutral-400 rounded-full"></span> + </button> + <!-- BAS 50% : iframe carte AEP + scroll articles Substack (PC4) --> <section class="border border-neutral-200 rounded overflow-y-auto bg-white"> <div class="h-full min-h-[60vh] md:min-h-[400px]"> @@ -66,6 +76,39 @@ import ScrollArticles from './ScrollArticles.astro'; </div> <script> + // Poignee repli zone HAUT (mobile only, D.3) + const grid = document.getElementById('col-centre-grid'); + const haut = document.getElementById('col-centre-haut'); + const poignee = document.getElementById('col-centre-poignee'); + + const applyRepliState = (replie: boolean) => { + if (!grid || !haut) return; + if (replie) { + grid.classList.remove('grid-rows-2'); + grid.style.gridTemplateRows = '0fr 1fr'; + haut.style.overflow = 'hidden'; + haut.style.minHeight = '0'; + poignee?.setAttribute('aria-label', 'Deployer la Carte O'); + } else { + grid.classList.add('grid-rows-2'); + grid.style.gridTemplateRows = ''; + haut.style.overflow = ''; + haut.style.minHeight = ''; + poignee?.setAttribute('aria-label', 'Replier la Carte O'); + } + }; + + // Etat initial depuis sessionStorage + const savedRepli = sessionStorage.getItem('tf-haut-replie'); + applyRepliState(savedRepli === 'true'); + + poignee?.addEventListener('click', () => { + const current = sessionStorage.getItem('tf-haut-replie') === 'true'; + const next = !current; + sessionStorage.setItem('tf-haut-replie', String(next)); + applyRepliState(next); + }); + // Tabs toggle. const tabs = document.querySelectorAll<HTMLButtonElement>('[data-tab]'); const panels = document.querySelectorAll<HTMLElement>('[data-tab-panel]'); diff --git a/src/components/astro/HamburgerMenu.astro b/src/components/astro/HamburgerMenu.astro index 0927d27..66121dc 100644 --- a/src/components/astro/HamburgerMenu.astro +++ b/src/components/astro/HamburgerMenu.astro @@ -5,7 +5,7 @@ <button id="hamburger-trigger" type="button" - class="fixed top-4 left-4 z-50 p-3 bg-white/95 border border-neutral-200 rounded-lg shadow-md hover:bg-white transition-colors md:top-6 md:left-6" + class="fixed top-4 right-4 z-50 p-3 bg-white/95 border border-neutral-200 rounded-lg shadow-md hover:bg-white transition-colors md:top-6 md:right-6" aria-label="Ouvrir le menu" aria-expanded="false" aria-controls="hamburger-drawer" diff --git a/src/components/astro/MobileTabBar.astro b/src/components/astro/MobileTabBar.astro new file mode 100644 index 0000000..9d88bff --- /dev/null +++ b/src/components/astro/MobileTabBar.astro @@ -0,0 +1,71 @@ +--- +// MobileTabBar - indicateur de colonne active sur mobile (V1.1-D.1) +// Ecoute l'event "swipe-position-change" emis par SwipeContainer.vue +// Affiche : Journal | Carte | Insta - colonne active surlignee +--- +<nav + id="mobile-tab-bar" + aria-label="Navigation colonnes" + class="fixed top-0 left-0 right-0 z-30 h-11 bg-white border-b border-neutral-200 flex items-stretch justify-around md:hidden" +> + <button + type="button" + data-tab-index="0" + class="mobile-tab flex-1 text-sm px-2 border-b-2 transition-colors" + aria-label="Aller au journal" + > + Journal + </button> + <button + type="button" + data-tab-index="1" + class="mobile-tab flex-1 text-sm px-2 border-b-2 transition-colors" + aria-label="Aller à la carte" + > + Carte + </button> + <button + type="button" + data-tab-index="2" + class="mobile-tab flex-1 text-sm px-2 border-b-2 transition-colors" + aria-label="Aller à Instagram" + > + Insta + </button> +</nav> + +<script> + const tabs = document.querySelectorAll<HTMLButtonElement>('.mobile-tab'); + + function setActive(pos: number) { + tabs.forEach((tab, i) => { + const active = i === pos; + tab.classList.toggle('text-neutral-900', active); + tab.classList.toggle('font-medium', active); + tab.classList.toggle('border-neutral-900', active); + tab.classList.toggle('text-neutral-400', !active); + tab.classList.toggle('border-transparent', !active); + }); + } + + // Etat initial depuis sessionStorage (cle utilisee par SwipeContainer.vue) + const saved = sessionStorage.getItem('pc-position'); + const initial = saved !== null && !Number.isNaN(Number(saved)) ? Number(saved) : 1; + setActive(initial); + + // Ecoute les changements de position emis par SwipeContainer.vue + document.addEventListener('swipe-position-change', (e: Event) => { + const detail = (e as CustomEvent<{ pos: number }>).detail; + if (detail && typeof detail.pos === 'number') { + setActive(detail.pos); + } + }); + + // Les boutons de la tab bar declenchent un scroll via un event custom + tabs.forEach((tab) => { + tab.addEventListener('click', () => { + const idx = Number(tab.dataset.tabIndex); + document.dispatchEvent(new CustomEvent('mobile-tab-scroll', { detail: { pos: idx } })); + }); + }); +</script> diff --git a/src/components/vue/SwipeContainer.vue b/src/components/vue/SwipeContainer.vue index 49b08f8..f0419c2 100644 --- a/src/components/vue/SwipeContainer.vue +++ b/src/components/vue/SwipeContainer.vue @@ -22,12 +22,17 @@ const resetFade = () => { }, 3000); }; +const emitPositionChange = (pos: number) => { + document.dispatchEvent(new CustomEvent('swipe-position-change', { detail: { pos } })); +}; + onMounted(() => { if (!emblaApi.value) return; emblaApi.value.on('select', () => { if (!emblaApi.value) return; selectedIndex.value = emblaApi.value.selectedScrollSnap(); sessionStorage.setItem('pc-position', String(selectedIndex.value)); + emitPositionChange(selectedIndex.value); resetFade(); }); const saved = sessionStorage.getItem('pc-position'); @@ -35,6 +40,13 @@ onMounted(() => { const idx = Number(saved); if (!Number.isNaN(idx)) emblaApi.value.scrollTo(idx, false); } + // Ecoute les clics de la MobileTabBar + document.addEventListener('mobile-tab-scroll', (e: Event) => { + const detail = (e as CustomEvent<{ pos: number }>).detail; + if (detail && typeof detail.pos === 'number') { + emblaApi.value?.scrollTo(detail.pos); + } + }); resetFade(); }); diff --git a/src/layouts/BaseLayout.astro b/src/layouts/BaseLayout.astro index bde0144..213d45c 100644 --- a/src/layouts/BaseLayout.astro +++ b/src/layouts/BaseLayout.astro @@ -9,7 +9,7 @@ interface Props { const { title = 'trans-former.fr', - description = 'Page-cerveau : journal, mindmap AEP, Insta', + description = "Architecture d'ecologie politique - journal, carte conceptuelle, manifeste", } = Astro.props; --- <!doctype html> diff --git a/src/pages/index.astro b/src/pages/index.astro index 71a5031..6e3ca82 100644 --- a/src/pages/index.astro +++ b/src/pages/index.astro @@ -7,10 +7,12 @@ import ColCentre from '../components/astro/ColCentre.astro'; import ColInsta from '../components/astro/ColInsta.astro'; import SwipeContainer from '../components/vue/SwipeContainer.vue'; import HamburgerMenu from '../components/astro/HamburgerMenu.astro'; +import MobileTabBar from '../components/astro/MobileTabBar.astro'; import PopupOnboarding from '../components/astro/PopupOnboarding.astro'; --- <BaseLayout title="trans-former.fr"> <HamburgerMenu /> + <MobileTabBar /> <PopupOnboarding /> <!-- Desktop : grid 3 colonnes --> @@ -20,8 +22,8 @@ import PopupOnboarding from '../components/astro/PopupOnboarding.astro'; <aside class="border-l border-neutral-200 overflow-y-auto"><ColInsta /></aside> </div> - <!-- Mobile : SwipeContainer Vue island --> - <div class="md:hidden h-screen overflow-hidden"> + <!-- Mobile : SwipeContainer Vue island - decale de 44px pour la tabbar --> + <div class="md:hidden overflow-hidden" style="height: calc(100dvh - 44px); margin-top: 44px;"> <SwipeContainer client:load> <ColJournal slot="left" /> <ColCentre slot="center" /> diff --git a/src/styles/global.css b/src/styles/global.css index f1d8c73..60b87f6 100644 --- a/src/styles/global.css +++ b/src/styles/global.css @@ -1 +1,23 @@ @import "tailwindcss"; + +/* Typographie monospace - labels editoriaux (V1.1-G.1) */ +.font-mono-editorial, +.hashtag-label, +.nature-badge, +.carte-o-label { + font-family: 'Courier New', Courier, monospace; +} + +/* Corps de texte pages statiques (V1.1-G.2) */ +.prose-page { + font-size: 1.0625rem; + line-height: 1.75; + color: #374151; + max-width: 65ch; +} +.prose-page h1, .prose-page h2, .prose-page h3 { + color: #111827; + font-weight: 600; + line-height: 1.3; +} + From 61e53a04d58e9416232e1d2551ef99138ca2edc6 Mon Sep 17 00:00:00 2001 From: Jules Neny <jules@trans-former.fr> Date: Mon, 11 May 2026 15:19:24 +0200 Subject: [PATCH 14/36] feat(v11-e): centre BAS embed dynamique click-journal + suppression ScrollArticles Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --- src/components/astro/ColCentre.astro | 10 +- src/components/astro/ScrollArticles.astro | 72 --------- src/components/vue/EmbedDynamique.vue | 181 ++++++++++++++++++++++ src/components/vue/JournalList.vue | 10 ++ 4 files changed, 195 insertions(+), 78 deletions(-) delete mode 100644 src/components/astro/ScrollArticles.astro create mode 100644 src/components/vue/EmbedDynamique.vue diff --git a/src/components/astro/ColCentre.astro b/src/components/astro/ColCentre.astro index 1f1f25b..41e1aec 100644 --- a/src/components/astro/ColCentre.astro +++ b/src/components/astro/ColCentre.astro @@ -3,8 +3,7 @@ // BAS : iframe carte AEP + scroll articles Substack (PC4). import CarteOWrapper from '../vue/CarteOWrapper.vue'; import ChatbotV2 from '../vue/ChatbotV2.vue'; -import IframeCarteAEP from './IframeCarteAEP.astro'; -import ScrollArticles from './ScrollArticles.astro'; +import EmbedDynamique from '../vue/EmbedDynamique.vue'; --- <div id="col-centre-grid" class="h-full grid grid-rows-2 gap-2 p-2"> <!-- HAUT 50% : tabs Carte O / Chatbot --> @@ -66,12 +65,11 @@ import ScrollArticles from './ScrollArticles.astro'; <span class="block w-8 h-0.5 bg-neutral-400 rounded-full"></span> </button> - <!-- BAS 50% : iframe carte AEP + scroll articles Substack (PC4) --> - <section class="border border-neutral-200 rounded overflow-y-auto bg-white"> + <!-- BAS 50% : embed dynamique (carte AEP default, article journal au click) --> + <section class="border border-neutral-200 rounded overflow-hidden bg-white"> <div class="h-full min-h-[60vh] md:min-h-[400px]"> - <IframeCarteAEP /> + <EmbedDynamique client:visible /> </div> - <ScrollArticles /> </section> </div> diff --git a/src/components/astro/ScrollArticles.astro b/src/components/astro/ScrollArticles.astro deleted file mode 100644 index c7b2e6d..0000000 --- a/src/components/astro/ScrollArticles.astro +++ /dev/null @@ -1,72 +0,0 @@ ---- -// PC4 - Liste articles Substack en scroll sous l'iframe carte. -// V1 placeholder data en dur ; PC6 (journal n8n) remplacera par fetch journal.json filtre tag #politique. -const articles = [ - { - date: '2026-04-28', - titre: 'Cap sur l\'autonomie : retour sur 6 mois de chantier', - url: 'https://transformations.substack.com/p/cap-autonomie', - }, - { - date: '2026-04-15', - titre: 'Le commun comme infrastructure', - url: 'https://transformations.substack.com/p/commun-infrastructure', - }, - { - date: '2026-03-30', - titre: 'Architecte ou operateur de bifurcation ?', - url: 'https://transformations.substack.com/p/architecte-bifurcation', - }, - { - date: '2026-03-12', - titre: 'Reseaux AEP : pourquoi la coordination est politique', - url: 'https://transformations.substack.com/p/aep-coordination', - }, - { - date: '2026-02-26', - titre: 'Sortir du sauveur, entrer dans le compagnon', - url: 'https://transformations.substack.com/p/sauveur-compagnon', - }, - { - date: '2026-02-08', - titre: 'Petit manifeste contre l\'expert isole', - url: 'https://transformations.substack.com/p/contre-expert-isole', - }, - { - date: '2026-01-22', - titre: 'Ce que la commande publique fait a la pensee', - url: 'https://transformations.substack.com/p/commande-publique', - }, - // TODO PC6 : remplacer par fetch journal.json filtre tag #politique. -]; ---- -<section class="border-t border-neutral-200 py-6 px-4 bg-white"> - <h3 class="text-sm font-semibold uppercase tracking-wider text-neutral-500 mb-4"> - Derniers articles ; Substack - </h3> - <ul class="space-y-3"> - {articles.map(({ date, titre, url }) => ( - <li> - <a - href={url} - target="_blank" - rel="noopener" - class="block group" - > - <time class="text-xs text-neutral-400">{date}</time> - <p class="text-sm text-neutral-800 group-hover:text-neutral-600 transition-colors leading-snug"> - {titre} - </p> - </a> - </li> - ))} - </ul> - <a - href="https://transformations.substack.com" - target="_blank" - rel="noopener" - class="inline-block mt-4 text-xs text-neutral-500 hover:text-neutral-900" - > - Voir tous les articles → - </a> -</section> diff --git a/src/components/vue/EmbedDynamique.vue b/src/components/vue/EmbedDynamique.vue new file mode 100644 index 0000000..f4b289a --- /dev/null +++ b/src/components/vue/EmbedDynamique.vue @@ -0,0 +1,181 @@ +<script setup lang="ts"> +import { ref, computed, onMounted, onUnmounted } from 'vue' + +interface JournalItem { + id: string + platform: 'substack' | 'gitea' | 'github' | 'instagram' | 'castopod' | 'blog' | 'linkedin' + hashtag: string + date: string + titre: string + extrait: string + url: string + thumbnail: string | null +} + +const selectedItem = ref<JournalItem | null>(null) +const iframeRef = ref<HTMLIFrameElement | null>(null) +const skeletonHidden = ref(false) + +const onJournalItemClick = (e: Event) => { + const ce = e as CustomEvent + if (ce.detail?.item) selectedItem.value = ce.detail.item +} + +onMounted(() => { + window.addEventListener('journal-item-click', onJournalItemClick as EventListener) +}) +onUnmounted(() => { + window.removeEventListener('journal-item-click', onJournalItemClick as EventListener) +}) + +const reset = () => { + selectedItem.value = null + skeletonHidden.value = false +} + +const onIframeLoad = () => { + if (iframeRef.value) { + iframeRef.value.classList.remove('opacity-0') + iframeRef.value.classList.add('opacity-100') + } + skeletonHidden.value = true +} + +const extractInstaShortcode = (url: string): string | null => { + const m = url.match(/\/(p|reel)\/([^\/\?#]+)/) + return m ? m[2] : null +} + +const embedUrl = computed(() => { + if (!selectedItem.value) return null + const item = selectedItem.value + if (item.platform === 'substack') { + return item.url.includes('?') ? item.url + '&embed=1' : item.url + '?embed=1' + } + if (item.platform === 'instagram') { + const sc = extractInstaShortcode(item.url) + return sc ? `https://www.instagram.com/p/${sc}/embed/` : null + } + return null +}) + +const platformLabel = (p: string) => { + const labels: Record<string, string> = { + substack: 'Substack', + instagram: 'Instagram', + gitea: 'Gitea', + github: 'GitHub', + castopod: 'Podcast', + blog: 'Blog', + linkedin: 'LinkedIn', + } + return labels[p] || p +} + +const formatDate = (iso: string) => { + try { + const d = new Date(iso) + return isNaN(d.getTime()) + ? '' + : `${String(d.getDate()).padStart(2, '0')}/${String(d.getMonth() + 1).padStart(2, '0')}` + } catch { + return '' + } +} +</script> + +<template> + <div class="embed-dynamique h-full flex flex-col relative"> + + <!-- DEFAULT : iframe AEP (aucun item selectionne) --> + <div v-if="!selectedItem" class="h-full"> + <div class="relative h-full bg-neutral-100"> + <div + v-if="!skeletonHidden" + id="embed-skeleton" + class="absolute inset-0 flex items-center justify-center bg-neutral-50 animate-pulse" + > + <span class="text-neutral-400 text-sm">Chargement de la carte AEP...</span> + </div> + <iframe + src="https://aep.trans-former.fr/agences" + title="Carte AEP" + class="w-full h-full border-0 opacity-0 transition-opacity duration-500" + sandbox="allow-scripts allow-same-origin allow-popups allow-forms" + @load="onIframeLoad" + ref="iframeRef" + ></iframe> + </div> + </div> + + <!-- EMBED MODE : teaser + embed live ou carte incitative --> + <div v-else class="h-full flex flex-col overflow-y-auto"> + + <!-- Header : reset + hashtag --> + <div class="flex items-center justify-between px-4 py-2 border-b border-neutral-200 bg-white sticky top-0 z-10"> + <button + class="text-xs text-neutral-500 hover:text-neutral-900 flex items-center gap-1" + @click="reset" + type="button" + > + - Retour a la carte + </button> + <span class="text-xs text-neutral-400" style="font-family: 'Courier New', Courier, monospace;"> + {{ selectedItem.hashtag }} + </span> + </div> + + <!-- Teaser --> + <div class="px-4 py-3 border-b border-neutral-100 bg-neutral-50"> + <p class="text-[11px] text-neutral-500 mb-1">{{ formatDate(selectedItem.date) }} - {{ selectedItem.platform }}</p> + <h3 class="text-sm font-semibold text-neutral-900 leading-snug mb-1">{{ selectedItem.titre }}</h3> + <p v-if="selectedItem.extrait" class="text-xs text-neutral-600 leading-relaxed line-clamp-3"> + {{ selectedItem.extrait }} + </p> + <img + v-if="selectedItem.thumbnail" + :src="selectedItem.thumbnail" + :alt="selectedItem.titre" + class="mt-2 w-full max-h-28 object-cover rounded" + loading="lazy" + /> + </div> + + <!-- Embed live (Substack ou Instagram) --> + <div v-if="embedUrl" class="flex-1 min-h-[200px] bg-white"> + <iframe + :src="embedUrl" + class="w-full h-full border-0 min-h-[300px]" + :title="selectedItem.titre" + loading="lazy" + sandbox="allow-scripts allow-same-origin allow-popups allow-forms allow-top-navigation" + ></iframe> + </div> + + <!-- Carte incitative (autres plateformes) --> + <div v-else class="flex-1 flex flex-col items-start justify-center px-4 py-6"> + <p class="text-xs text-neutral-500 mb-3 italic">Embed non disponible pour cette plateforme.</p> + <a + :href="selectedItem.url" + target="_blank" + rel="noopener noreferrer" + class="inline-block px-4 py-2 bg-neutral-900 text-white text-sm rounded-lg hover:bg-neutral-700 transition-colors" + > + Voir sur {{ platformLabel(selectedItem.platform) }} -> + </a> + </div> + + <!-- CTA propagation universel --> + <div class="px-4 py-3 border-t border-neutral-100"> + <a + :href="selectedItem.url" + target="_blank" + rel="noopener noreferrer" + class="text-xs text-neutral-500 hover:text-neutral-900 underline" + > + Continuer sur {{ platformLabel(selectedItem.platform) }} - commenter, partager + </a> + </div> + </div> + </div> +</template> diff --git a/src/components/vue/JournalList.vue b/src/components/vue/JournalList.vue index f0e342d..20f0485 100644 --- a/src/components/vue/JournalList.vue +++ b/src/components/vue/JournalList.vue @@ -77,6 +77,15 @@ onUnmounted(() => { window.removeEventListener('platform-filter-change', onPlatformChange as EventListener) }) +const onItemClick = (item: JournalItem, e: MouseEvent) => { + if (e.metaKey || e.ctrlKey) { + window.open(item.url, '_blank', 'noopener') + return + } + e.preventDefault() + window.dispatchEvent(new CustomEvent('journal-item-click', { detail: { item } })) +} + const visibleItems = computed(() => { const keys = Object.keys(filters.value) let filtered: JournalItem[] @@ -156,6 +165,7 @@ const platformLabel = (p: string) => { target="_blank" rel="noopener noreferrer" class="block group" + @click="onItemClick(item, $event)" > <div class="flex items-baseline gap-2 text-[11px] text-neutral-500 mb-1"> <time>{{ formatDate(item.date) }}</time> From 046f34ec8b7bb6e08c88ea714dd8c8b10653c40a Mon Sep 17 00:00:00 2001 From: Jules Neny <jules@trans-former.fr> Date: Mon, 11 May 2026 15:22:44 +0200 Subject: [PATCH 15/36] feat(v11-f): drag-resize desktop ColCentre HAUT/BAS + persist session --- src/components/astro/ColCentre.astro | 87 +++++++++++++++++++++++++++- 1 file changed, 85 insertions(+), 2 deletions(-) diff --git a/src/components/astro/ColCentre.astro b/src/components/astro/ColCentre.astro index 41e1aec..f67ea2f 100644 --- a/src/components/astro/ColCentre.astro +++ b/src/components/astro/ColCentre.astro @@ -7,7 +7,7 @@ import EmbedDynamique from '../vue/EmbedDynamique.vue'; --- <div id="col-centre-grid" class="h-full grid grid-rows-2 gap-2 p-2"> <!-- HAUT 50% : tabs Carte O / Chatbot --> - <section id="col-centre-haut" class="border border-neutral-200 rounded flex flex-col overflow-hidden bg-white"> + <section id="col-centre-haut" class="border border-neutral-200 rounded flex flex-col overflow-hidden bg-white" style="min-height: 0;"> <nav role="tablist" aria-label="Vues centrales" class="flex border-b border-neutral-200 px-1 pt-1"> <button type="button" @@ -55,6 +55,15 @@ import EmbedDynamique from '../vue/EmbedDynamique.vue'; </div> </section> + <!-- Drag handle desktop - redimensionnement vertical md+ --> + <div + id="col-centre-drag-handle" + class="hidden md:flex items-center justify-center h-2 cursor-row-resize hover:bg-neutral-200 transition-colors w-full -mt-1 -mb-1" + aria-hidden="true" + > + <span class="block w-10 h-0.5 bg-neutral-300 rounded-full"></span> + </div> + <!-- Poignee repli zone HAUT - mobile only --> <button id="col-centre-poignee" @@ -66,7 +75,7 @@ import EmbedDynamique from '../vue/EmbedDynamique.vue'; </button> <!-- BAS 50% : embed dynamique (carte AEP default, article journal au click) --> - <section class="border border-neutral-200 rounded overflow-hidden bg-white"> + <section class="border border-neutral-200 rounded overflow-hidden bg-white" style="min-height: 0;"> <div class="h-full min-h-[60vh] md:min-h-[400px]"> <EmbedDynamique client:visible /> </div> @@ -107,6 +116,80 @@ import EmbedDynamique from '../vue/EmbedDynamique.vue'; applyRepliState(next); }); + // Drag-resize desktop (>=768px) + const dragHandle = document.getElementById('col-centre-drag-handle'); + const gridEl = document.getElementById('col-centre-grid'); + + if (dragHandle && gridEl) { + let isDragging = false; + let startY = 0; + let startTop = 0; + + const getGridHeight = () => gridEl.getBoundingClientRect().height; + + const getHautPercent = (): number => { + const rows = gridEl.style.gridTemplateRows; + if (rows && rows.includes('fr')) { + const parts = rows.split(' '); + if (parts.length >= 2) { + const top = parseFloat(parts[0]) || 1; + const bot = parseFloat(parts[parts.length - 1]) || 1; + return (top / (top + bot)) * 100; + } + } + if (rows && rows.includes('%')) { + const parts = rows.split(' '); + return parseFloat(parts[0]) || 50; + } + return 50; + }; + + dragHandle.addEventListener('mousedown', (e: MouseEvent) => { + if (sessionStorage.getItem('tf-haut-replie') === 'true') return; + isDragging = true; + startY = e.clientY; + startTop = (getHautPercent() / 100) * getGridHeight(); + document.body.style.cursor = 'row-resize'; + document.body.style.userSelect = 'none'; + e.preventDefault(); + }); + + document.addEventListener('mousemove', (e: MouseEvent) => { + if (!isDragging || !gridEl) return; + const delta = e.clientY - startY; + const totalH = getGridHeight(); + const newTop = Math.min(Math.max(startTop + delta, totalH * 0.2), totalH * 0.8); + const topPct = (newTop / totalH) * 100; + const botPct = 100 - topPct; + gridEl.style.gridTemplateRows = `${topPct.toFixed(1)}% ${botPct.toFixed(1)}%`; + gridEl.classList.remove('grid-rows-2'); + }); + + document.addEventListener('mouseup', () => { + if (isDragging) { + isDragging = false; + document.body.style.cursor = ''; + document.body.style.userSelect = ''; + const rows = gridEl.style.gridTemplateRows; + if (rows) sessionStorage.setItem('tf-centre-rows', rows); + } + }); + + // Restaurer position depuis sessionStorage + const savedRows = sessionStorage.getItem('tf-centre-rows'); + if (savedRows && sessionStorage.getItem('tf-haut-replie') !== 'true') { + gridEl.style.gridTemplateRows = savedRows; + gridEl.classList.remove('grid-rows-2'); + } + + // Double-click sur drag handle = reset 50/50 + dragHandle.addEventListener('dblclick', () => { + gridEl.style.gridTemplateRows = ''; + gridEl.classList.add('grid-rows-2'); + sessionStorage.removeItem('tf-centre-rows'); + }); + } + // Tabs toggle. const tabs = document.querySelectorAll<HTMLButtonElement>('[data-tab]'); const panels = document.querySelectorAll<HTMLElement>('[data-tab-panel]'); From 95b75d48664b24a5ca44fefd75b6f2a378bed705 Mon Sep 17 00:00:00 2001 From: Jules Neny <jules@trans-former.fr> Date: Mon, 11 May 2026 18:32:37 +0200 Subject: [PATCH 16/36] feat(v12-k): nav gauche palette terre + sub-pills Court/Article --- src/components/astro/ColJournal.astro | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/components/astro/ColJournal.astro b/src/components/astro/ColJournal.astro index 1c50096..81d1714 100644 --- a/src/components/astro/ColJournal.astro +++ b/src/components/astro/ColJournal.astro @@ -5,19 +5,19 @@ const categories = [ { id: 'politique', label: 'Politique', - color: '#1d4ed8', + color: '#B5443A', hashtags: ['#politique', '#aep-politique'], plateformes: [ - { id: 'instagram', label: '@aep.politique', url: 'https://www.instagram.com/aep.politique/' }, + { id: 'instagram', label: 'Court', url: 'https://www.instagram.com/aep.politique/' }, { id: 'castopod', label: 'Podcast', url: 'https://podcast.trans-former.fr' }, - { id: 'substack', label: 'Substack', url: 'https://julesneny.substack.com' }, + { id: 'substack', label: 'Article', url: 'https://julesneny.substack.com' }, ], hasSelector: true, }, { id: 'art', label: 'Art', - color: '#dc2626', + color: '#5B6B3A', hashtags: ['#peinture', '#art'], plateformes: [ { id: 'instagram', label: '@julesneny', url: 'https://www.instagram.com/julesneny/' }, @@ -27,7 +27,7 @@ const categories = [ { id: 'outils', label: 'Outils', - color: '#16a34a', + color: '#475569', hashtags: ['#stack', '#building-public'], plateformes: [ { id: 'gitea', label: 'Gitea', url: 'https://git.trans-former.fr/jules' }, @@ -68,7 +68,7 @@ const categories = [ type="button" data-platform-id={p.id} class="platform-pill" - style="font-family:'Courier New',Courier,monospace;font-size:12px;padding:2px 8px;border-radius:12px;cursor:pointer;border:1px solid #1d4ed8;background:transparent;color:#1d4ed8;" + style="font-family:'Courier New',Courier,monospace;font-size:12px;padding:2px 8px;border-radius:12px;cursor:pointer;border:1px solid #B5443A;background:transparent;color:#B5443A;" > {p.label} </button> @@ -219,11 +219,11 @@ const categories = [ pills.forEach((pill) => { const pid = pill.dataset.platformId; if (!active || pid === active) { - pill.style.background = '#1d4ed8'; + pill.style.background = '#B5443A'; pill.style.color = '#fff'; } else { pill.style.background = 'transparent'; - pill.style.color = '#1d4ed8'; + pill.style.color = '#B5443A'; } }); }; From 6ea256f8a4f387f4b50c5fcdc90a2b7d2c724662 Mon Sep 17 00:00:00 2001 From: Jules Neny <jules@trans-former.fr> Date: Mon, 11 May 2026 18:33:46 +0200 Subject: [PATCH 17/36] feat(v12-l): footer 1-line 3-zones (nav + subscribe + logos RS terre) --- src/components/astro/Footer.astro | 118 ++++++++++++++++++++++-------- 1 file changed, 88 insertions(+), 30 deletions(-) diff --git a/src/components/astro/Footer.astro b/src/components/astro/Footer.astro index e984908..147c35c 100644 --- a/src/components/astro/Footer.astro +++ b/src/components/astro/Footer.astro @@ -1,36 +1,94 @@ --- -// Footer.astro - CTA infolettre Kit + nav footer +// Footer.astro - V1.2-L : 1 ligne 3 zones (nav / subscribe / logos RS) +// Style monochrome encre #0F172A palette terre V1.2 --- -<footer class="border-t border-neutral-200 px-6 py-8 text-sm bg-white"> - <div class="max-w-3xl mx-auto"> - <h3 class="font-semibold mb-1" style="font-family: 'Courier New', Courier, monospace;"> - S'abonner a la lettre - </h3> - <p class="text-neutral-600 text-xs mb-3"> - 1-2 emails par mois - pas de spam - desinscription en 1 clic. - </p> - <form id="subscribe-form" class="flex gap-2 max-w-md"> - <input - type="email" - name="email" - required - placeholder="ton@email.fr" - class="flex-1 px-3 py-2 border border-neutral-300 rounded-lg text-sm focus:outline-none focus:border-neutral-900" - /> - <button - type="submit" - class="px-4 py-2 bg-neutral-900 text-white rounded-lg text-sm hover:bg-neutral-700 transition-colors" - > - s'abonner - </button> - </form> - <p id="subscribe-msg" class="mt-2 text-xs text-neutral-500 min-h-[1rem]"></p> - <nav class="mt-6 flex flex-wrap gap-4 text-xs text-neutral-500"> - <a href="/manifeste" class="hover:text-neutral-900">Manifeste</a> - <a href="/a-propos" class="hover:text-neutral-900">A propos</a> - <a href="/mentions-legales" class="hover:text-neutral-900">Mentions legales</a> - <a href="https://www.instagram.com/aep.politique/" target="_blank" rel="noopener" class="hover:text-neutral-900">@aep.politique</a> +<footer class="border-t border-neutral-200 px-6 py-4 bg-[#FAFAF7] text-[#0F172A]"> + <div class="flex flex-col md:flex-row md:items-center md:justify-between gap-3 md:gap-6"> + + <!-- ZONE GAUCHE : liens nav --> + <nav class="flex gap-4 text-xs justify-center md:justify-start"> + <a href="/manifeste" class="opacity-60 hover:opacity-100 transition-opacity">Manifeste</a> + <a href="/a-propos" class="opacity-60 hover:opacity-100 transition-opacity">A propos</a> + <a href="/mentions-legales" class="opacity-60 hover:opacity-100 transition-opacity">Mentions legales</a> </nav> + + <!-- ZONE CENTRE : subscribe form compact (endpoint /api/subscribe V1.1-I) --> + <div class="flex flex-col items-center gap-1"> + <form id="subscribe-form" class="flex gap-2 items-center"> + <input + type="email" + name="email" + required + placeholder="ton@email.fr" + class="px-3 py-1.5 border border-neutral-300 rounded text-xs focus:outline-none focus:border-[#0F172A] bg-white" + /> + <button + type="submit" + class="px-3 py-1.5 bg-[#0F172A] text-white rounded text-xs hover:bg-[#475569] transition-colors whitespace-nowrap" + > + s'abonner + </button> + </form> + <p id="subscribe-msg" class="text-[10px] text-neutral-500 min-h-[0.75rem]"></p> + </div> + + <!-- ZONE DROITE : logos RS cliquables (SVG inline, fill #0F172A 60%) --> + <div class="flex gap-3 items-center justify-center md:justify-end text-[#0F172A]"> + <!-- Instagram --> + <a + href="https://www.instagram.com/aep.politique/" + target="_blank" + rel="noopener noreferrer" + aria-label="Instagram @aep.politique" + class="opacity-60 hover:opacity-100 transition-opacity" + > + <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" class="w-[18px] h-[18px] md:w-5 md:h-5" fill="currentColor" aria-hidden="true"> + <path d="M12 2.163c3.204 0 3.584.012 4.85.07 1.366.062 2.633.336 3.608 1.311.975.975 1.249 2.242 1.311 3.608.058 1.266.069 1.646.069 4.85s-.012 3.584-.07 4.85c-.062 1.366-.336 2.633-1.311 3.608-.975.975-2.242 1.249-3.608 1.311-1.266.058-1.646.07-4.85.07s-3.584-.012-4.85-.07c-1.366-.062-2.633-.336-3.608-1.311-.975-.975-1.249-2.242-1.311-3.608C2.175 15.647 2.163 15.267 2.163 12s.012-3.584.07-4.85c.062-1.366.336-2.633 1.311-3.608.975-.975 2.242-1.249 3.608-1.311 1.266-.058 1.646-.07 4.85-.07zm0 1.838c-3.15 0-3.522.012-4.766.069-1.024.047-1.58.218-1.95.362-.49.19-.84.418-1.207.786-.367.367-.595.717-.786 1.207-.144.37-.315.926-.362 1.95-.057 1.244-.069 1.616-.069 4.766s.012 3.522.069 4.766c.047 1.024.218 1.58.362 1.95.19.49.418.84.786 1.207.367.367.717.595 1.207.786.37.144.926.315 1.95.362 1.244.057 1.616.069 4.766.069s3.522-.012 4.766-.069c1.024-.047 1.58-.218 1.95-.362.49-.19.84-.418 1.207-.786.367-.367.595-.717.786-1.207.144-.37.315-.926.362-1.95.057-1.244.069-1.616.069-4.766s-.012-3.522-.069-4.766c-.047-1.024-.218-1.58-.362-1.95-.19-.49-.418-.84-.786-1.207-.367-.367-.717-.595-1.207-.786-.37-.144-.926-.315-1.95-.362C15.522 4.013 15.15 4.001 12 4.001zm0 3.135a4.864 4.864 0 110 9.728 4.864 4.864 0 010-9.728zm0 8.027a3.162 3.162 0 100-6.325 3.162 3.162 0 000 6.325zm6.187-8.249a1.137 1.137 0 11-2.275 0 1.137 1.137 0 012.275 0z"/> + </svg> + </a> + + <!-- GitHub (TODO: si 404 connu, fallback https://git.trans-former.fr/jules Gitea) --> + <a + href="https://github.com/julesneny" + target="_blank" + rel="noopener noreferrer" + aria-label="GitHub" + class="opacity-60 hover:opacity-100 transition-opacity" + > + <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" class="w-[18px] h-[18px] md:w-5 md:h-5" fill="currentColor" aria-hidden="true"> + <path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"/> + </svg> + </a> + + <!-- LinkedIn TODO: confirmer URL LinkedIn Jules --> + <a + href="https://www.linkedin.com/in/jules-neny/" + target="_blank" + rel="noopener noreferrer" + aria-label="LinkedIn" + class="opacity-60 hover:opacity-100 transition-opacity" + > + <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" class="w-[18px] h-[18px] md:w-5 md:h-5" fill="currentColor" aria-hidden="true"> + <path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433a2.062 2.062 0 01-2.063-2.065 2.063 2.063 0 112.063 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z"/> + </svg> + </a> + + <!-- Substack --> + <a + href="https://julesneny.substack.com" + target="_blank" + rel="noopener noreferrer" + aria-label="Substack" + class="opacity-60 hover:opacity-100 transition-opacity" + > + <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" class="w-[18px] h-[18px] md:w-5 md:h-5" fill="currentColor" aria-hidden="true"> + <path d="M22.539 8.242H1.46V5.406h21.08v2.836zM1.46 10.812V24L12 18.11 22.54 24V10.812H1.46zM22.54 0H1.46v2.836h21.08V0z"/> + </svg> + </a> + + <!-- TODO V1.3 : ajouter logo Pinterest quand compte créé --> + </div> + </div> </footer> From 272fb5c1817acbe051e2a278faa76ca86339fa38 Mon Sep 17 00:00:00 2001 From: Jules Neny <jules@trans-former.fr> Date: Mon, 11 May 2026 18:34:01 +0200 Subject: [PATCH 18/36] feat(v12-r): col-centre default 1/3 Carte O + 2/3 iframe AEP (vs 50/50) --- src/components/astro/ColCentre.astro | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/components/astro/ColCentre.astro b/src/components/astro/ColCentre.astro index f67ea2f..fe7c548 100644 --- a/src/components/astro/ColCentre.astro +++ b/src/components/astro/ColCentre.astro @@ -5,7 +5,7 @@ import CarteOWrapper from '../vue/CarteOWrapper.vue'; import ChatbotV2 from '../vue/ChatbotV2.vue'; import EmbedDynamique from '../vue/EmbedDynamique.vue'; --- -<div id="col-centre-grid" class="h-full grid grid-rows-2 gap-2 p-2"> +<div id="col-centre-grid" class="h-full grid gap-2 p-2" style="grid-template-rows: 1fr 2fr;"> <!-- HAUT 50% : tabs Carte O / Chatbot --> <section id="col-centre-haut" class="border border-neutral-200 rounded flex flex-col overflow-hidden bg-white" style="min-height: 0;"> <nav role="tablist" aria-label="Vues centrales" class="flex border-b border-neutral-200 px-1 pt-1"> @@ -97,8 +97,8 @@ import EmbedDynamique from '../vue/EmbedDynamique.vue'; haut.style.minHeight = '0'; poignee?.setAttribute('aria-label', 'Deployer la Carte O'); } else { - grid.classList.add('grid-rows-2'); - grid.style.gridTemplateRows = ''; + grid.classList.remove('grid-rows-2'); + grid.style.gridTemplateRows = '1fr 2fr'; haut.style.overflow = ''; haut.style.minHeight = ''; poignee?.setAttribute('aria-label', 'Replier la Carte O'); @@ -141,7 +141,7 @@ import EmbedDynamique from '../vue/EmbedDynamique.vue'; const parts = rows.split(' '); return parseFloat(parts[0]) || 50; } - return 50; + return 33.33; }; dragHandle.addEventListener('mousedown', (e: MouseEvent) => { @@ -182,10 +182,10 @@ import EmbedDynamique from '../vue/EmbedDynamique.vue'; gridEl.classList.remove('grid-rows-2'); } - // Double-click sur drag handle = reset 50/50 + // Double-click sur drag handle = reset default 1/3 + 2/3 dragHandle.addEventListener('dblclick', () => { - gridEl.style.gridTemplateRows = ''; - gridEl.classList.add('grid-rows-2'); + gridEl.style.gridTemplateRows = '1fr 2fr'; + gridEl.classList.remove('grid-rows-2'); sessionStorage.removeItem('tf-centre-rows'); }); } From 7791054ca055f7836692e1468192c55cdd48b3d3 Mon Sep 17 00:00:00 2001 From: Jules Neny <jules@trans-former.fr> Date: Mon, 11 May 2026 18:40:50 +0200 Subject: [PATCH 19/36] =?UTF-8?q?feat(v12-q):=20iframe=20AEP=20forc=C3=A9?= =?UTF-8?q?=20desktop=20via=20transform=20scale=20+=20viewport=201440px?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/vue/EmbedDynamique.vue | 33 ++++++++++++++++++++++++--- 1 file changed, 30 insertions(+), 3 deletions(-) diff --git a/src/components/vue/EmbedDynamique.vue b/src/components/vue/EmbedDynamique.vue index f4b289a..949c0d6 100644 --- a/src/components/vue/EmbedDynamique.vue +++ b/src/components/vue/EmbedDynamique.vue @@ -14,8 +14,27 @@ interface JournalItem { const selectedItem = ref<JournalItem | null>(null) const iframeRef = ref<HTMLIFrameElement | null>(null) +const wrapperRef = ref<HTMLDivElement | null>(null) const skeletonHidden = ref(false) +// Force rendu desktop de l'iframe AEP : viewport simulée 1440px + scale dynamique +const VIEWPORT_W = 1440 +const iframeScale = ref(0.42) +let resizeObs: ResizeObserver | null = null + +const updateScale = () => { + if (!wrapperRef.value) return + const w = wrapperRef.value.clientWidth + if (w > 0) iframeScale.value = w / VIEWPORT_W +} + +const iframeStyle = computed(() => ({ + width: VIEWPORT_W + 'px', + height: (100 / iframeScale.value) + '%', + transform: `scale(${iframeScale.value})`, + transformOrigin: '0 0', +})) + const onJournalItemClick = (e: Event) => { const ce = e as CustomEvent if (ce.detail?.item) selectedItem.value = ce.detail.item @@ -23,9 +42,16 @@ const onJournalItemClick = (e: Event) => { onMounted(() => { window.addEventListener('journal-item-click', onJournalItemClick as EventListener) + if (wrapperRef.value && typeof ResizeObserver !== 'undefined') { + updateScale() + resizeObs = new ResizeObserver(updateScale) + resizeObs.observe(wrapperRef.value) + } }) onUnmounted(() => { window.removeEventListener('journal-item-click', onJournalItemClick as EventListener) + resizeObs?.disconnect() + resizeObs = null }) const reset = () => { @@ -89,18 +115,19 @@ const formatDate = (iso: string) => { <!-- DEFAULT : iframe AEP (aucun item selectionne) --> <div v-if="!selectedItem" class="h-full"> - <div class="relative h-full bg-neutral-100"> + <div ref="wrapperRef" class="relative h-full bg-neutral-100 overflow-hidden"> <div v-if="!skeletonHidden" id="embed-skeleton" - class="absolute inset-0 flex items-center justify-center bg-neutral-50 animate-pulse" + class="absolute inset-0 flex items-center justify-center bg-neutral-50 animate-pulse z-10" > <span class="text-neutral-400 text-sm">Chargement de la carte AEP...</span> </div> <iframe src="https://aep.trans-former.fr/agences" title="Carte AEP" - class="w-full h-full border-0 opacity-0 transition-opacity duration-500" + class="absolute top-0 left-0 border-0 opacity-0 transition-opacity duration-500" + :style="iframeStyle" sandbox="allow-scripts allow-same-origin allow-popups allow-forms" @load="onIframeLoad" ref="iframeRef" From 3ba46288185e1c35a45f9d7258977fb9a1977662 Mon Sep 17 00:00:00 2001 From: Jules Neny <jules@trans-former.fr> Date: Mon, 11 May 2026 18:41:51 +0200 Subject: [PATCH 20/36] feat(v12-m): header bandeau Trans-Former / Jules Neny / baseline palette terre --- src/components/astro/MobileTabBar.astro | 2 +- src/components/astro/SiteHeader.astro | 52 +++++++++++++++++++++++++ src/layouts/BaseLayout.astro | 8 +++- src/pages/index.astro | 11 ++++-- 4 files changed, 66 insertions(+), 7 deletions(-) create mode 100644 src/components/astro/SiteHeader.astro diff --git a/src/components/astro/MobileTabBar.astro b/src/components/astro/MobileTabBar.astro index 9d88bff..afe7a05 100644 --- a/src/components/astro/MobileTabBar.astro +++ b/src/components/astro/MobileTabBar.astro @@ -6,7 +6,7 @@ <nav id="mobile-tab-bar" aria-label="Navigation colonnes" - class="fixed top-0 left-0 right-0 z-30 h-11 bg-white border-b border-neutral-200 flex items-stretch justify-around md:hidden" + class="fixed top-12 left-0 right-0 z-30 h-11 bg-white border-b border-neutral-200 flex items-stretch justify-around md:hidden" > <button type="button" diff --git a/src/components/astro/SiteHeader.astro b/src/components/astro/SiteHeader.astro new file mode 100644 index 0000000..38fa6b6 --- /dev/null +++ b/src/components/astro/SiteHeader.astro @@ -0,0 +1,52 @@ +--- +// SiteHeader.astro - V1.2-M : bandeau header pleine largeur identite site +// Palette terre figee : papier #FAFAF7, encre #0F172A, encre douce #475569 +// Composition retenue : 2 lignes hierarchique +// ligne 1 : "Trans-Former" wordmark dominant (semibold tracking serre) +// ligne 2 : "Jules Neny" + baseline italique cote a cote (separateur point median) +// Rationale : le wordmark domine sans ecraser ; la baseline reste lisible ; +// composition adaptee a un manifeste (hierarchie typographique forte). +// Hauteur : ~64px desktop / ~48px mobile (compacte) +// Baseline raccourcie mobile : "architecture politique du vivant" +--- +<header + class="site-header w-full border-b border-[#E5E7EB] bg-[#FAFAF7] text-[#0F172A] px-4 md:px-6 flex items-center" + role="banner" +> + <a + href="/" + class="flex flex-col md:flex-row md:items-baseline gap-x-3 gap-y-0 no-underline text-[#0F172A] hover:text-[#0F172A]" + aria-label="trans-former.fr - retour accueil" + > + <!-- Ligne 1 : wordmark dominant --> + <span + class="font-semibold tracking-tight text-[#0F172A] text-[17px] md:text-[20px] leading-none" + > + Trans-Former + </span> + + <!-- Ligne 2 (desktop : inline ; mobile : sous wordmark) : Jules Neny + baseline --> + <span class="flex items-baseline gap-2 text-[#475569] leading-none"> + <span class="text-[11px] md:text-[13px]">Jules Neny</span> + <span class="text-[#94A3B8]" aria-hidden="true">·</span> + <!-- Baseline longue : desktop / Baseline courte : mobile --> + <span class="italic text-[11px] md:text-[13px] hidden sm:inline"> + architecture d'ecologie politique + </span> + <span class="italic text-[11px] inline sm:hidden"> + architecture politique du vivant + </span> + </span> + </a> +</header> + +<style> + .site-header { + height: 48px; + } + @media (min-width: 768px) { + .site-header { + height: 64px; + } + } +</style> diff --git a/src/layouts/BaseLayout.astro b/src/layouts/BaseLayout.astro index 213d45c..77e6397 100644 --- a/src/layouts/BaseLayout.astro +++ b/src/layouts/BaseLayout.astro @@ -1,6 +1,7 @@ --- import '../styles/global.css'; import Footer from '../components/astro/Footer.astro'; +import SiteHeader from '../components/astro/SiteHeader.astro'; interface Props { title?: string; @@ -28,8 +29,11 @@ const { <meta name="twitter:title" content={title} /> <meta name="twitter:description" content={description} /> </head> - <body class="m-0 bg-white text-neutral-900 antialiased"> - <slot /> + <body class="m-0 bg-white text-neutral-900 antialiased min-h-screen flex flex-col"> + <SiteHeader /> + <div class="flex-1 flex flex-col min-h-0"> + <slot /> + </div> <Footer /> </body> </html> diff --git a/src/pages/index.astro b/src/pages/index.astro index 6e3ca82..eb3aaf9 100644 --- a/src/pages/index.astro +++ b/src/pages/index.astro @@ -15,15 +15,18 @@ import PopupOnboarding from '../components/astro/PopupOnboarding.astro'; <MobileTabBar /> <PopupOnboarding /> - <!-- Desktop : grid 3 colonnes --> - <div class="hidden md:grid md:grid-cols-[320px_1fr_320px] h-screen overflow-hidden"> + <!-- Desktop : grid 3 colonnes (header 64px deja consommes par SiteHeader, on prend le reste) --> + <div + class="hidden md:grid md:grid-cols-[320px_1fr_320px] overflow-hidden" + style="height: calc(100vh - 64px);" + > <aside class="border-r border-neutral-200 overflow-y-auto"><ColJournal /></aside> <main class="overflow-hidden"><ColCentre /></main> <aside class="border-l border-neutral-200 overflow-y-auto"><ColInsta /></aside> </div> - <!-- Mobile : SwipeContainer Vue island - decale de 44px pour la tabbar --> - <div class="md:hidden overflow-hidden" style="height: calc(100dvh - 44px); margin-top: 44px;"> + <!-- Mobile : SwipeContainer Vue island - header 48px + tabbar 44px = 92px reserves --> + <div class="md:hidden overflow-hidden" style="height: calc(100dvh - 48px - 44px); margin-top: 44px;"> <SwipeContainer client:load> <ColJournal slot="left" /> <ColCentre slot="center" /> From 3f2783e3fc15f0d85e49b598cafce4142352e893 Mon Sep 17 00:00:00 2001 From: Jules Neny <jules@trans-former.fr> Date: Mon, 11 May 2026 18:42:06 +0200 Subject: [PATCH 21/36] feat(v12-n): Carte O fusion noeud central + palette minimaliste encre/papier/ocre - YAML: fusion 3 noeuds confus (centre + ncs-politique + medecine-corps-social) en 1 seul noeud central 'Contrat social + Medecine du corps social' - Build script: toutes les thematiques rattachees directement au centre (suppression mapping NCS/MDCS), radius central 30px, projets 18px - CarteO.vue palette V1.2: central #0F172A (encre), essais #FFFFFF stroke encre, projets #B45309 (ocre conserve) - Labels: inscrit dans le cercle (blanc) pour central+projets, a droite (encre douce) pour essais - Label central long split sur 2-3 lignes via splitCentralLabel() - Background: #FAFAF7 (papier, raccord colonnes laterales) - Liens: #94A3B8 opacity 0.4 1px 17 nodes / 19 edges. Build SSR 5 pages prerender + server, 0 warning. --- public/data/carte-o-source.yaml | 20 +---- public/data/carte-o.json | 100 +++++++++---------------- scripts/build-carte-o.js | 48 +++--------- src/components/vue/CarteO.vue | 126 +++++++++++++++++++++++++------- 4 files changed, 152 insertions(+), 142 deletions(-) diff --git a/public/data/carte-o-source.yaml b/public/data/carte-o-source.yaml index a70c2ac..3a8dafd 100644 --- a/public/data/carte-o-source.yaml +++ b/public/data/carte-o-source.yaml @@ -8,26 +8,14 @@ version: "1.1" centre: - id: "nouveau-contrat-social" - label: "Nouveau Contrat Social" + id: "contrat-social-medecine-corps-social" + label: "Contrat social + Medecine du corps social" niveau: 0 nature: essai statut: gestation - resume: "Assemblage de tout ce que j'ecris qui s'entrechoque - inventer un nouveau contrat social." + resume: "Manifeste central AEP : inventer un nouveau contrat social et diagnostiquer/soigner les pathologies du corps social." -concepts_force: - - id: "ncs-politique" - label: "Nouveau contrat social" - niveau: 1 - nature: essai - statut: gestation - resume: "L'ecriture politique d'un futur habitable - essai central AEP." - - id: "medecine-corps-social" - label: "Medecine du corps social" - niveau: 1 - nature: essai - statut: gestation - resume: "Diagnostiquer et soigner les pathologies du corps social - concept AEP mdcs." +concepts_force: [] thematiques: - id: "systemique" diff --git a/public/data/carte-o.json b/public/data/carte-o.json index 340867b..f3f6661 100644 --- a/public/data/carte-o.json +++ b/public/data/carte-o.json @@ -1,35 +1,15 @@ { "version": "1.1", - "generatedAt": "2026-05-11T13:04:29.806Z", + "generatedAt": "2026-05-11T16:41:21.600Z", "nodes": [ { - "id": "nouveau-contrat-social", - "label": "Nouveau Contrat Social", + "id": "contrat-social-medecine-corps-social", + "label": "Contrat social + Medecine du corps social", "niveau": 0, "nature": "essai", "statut": "gestation", - "resume": "Assemblage de tout ce que j'ecris qui s'entrechoque - inventer un nouveau contrat social.", - "radius": 28, - "family": "concept" - }, - { - "id": "ncs-politique", - "label": "Nouveau contrat social", - "niveau": 1, - "nature": "essai", - "statut": "gestation", - "resume": "L'ecriture politique d'un futur habitable - essai central AEP.", - "radius": 18, - "family": "concept" - }, - { - "id": "medecine-corps-social", - "label": "Medecine du corps social", - "niveau": 1, - "nature": "essai", - "statut": "gestation", - "resume": "Diagnostiquer et soigner les pathologies du corps social - concept AEP mdcs.", - "radius": 18, + "resume": "Manifeste central AEP : inventer un nouveau contrat social et diagnostiquer/soigner les pathologies du corps social.", + "radius": 30, "family": "concept" }, { @@ -39,7 +19,7 @@ "nature": "essai", "statut": "gestation", "resume": null, - "radius": 10, + "radius": 12, "family": "concept" }, { @@ -49,7 +29,7 @@ "nature": "essai", "statut": "gestation", "resume": null, - "radius": 10, + "radius": 12, "family": "concept" }, { @@ -59,7 +39,7 @@ "nature": "essai", "statut": "gestation", "resume": null, - "radius": 10, + "radius": 12, "family": "concept" }, { @@ -69,7 +49,7 @@ "nature": "essai", "statut": "gestation", "resume": null, - "radius": 10, + "radius": 12, "family": "concept" }, { @@ -79,7 +59,7 @@ "nature": "essai", "statut": "gestation", "resume": null, - "radius": 10, + "radius": 12, "family": "concept" }, { @@ -89,7 +69,7 @@ "nature": "essai", "statut": "gestation", "resume": null, - "radius": 10, + "radius": 12, "family": "concept" }, { @@ -99,7 +79,7 @@ "nature": "essai", "statut": "gestation", "resume": null, - "radius": 10, + "radius": 12, "family": "concept" }, { @@ -109,7 +89,7 @@ "nature": "essai", "statut": "gestation", "resume": null, - "radius": 10, + "radius": 12, "family": "concept" }, { @@ -119,7 +99,7 @@ "nature": "essai", "statut": "gestation", "resume": null, - "radius": 10, + "radius": 12, "family": "concept" }, { @@ -129,7 +109,7 @@ "nature": "essai", "statut": "gestation", "resume": null, - "radius": 10, + "radius": 12, "family": "concept" }, { @@ -139,7 +119,7 @@ "nature": "essai", "statut": "gestation", "resume": null, - "radius": 10, + "radius": 12, "family": "concept" }, { @@ -149,7 +129,7 @@ "nature": "essai", "statut": "gestation", "resume": null, - "radius": 10, + "radius": 12, "family": "concept" }, { @@ -159,7 +139,7 @@ "nature": "essai", "statut": "gestation", "resume": null, - "radius": 10, + "radius": 12, "family": "concept" }, { @@ -169,7 +149,7 @@ "nature": "essai", "statut": "gestation", "resume": null, - "radius": 10, + "radius": 12, "family": "concept" }, { @@ -179,7 +159,7 @@ "nature": "essai", "statut": "gestation", "resume": null, - "radius": 10, + "radius": 12, "family": "concept" }, { @@ -189,77 +169,69 @@ "nature": "projet", "statut": "gestation", "resume": "Transport, mobilite, industrie, politique - projet archi. Exemple de projet archi relie aux thematiques AEP.", - "radius": 14, + "radius": 18, "family": "ressource" } ], "edges": [ { - "source": "nouveau-contrat-social", - "target": "ncs-politique" - }, - { - "source": "nouveau-contrat-social", - "target": "medecine-corps-social" - }, - { - "source": "ncs-politique", + "source": "contrat-social-medecine-corps-social", "target": "systemique" }, { - "source": "ncs-politique", + "source": "contrat-social-medecine-corps-social", "target": "pratiques-collectives" }, { - "source": "medecine-corps-social", + "source": "contrat-social-medecine-corps-social", "target": "art-narration" }, { - "source": "ncs-politique", + "source": "contrat-social-medecine-corps-social", "target": "pouvoir-domination" }, { - "source": "nouveau-contrat-social", + "source": "contrat-social-medecine-corps-social", "target": "medias-critique" }, { - "source": "nouveau-contrat-social", + "source": "contrat-social-medecine-corps-social", "target": "justice-securite" }, { - "source": "medecine-corps-social", + "source": "contrat-social-medecine-corps-social", "target": "sante-globale" }, { - "source": "nouveau-contrat-social", + "source": "contrat-social-medecine-corps-social", "target": "agriculture" }, { - "source": "ncs-politique", + "source": "contrat-social-medecine-corps-social", "target": "post-croissance" }, { - "source": "medecine-corps-social", + "source": "contrat-social-medecine-corps-social", "target": "anthropocene" }, { - "source": "ncs-politique", + "source": "contrat-social-medecine-corps-social", "target": "education" }, { - "source": "nouveau-contrat-social", + "source": "contrat-social-medecine-corps-social", "target": "urbanisme" }, { - "source": "nouveau-contrat-social", + "source": "contrat-social-medecine-corps-social", "target": "geopolitique" }, { - "source": "medecine-corps-social", + "source": "contrat-social-medecine-corps-social", "target": "ia-technologie" }, { - "source": "medecine-corps-social", + "source": "contrat-social-medecine-corps-social", "target": "spiritualite" }, { diff --git a/scripts/build-carte-o.js b/scripts/build-carte-o.js index 170527e..2348dfc 100644 --- a/scripts/build-carte-o.js +++ b/scripts/build-carte-o.js @@ -11,12 +11,12 @@ const REPO_ROOT = path.resolve(__dirname, '..') const SOURCE = path.join(REPO_ROOT, 'public/data/carte-o-source.yaml') const OUTPUT = path.join(REPO_ROOT, 'public/data/carte-o.json') -// radius par niveau + nature +// radius par niveau + nature (V1.2-N palette minimaliste) function getRadius(niveau, nature) { - if (niveau === 0) return 28 - if (niveau === 1) return 18 - if (niveau === 2 && nature === 'projet') return 14 - return 10 + if (niveau === 0) return 30 + if (nature === 'projet') return 18 + if (niveau === 1) return 16 + return 12 } // compat backward : nature -> family @@ -24,30 +24,8 @@ function getFamily(nature) { return nature === 'projet' ? 'ressource' : 'concept' } -// thematiques rattachees directement au centre (ni ncs-politique ni medecine-corps-social) -const CENTRE_THEMATIQUES = new Set([ - 'medias-critique', - 'justice-securite', - 'agriculture', - 'urbanisme', - 'geopolitique', -]) - -const NCS_THEMATIQUES = new Set([ - 'systemique', - 'pratiques-collectives', - 'pouvoir-domination', - 'post-croissance', - 'education', -]) - -const MDCS_THEMATIQUES = new Set([ - 'art-narration', - 'sante-globale', - 'spiritualite', - 'ia-technologie', - 'anthropocene', -]) +// V1.2-N : noeud central fusionne -> toutes les thematiques sont rattachees au centre +// (les anciens groupes NCS_THEMATIQUES / MDCS_THEMATIQUES sont supprimes avec leurs sous-noeuds) async function main() { const raw = await fs.readFile(SOURCE, 'utf-8') @@ -80,20 +58,16 @@ async function main() { const centreId = data.centre.id addNode(data.centre) - for (const cf of data.concepts_force) { + // concepts_force vide en V1.2-N (fusionne dans le centre) + for (const cf of (data.concepts_force || [])) { addNode(cf) addEdge(centreId, cf.id) } + // toutes les thematiques rattachees directement au noeud central for (const th of data.thematiques) { addNode(th) - if (NCS_THEMATIQUES.has(th.id)) { - addEdge('ncs-politique', th.id) - } else if (MDCS_THEMATIQUES.has(th.id)) { - addEdge('medecine-corps-social', th.id) - } else if (CENTRE_THEMATIQUES.has(th.id)) { - addEdge(centreId, th.id) - } + addEdge(centreId, th.id) } for (const proj of data.projets) { diff --git a/src/components/vue/CarteO.vue b/src/components/vue/CarteO.vue index b055fc9..61cfa3e 100644 --- a/src/components/vue/CarteO.vue +++ b/src/components/vue/CarteO.vue @@ -64,32 +64,46 @@ let zoomBehavior: d3.ZoomBehavior<SVGSVGElement, unknown> | null = null const isMobile = computed(() => width.value < 600) const nodeRadius = computed(() => isMobile.value ? 10 : 14) +// V1.2-N palette : encre / papier / ocre +// - central niveau 0 : fill encre #0F172A, label blanc inscrit dans le cercle +// - essais (niveaux 1-2) : fill papier #FFFFFF, stroke encre, label encre a droite +// - projets : fill ocre #B45309, label blanc inscrit dans le cercle +// - statut "edite" : stroke epaissi (l'epaisseur conserve l'info, pas la couleur) function colorFor(d: SimNode): string { - if (d.nature === 'projet') return '#b45309' - if (d.niveau === 0) return '#1d4ed8' - if (d.niveau === 1) return '#2563eb' - if (d.niveau === 2) return '#60a5fa' - return props.familyColors[d.family] || '#9ca3af' + if (d.nature === 'projet') return '#B45309' + if (d.niveau === 0) return '#0F172A' + return '#FFFFFF' } function getRadius(d: SimNode): number { - return d.radius ?? nodeRadius.value + if (d.radius != null) return d.radius + if (d.niveau === 0) return 30 + if (d.nature === 'projet') return 18 + return nodeRadius.value } function strokeFor(d: SimNode): string { - return d.statut === 'edite' ? '#0f172a' : '#94a3b8' + if (d.nature === 'projet') return '#B45309' + if (d.niveau === 0) return '#0F172A' + return '#0F172A' } function strokeWidthFor(d: SimNode): number { - return d.statut === 'edite' ? 2.5 : 1 + return d.statut === 'edite' ? 2.5 : 1.5 } function labelWeightFor(d: SimNode): string { return d.statut === 'edite' ? 'bold' : 'normal' } +// Label inscrit dans le cercle (fond fonce) pour central + projets +function labelInsideFor(d: SimNode): boolean { + return d.niveau === 0 || d.nature === 'projet' +} + function labelColorFor(d: SimNode): string { - return d.statut === 'edite' ? '#0f172a' : '#6b7280' + if (labelInsideFor(d)) return '#FFFFFF' + return d.statut === 'edite' ? '#0F172A' : '#475569' } function truncate(str: string, max: number): string { @@ -97,6 +111,27 @@ function truncate(str: string, max: number): string { return str.length > max ? str.slice(0, max - 1) + '…' : str } +// Split intelligent du label central sur 2-3 lignes (autour de "+") +function splitCentralLabel(label: string): string[] { + if (!label) return [''] + // priorite : split sur " + " si present + if (label.includes(' + ')) { + const parts = label.split(' + ') + return [parts[0].trim(), '+', parts.slice(1).join(' + ').trim()] + } + // fallback : split a peu pres au milieu sur un espace + if (label.length > 14) { + const mid = Math.floor(label.length / 2) + const left = label.lastIndexOf(' ', mid) + const right = label.indexOf(' ', mid) + const cut = (mid - left <= right - mid && left > 0) ? left : right + if (cut > 0) { + return [label.slice(0, cut).trim(), label.slice(cut).trim()] + } + } + return [label] +} + function buildSimNodes(): SimNode[] { return props.nodes.map(n => ({ ...n })) } @@ -167,14 +202,14 @@ function render() { const fontSize = isMobile.value ? 9 : 11 - // Liens + // Liens — gris doux papier gLinks! .selectAll<SVGLineElement, SimLink>('line') .data(simLinks) .join('line') - .attr('stroke', '#94a3b8') - .attr('stroke-opacity', 0.45) - .attr('stroke-width', 1.2) + .attr('stroke', '#94A3B8') + .attr('stroke-opacity', 0.4) + .attr('stroke-width', 1) // Noeuds = groupe <g> par node const nodeGroups = gNodes! @@ -202,17 +237,58 @@ function render() { .attr('stroke', d => strokeFor(d)) .attr('stroke-width', d => strokeWidthFor(d)) - // Label - nodeGroups.append('text') - .attr('text-anchor', 'start') - .attr('dominant-baseline', 'central') - .attr('dx', d => getRadius(d) + 4) - .attr('font-size', fontSize) - .attr('font-family', 'system-ui, sans-serif') - .attr('font-weight', d => labelWeightFor(d)) - .attr('fill', d => labelColorFor(d)) - .attr('pointer-events', 'none') - .text(d => truncate(d.label, isMobile.value ? 18 : 30)) + // Label : inscrit dans le cercle pour central + projets, a droite sinon + // Pour le noeud central (label long), on passe en multi-lignes + nodeGroups.each(function (d) { + const g = d3.select(this) + const inside = labelInsideFor(d) + + if (inside && d.niveau === 0) { + // Noeud central : label en 2 lignes inscrites au centre + const parts = splitCentralLabel(d.label) + const lineHeight = isMobile.value ? 10 : 12 + const fs = isMobile.value ? 9 : 11 + const startY = -((parts.length - 1) * lineHeight) / 2 + parts.forEach((line, i) => { + g.append('text') + .attr('text-anchor', 'middle') + .attr('dominant-baseline', 'central') + .attr('x', 0) + .attr('y', startY + i * lineHeight) + .attr('font-size', fs) + .attr('font-family', 'system-ui, sans-serif') + .attr('font-weight', 'bold') + .attr('fill', '#FFFFFF') + .attr('pointer-events', 'none') + .text(line) + }) + } else if (inside) { + // Projets : label centre dans le cercle (court) + g.append('text') + .attr('text-anchor', 'middle') + .attr('dominant-baseline', 'central') + .attr('x', 0) + .attr('y', 0) + .attr('font-size', fontSize) + .attr('font-family', 'system-ui, sans-serif') + .attr('font-weight', labelWeightFor(d)) + .attr('fill', '#FFFFFF') + .attr('pointer-events', 'none') + .text(truncate(d.label, 6)) + } else { + // Essais : label a droite du cercle + g.append('text') + .attr('text-anchor', 'start') + .attr('dominant-baseline', 'central') + .attr('dx', getRadius(d) + 4) + .attr('font-size', fontSize) + .attr('font-family', 'system-ui, sans-serif') + .attr('font-weight', labelWeightFor(d)) + .attr('fill', labelColorFor(d)) + .attr('pointer-events', 'none') + .text(truncate(d.label, isMobile.value ? 18 : 30)) + } + }) // Tooltip <title> nodeGroups.append('title') @@ -325,7 +401,7 @@ onUnmounted(() => { width: 100%; height: 100%; position: relative; - background: #fafafa; + background: #FAFAF7; overflow: hidden; } From a1d6271b655c0b24c1e29414b36dd3d38df5f7a6 Mon Sep 17 00:00:00 2001 From: Jules Neny <jules@trans-former.fr> Date: Mon, 11 May 2026 18:48:15 +0200 Subject: [PATCH 22/36] feat(v12-o): Carte O logos plateforme via Brandfetch CDN (visible zoom>1.5x) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Champ optionnel domain dans YAML carte-o-source : propage vers JSON et permet d'afficher un logo plateforme en bas-droite de chaque node (cercle blanc 18px + image clippee circulaire 14px) quand le zoom depasse 1.5x. V1.2-O par defaut : substack.com sur les 15 thematiques essais. Centre + projet TMIP gardent leur fill brut (encre / ocre). Toggle visibilite via callback zoom (opacity 0/1 sur .logo-overlay). A flagger : CDN Brandfetch retourne 403 en curl server-side avec le client ID fourni. A revalider en browser (origin trans-former.fr) — le CDN peut exiger un Origin header autorise. Si bloque, fallback prevu en V1.3 (proxy local ou logos packages dans /public/logos/). Files: - public/data/carte-o-source.yaml : +15 champs domain - scripts/build-carte-o.js : propagation domain -> JSON - src/components/vue/CarteO.vue : CarteNode.domain + logoUrl helper + logo-overlay (circle + image clip-path) + toggle visibilite zoom - public/data/carte-o.json : regenere (15/17 nodes ont domain) --- public/data/carte-o-source.yaml | 18 +++++++++++++ public/data/carte-o.json | 47 ++++++++++++++++++++++----------- scripts/build-carte-o.js | 7 +++-- src/components/vue/CarteO.vue | 38 ++++++++++++++++++++++++++ 4 files changed, 92 insertions(+), 18 deletions(-) diff --git a/public/data/carte-o-source.yaml b/public/data/carte-o-source.yaml index 3a8dafd..40ad47f 100644 --- a/public/data/carte-o-source.yaml +++ b/public/data/carte-o-source.yaml @@ -4,6 +4,9 @@ # statut: gestation (draft/en cours) | edite (publie) # nature: essai (texte politique) | projet (projet archi) # niveau: 0 (centre) | 1 (concepts force) | 2 (thematiques/projets) +# domain (optionnel) : domaine plateforme source pour logo Brandfetch CDN +# - affiche logo en bas-droite du node si zoom > 1.5x +# - V1.2 par defaut : substack.com pour tous les essais AEP version: "1.1" @@ -23,76 +26,91 @@ thematiques: niveau: 2 nature: essai statut: gestation + domain: "substack.com" - id: "pratiques-collectives" label: "Pratiques collectives" niveau: 2 nature: essai statut: gestation + domain: "substack.com" - id: "art-narration" label: "Art & narration" niveau: 2 nature: essai statut: gestation + domain: "substack.com" - id: "pouvoir-domination" label: "Rapport au pouvoir" niveau: 2 nature: essai statut: gestation + domain: "substack.com" - id: "medias-critique" label: "Medias & pensee critique" niveau: 2 nature: essai statut: gestation + domain: "substack.com" - id: "justice-securite" label: "Justice & securite" niveau: 2 nature: essai statut: gestation + domain: "substack.com" - id: "sante-globale" label: "Sante globale" niveau: 2 nature: essai statut: gestation + domain: "substack.com" - id: "agriculture" label: "Agriculture" niveau: 2 nature: essai statut: gestation + domain: "substack.com" - id: "post-croissance" label: "Post-croissance" niveau: 2 nature: essai statut: gestation + domain: "substack.com" - id: "anthropocene" label: "Anthropocene & effondrement" niveau: 2 nature: essai statut: gestation + domain: "substack.com" - id: "education" label: "Education a la transformation" niveau: 2 nature: essai statut: gestation + domain: "substack.com" - id: "urbanisme" label: "Urbanisme" niveau: 2 nature: essai statut: gestation + domain: "substack.com" - id: "geopolitique" label: "Geopolitique & decolonisation" niveau: 2 nature: essai statut: gestation + domain: "substack.com" - id: "ia-technologie" label: "IA & technologie" niveau: 2 nature: essai statut: gestation + domain: "substack.com" - id: "spiritualite" label: "Spiritualite" niveau: 2 nature: essai statut: gestation + domain: "substack.com" projets: - id: "tmip" diff --git a/public/data/carte-o.json b/public/data/carte-o.json index f3f6661..6c0f168 100644 --- a/public/data/carte-o.json +++ b/public/data/carte-o.json @@ -1,6 +1,6 @@ { "version": "1.1", - "generatedAt": "2026-05-11T16:41:21.600Z", + "generatedAt": "2026-05-11T16:47:45.459Z", "nodes": [ { "id": "contrat-social-medecine-corps-social", @@ -20,7 +20,8 @@ "statut": "gestation", "resume": null, "radius": 12, - "family": "concept" + "family": "concept", + "domain": "substack.com" }, { "id": "pratiques-collectives", @@ -30,7 +31,8 @@ "statut": "gestation", "resume": null, "radius": 12, - "family": "concept" + "family": "concept", + "domain": "substack.com" }, { "id": "art-narration", @@ -40,7 +42,8 @@ "statut": "gestation", "resume": null, "radius": 12, - "family": "concept" + "family": "concept", + "domain": "substack.com" }, { "id": "pouvoir-domination", @@ -50,7 +53,8 @@ "statut": "gestation", "resume": null, "radius": 12, - "family": "concept" + "family": "concept", + "domain": "substack.com" }, { "id": "medias-critique", @@ -60,7 +64,8 @@ "statut": "gestation", "resume": null, "radius": 12, - "family": "concept" + "family": "concept", + "domain": "substack.com" }, { "id": "justice-securite", @@ -70,7 +75,8 @@ "statut": "gestation", "resume": null, "radius": 12, - "family": "concept" + "family": "concept", + "domain": "substack.com" }, { "id": "sante-globale", @@ -80,7 +86,8 @@ "statut": "gestation", "resume": null, "radius": 12, - "family": "concept" + "family": "concept", + "domain": "substack.com" }, { "id": "agriculture", @@ -90,7 +97,8 @@ "statut": "gestation", "resume": null, "radius": 12, - "family": "concept" + "family": "concept", + "domain": "substack.com" }, { "id": "post-croissance", @@ -100,7 +108,8 @@ "statut": "gestation", "resume": null, "radius": 12, - "family": "concept" + "family": "concept", + "domain": "substack.com" }, { "id": "anthropocene", @@ -110,7 +119,8 @@ "statut": "gestation", "resume": null, "radius": 12, - "family": "concept" + "family": "concept", + "domain": "substack.com" }, { "id": "education", @@ -120,7 +130,8 @@ "statut": "gestation", "resume": null, "radius": 12, - "family": "concept" + "family": "concept", + "domain": "substack.com" }, { "id": "urbanisme", @@ -130,7 +141,8 @@ "statut": "gestation", "resume": null, "radius": 12, - "family": "concept" + "family": "concept", + "domain": "substack.com" }, { "id": "geopolitique", @@ -140,7 +152,8 @@ "statut": "gestation", "resume": null, "radius": 12, - "family": "concept" + "family": "concept", + "domain": "substack.com" }, { "id": "ia-technologie", @@ -150,7 +163,8 @@ "statut": "gestation", "resume": null, "radius": 12, - "family": "concept" + "family": "concept", + "domain": "substack.com" }, { "id": "spiritualite", @@ -160,7 +174,8 @@ "statut": "gestation", "resume": null, "radius": 12, - "family": "concept" + "family": "concept", + "domain": "substack.com" }, { "id": "tmip", diff --git a/scripts/build-carte-o.js b/scripts/build-carte-o.js index 2348dfc..ae6180f 100644 --- a/scripts/build-carte-o.js +++ b/scripts/build-carte-o.js @@ -43,7 +43,7 @@ async function main() { } function addNode(obj) { - nodes.push({ + const node = { id: obj.id, label: obj.label, niveau: obj.niveau, @@ -52,7 +52,10 @@ async function main() { resume: obj.resume || null, radius: getRadius(obj.niveau, obj.nature), family: getFamily(obj.nature), - }) + } + // V1.2-O : propage le champ optionnel domain (logo plateforme via Brandfetch CDN) + if (obj.domain) node.domain = obj.domain + nodes.push(node) } const centreId = data.centre.id diff --git a/src/components/vue/CarteO.vue b/src/components/vue/CarteO.vue index 61cfa3e..c15c457 100644 --- a/src/components/vue/CarteO.vue +++ b/src/components/vue/CarteO.vue @@ -19,8 +19,15 @@ interface CarteNode { slug?: string theme?: string path?: string + domain?: string // V1.2-O : domaine plateforme source (logo Brandfetch CDN) } +// V1.2-O : logos plateforme via Brandfetch CDN (visible zoom > 1.5x seulement) +const BRANDFETCH_CLIENT_ID = '4ae58bd85c8140eab0cee72f40656120' +const LOGO_ZOOM_THRESHOLD = 1.5 +const logoUrl = (domain: string) => + `https://cdn.brandfetch.io/${domain}/w/64/h/64?c=${BRANDFETCH_CLIENT_ID}` + interface CarteEdge { source: string | CarteNode target: string | CarteNode @@ -187,6 +194,11 @@ function initSvg() { .scaleExtent([0.3, 4]) .on('zoom', (event) => { gZoom!.attr('transform', event.transform.toString()) + // V1.2-O : toggle visibilite logos plateforme selon le niveau de zoom + // (evite la surcharge visuelle au niveau d'ensemble, montre detail au drill-down) + const scale = event.transform.k + gZoom!.selectAll('.logo-overlay') + .style('opacity', scale > LOGO_ZOOM_THRESHOLD ? 1 : 0) }) svgRoot.call(zoomBehavior as any) @@ -290,6 +302,32 @@ function render() { } }) + // V1.2-O : Logo plateforme (visible zoom > 1.5x seulement) + // Cercle blanc 14px en bas-droite du node (offset +60% x/+60% y du radius) + // Image clip-path circle pour avatar style. Hidden par defaut (opacity 0). + const nodeGroupsWithLogo = nodeGroups.filter(d => !!d.domain) + + const logoOverlay = nodeGroupsWithLogo.append('g') + .attr('class', 'logo-overlay') + .style('opacity', 0) + .attr('pointer-events', 'none') + + logoOverlay.append('circle') + .attr('cx', d => getRadius(d) * 0.6) + .attr('cy', d => getRadius(d) * 0.6) + .attr('r', 9) + .attr('fill', '#FFFFFF') + .attr('stroke', '#0F172A') + .attr('stroke-width', 0.5) + + logoOverlay.append('image') + .attr('href', d => logoUrl(d.domain!)) + .attr('x', d => getRadius(d) * 0.6 - 7) + .attr('y', d => getRadius(d) * 0.6 - 7) + .attr('width', 14) + .attr('height', 14) + .attr('clip-path', d => `circle(7px at ${getRadius(d) * 0.6}px ${getRadius(d) * 0.6}px)`) + // Tooltip <title> nodeGroups.append('title') .text(d => `${d.label}\n[${d.family}]\n${truncate(d.resume || d.intention || '', 200)}`) From 12a2d403716aa999facea14a03545a8dd364407e Mon Sep 17 00:00:00 2001 From: Jules Neny <jules@trans-former.fr> Date: Mon, 11 May 2026 18:52:11 +0200 Subject: [PATCH 23/36] feat(v12-p): preview article 3 zones + colonnes scrollables indep - PreviewArticle.vue : nouveau composant qui ecoute journal-item-click et s'insere entre Carte O et iframe AEP - EmbedDynamique.vue : retire le swap article (iframe AEP toujours visible en bas) - ColCentre.astro : passe en flex-col, preview ouverte = Carte O 33vh + Preview auto + iframe 67vh, overflow-y-auto sur le container - Bouton 'Retour a la carte' emet preview-close -> grid revient 1/3 + 2/3 - Scroll independant : Journal (gauche), Centre (preview), Insta (droite) - Drag-resize desactive quand preview ouverte (anti-collision) --- src/components/astro/ColCentre.astro | 182 ++++++++++++++++++-------- src/components/vue/EmbedDynamique.vue | 142 +------------------- src/components/vue/PreviewArticle.vue | 152 +++++++++++++++++++++ 3 files changed, 280 insertions(+), 196 deletions(-) create mode 100644 src/components/vue/PreviewArticle.vue diff --git a/src/components/astro/ColCentre.astro b/src/components/astro/ColCentre.astro index fe7c548..26ceb80 100644 --- a/src/components/astro/ColCentre.astro +++ b/src/components/astro/ColCentre.astro @@ -1,13 +1,30 @@ --- // Centre - HAUT : tabs (Carte O mindmap | Chatbot RAG branche PC7). -// BAS : iframe carte AEP + scroll articles Substack (PC4). +// MILIEU : preview article (V1.2-P) - inseree au clic journal-item-click. +// BAS : iframe carte AEP (toujours visible). import CarteOWrapper from '../vue/CarteOWrapper.vue'; import ChatbotV2 from '../vue/ChatbotV2.vue'; import EmbedDynamique from '../vue/EmbedDynamique.vue'; +import PreviewArticle from '../vue/PreviewArticle.vue'; --- -<div id="col-centre-grid" class="h-full grid gap-2 p-2" style="grid-template-rows: 1fr 2fr;"> - <!-- HAUT 50% : tabs Carte O / Chatbot --> - <section id="col-centre-haut" class="border border-neutral-200 rounded flex flex-col overflow-hidden bg-white" style="min-height: 0;"> +<!-- + V1.2-P : Col centre = flex column container. + - Default : Carte O (1/3) + iframe AEP (2/3), pas de scroll vertical (h-full). + - Preview ouverte : Carte O (33vh fixe) + Preview (auto) + iframe AEP (67vh fixe), overflow-y-auto. + Flex-basis dynamique pilote via JS. +--> +<div + id="col-centre-grid" + class="flex flex-col gap-2 p-2" + data-preview-open="false" + style="height: 100%; overflow-y: hidden;" +> + <!-- HAUT (default flex-1 base 33%) : tabs Carte O / Chatbot --> + <section + id="col-centre-haut" + class="border border-neutral-200 rounded flex flex-col overflow-hidden bg-white" + style="min-height: 0; flex: 1 1 33%;" + > <nav role="tablist" aria-label="Vues centrales" class="flex border-b border-neutral-200 px-1 pt-1"> <button type="button" @@ -58,7 +75,7 @@ import EmbedDynamique from '../vue/EmbedDynamique.vue'; <!-- Drag handle desktop - redimensionnement vertical md+ --> <div id="col-centre-drag-handle" - class="hidden md:flex items-center justify-center h-2 cursor-row-resize hover:bg-neutral-200 transition-colors w-full -mt-1 -mb-1" + class="hidden md:flex items-center justify-center h-2 cursor-row-resize hover:bg-neutral-200 transition-colors w-full -mt-1 -mb-1 shrink-0" aria-hidden="true" > <span class="block w-10 h-0.5 bg-neutral-300 rounded-full"></span> @@ -69,13 +86,24 @@ import EmbedDynamique from '../vue/EmbedDynamique.vue'; id="col-centre-poignee" type="button" aria-label="Replier ou deployer la Carte O" - class="md:hidden flex items-center justify-center h-6 bg-neutral-100 border-y border-neutral-200 cursor-pointer w-full -mt-2 -mb-2 hover:bg-neutral-200 transition-colors" + class="md:hidden flex items-center justify-center h-6 bg-neutral-100 border-y border-neutral-200 cursor-pointer w-full -mt-2 -mb-2 hover:bg-neutral-200 transition-colors shrink-0" > <span class="block w-8 h-0.5 bg-neutral-400 rounded-full"></span> </button> - <!-- BAS 50% : embed dynamique (carte AEP default, article journal au click) --> - <section class="border border-neutral-200 rounded overflow-hidden bg-white" style="min-height: 0;"> + <!-- MILIEU (V1.2-P) : preview article inseree entre Carte O et iframe AEP. + Pas de border ici - PreviewArticle.vue gere son propre conteneur. + shrink-0 pour preserver sa taille auto, sinon flex pourrait l'ecraser. --> + <div id="col-centre-preview-slot" class="shrink-0" style="display: contents;"> + <PreviewArticle client:visible /> + </div> + + <!-- BAS (default flex-1 base 67%) : iframe carte AEP toujours visible --> + <section + id="col-centre-bas" + class="border border-neutral-200 rounded overflow-hidden bg-white" + style="min-height: 0; flex: 1 1 67%;" + > <div class="h-full min-h-[60vh] md:min-h-[400px]"> <EmbedDynamique client:visible /> </div> @@ -83,29 +111,32 @@ import EmbedDynamique from '../vue/EmbedDynamique.vue'; </div> <script> - // Poignee repli zone HAUT (mobile only, D.3) + // Poignee repli zone HAUT (mobile only) const grid = document.getElementById('col-centre-grid'); const haut = document.getElementById('col-centre-haut'); + const bas = document.getElementById('col-centre-bas'); const poignee = document.getElementById('col-centre-poignee'); + // Sauvegarde flex-basis defaults pour restaure apres fermeture preview + let defaultHautFlex = '1 1 33%'; + let defaultBasFlex = '1 1 67%'; + const applyRepliState = (replie: boolean) => { if (!grid || !haut) return; + if (grid.dataset.previewOpen === 'true') return; // skip si preview ouverte if (replie) { - grid.classList.remove('grid-rows-2'); - grid.style.gridTemplateRows = '0fr 1fr'; + haut.style.flex = '0 0 0%'; haut.style.overflow = 'hidden'; haut.style.minHeight = '0'; poignee?.setAttribute('aria-label', 'Deployer la Carte O'); } else { - grid.classList.remove('grid-rows-2'); - grid.style.gridTemplateRows = '1fr 2fr'; + haut.style.flex = defaultHautFlex; haut.style.overflow = ''; - haut.style.minHeight = ''; + haut.style.minHeight = '0'; poignee?.setAttribute('aria-label', 'Replier la Carte O'); } }; - // Etat initial depuis sessionStorage const savedRepli = sessionStorage.getItem('tf-haut-replie'); applyRepliState(savedRepli === 'true'); @@ -116,53 +147,78 @@ import EmbedDynamique from '../vue/EmbedDynamique.vue'; applyRepliState(next); }); - // Drag-resize desktop (>=768px) - const dragHandle = document.getElementById('col-centre-drag-handle'); - const gridEl = document.getElementById('col-centre-grid'); + // V1.2-P : preview ouverte = container scrollable, Carte O et iframe AEP figes en vh. + const applyPreviewState = (open: boolean) => { + if (!grid || !haut || !bas) return; + grid.dataset.previewOpen = String(open); + if (open) { + // Memorise les flex actuels avant override (au cas ou l'user a drag-resize) + const curHautFlex = haut.style.flex; + const curBasFlex = bas.style.flex; + if (curHautFlex && !curHautFlex.startsWith('0 0')) defaultHautFlex = curHautFlex; + if (curBasFlex) defaultBasFlex = curBasFlex; - if (dragHandle && gridEl) { + // Figer hauteurs : Carte O 33vh, iframe AEP 67vh - on perd le flex + haut.style.flex = '0 0 33vh'; + bas.style.flex = '0 0 67vh'; + // Le container reste a 100% du parent (overflow-hidden de <main>), + // mais on active le scroll interne pour passer Carte O -> Preview -> iframe AEP. + grid.style.height = '100%'; + grid.style.minHeight = ''; + grid.style.overflowY = 'auto'; + } else { + haut.style.flex = defaultHautFlex; + bas.style.flex = defaultBasFlex; + grid.style.height = '100%'; + grid.style.minHeight = ''; + grid.style.overflowY = 'hidden'; + } + }; + + window.addEventListener('journal-item-click', () => { + applyPreviewState(true); + // Scroll vers la preview apres mount + requestAnimationFrame(() => { + const preview = document.querySelector('.preview-article'); + if (preview && grid) { + const previewTop = (preview as HTMLElement).offsetTop; + grid.scrollTo({ top: Math.max(0, previewTop - 8), behavior: 'smooth' }); + } + }); + }); + window.addEventListener('preview-close', () => { + applyPreviewState(false); + }); + + // Drag-resize desktop (>=768px) - desactive quand preview ouverte + const dragHandle = document.getElementById('col-centre-drag-handle'); + + if (dragHandle && grid && haut && bas) { let isDragging = false; let startY = 0; - let startTop = 0; - - const getGridHeight = () => gridEl.getBoundingClientRect().height; - - const getHautPercent = (): number => { - const rows = gridEl.style.gridTemplateRows; - if (rows && rows.includes('fr')) { - const parts = rows.split(' '); - if (parts.length >= 2) { - const top = parseFloat(parts[0]) || 1; - const bot = parseFloat(parts[parts.length - 1]) || 1; - return (top / (top + bot)) * 100; - } - } - if (rows && rows.includes('%')) { - const parts = rows.split(' '); - return parseFloat(parts[0]) || 50; - } - return 33.33; - }; + let startHautH = 0; + let containerH = 0; dragHandle.addEventListener('mousedown', (e: MouseEvent) => { + if (grid.dataset.previewOpen === 'true') return; if (sessionStorage.getItem('tf-haut-replie') === 'true') return; isDragging = true; startY = e.clientY; - startTop = (getHautPercent() / 100) * getGridHeight(); + startHautH = haut.getBoundingClientRect().height; + containerH = grid.getBoundingClientRect().height; document.body.style.cursor = 'row-resize'; document.body.style.userSelect = 'none'; e.preventDefault(); }); document.addEventListener('mousemove', (e: MouseEvent) => { - if (!isDragging || !gridEl) return; + if (!isDragging) return; const delta = e.clientY - startY; - const totalH = getGridHeight(); - const newTop = Math.min(Math.max(startTop + delta, totalH * 0.2), totalH * 0.8); - const topPct = (newTop / totalH) * 100; - const botPct = 100 - topPct; - gridEl.style.gridTemplateRows = `${topPct.toFixed(1)}% ${botPct.toFixed(1)}%`; - gridEl.classList.remove('grid-rows-2'); + const newHautH = Math.min(Math.max(startHautH + delta, containerH * 0.2), containerH * 0.8); + const hautPct = (newHautH / containerH) * 100; + const basPct = 100 - hautPct; + haut.style.flex = `1 1 ${hautPct.toFixed(1)}%`; + bas.style.flex = `1 1 ${basPct.toFixed(1)}%`; }); document.addEventListener('mouseup', () => { @@ -170,23 +226,37 @@ import EmbedDynamique from '../vue/EmbedDynamique.vue'; isDragging = false; document.body.style.cursor = ''; document.body.style.userSelect = ''; - const rows = gridEl.style.gridTemplateRows; - if (rows) sessionStorage.setItem('tf-centre-rows', rows); + const hf = haut.style.flex; + const bf = bas.style.flex; + if (hf) { + sessionStorage.setItem('tf-centre-haut-flex', hf); + defaultHautFlex = hf; + } + if (bf) { + sessionStorage.setItem('tf-centre-bas-flex', bf); + defaultBasFlex = bf; + } } }); // Restaurer position depuis sessionStorage - const savedRows = sessionStorage.getItem('tf-centre-rows'); - if (savedRows && sessionStorage.getItem('tf-haut-replie') !== 'true') { - gridEl.style.gridTemplateRows = savedRows; - gridEl.classList.remove('grid-rows-2'); + const savedHF = sessionStorage.getItem('tf-centre-haut-flex'); + const savedBF = sessionStorage.getItem('tf-centre-bas-flex'); + if (savedHF && savedBF && sessionStorage.getItem('tf-haut-replie') !== 'true') { + haut.style.flex = savedHF; + bas.style.flex = savedBF; + defaultHautFlex = savedHF; + defaultBasFlex = savedBF; } // Double-click sur drag handle = reset default 1/3 + 2/3 dragHandle.addEventListener('dblclick', () => { - gridEl.style.gridTemplateRows = '1fr 2fr'; - gridEl.classList.remove('grid-rows-2'); - sessionStorage.removeItem('tf-centre-rows'); + haut.style.flex = '1 1 33%'; + bas.style.flex = '1 1 67%'; + sessionStorage.removeItem('tf-centre-haut-flex'); + sessionStorage.removeItem('tf-centre-bas-flex'); + defaultHautFlex = '1 1 33%'; + defaultBasFlex = '1 1 67%'; }); } diff --git a/src/components/vue/EmbedDynamique.vue b/src/components/vue/EmbedDynamique.vue index 949c0d6..bf53d6e 100644 --- a/src/components/vue/EmbedDynamique.vue +++ b/src/components/vue/EmbedDynamique.vue @@ -1,23 +1,11 @@ <script setup lang="ts"> import { ref, computed, onMounted, onUnmounted } from 'vue' -interface JournalItem { - id: string - platform: 'substack' | 'gitea' | 'github' | 'instagram' | 'castopod' | 'blog' | 'linkedin' - hashtag: string - date: string - titre: string - extrait: string - url: string - thumbnail: string | null -} - -const selectedItem = ref<JournalItem | null>(null) const iframeRef = ref<HTMLIFrameElement | null>(null) const wrapperRef = ref<HTMLDivElement | null>(null) const skeletonHidden = ref(false) -// Force rendu desktop de l'iframe AEP : viewport simulée 1440px + scale dynamique +// Force rendu desktop de l'iframe AEP : viewport simulee 1440px + scale dynamique const VIEWPORT_W = 1440 const iframeScale = ref(0.42) let resizeObs: ResizeObserver | null = null @@ -35,13 +23,7 @@ const iframeStyle = computed(() => ({ transformOrigin: '0 0', })) -const onJournalItemClick = (e: Event) => { - const ce = e as CustomEvent - if (ce.detail?.item) selectedItem.value = ce.detail.item -} - onMounted(() => { - window.addEventListener('journal-item-click', onJournalItemClick as EventListener) if (wrapperRef.value && typeof ResizeObserver !== 'undefined') { updateScale() resizeObs = new ResizeObserver(updateScale) @@ -49,16 +31,10 @@ onMounted(() => { } }) onUnmounted(() => { - window.removeEventListener('journal-item-click', onJournalItemClick as EventListener) resizeObs?.disconnect() resizeObs = null }) -const reset = () => { - selectedItem.value = null - skeletonHidden.value = false -} - const onIframeLoad = () => { if (iframeRef.value) { iframeRef.value.classList.remove('opacity-0') @@ -66,55 +42,11 @@ const onIframeLoad = () => { } skeletonHidden.value = true } - -const extractInstaShortcode = (url: string): string | null => { - const m = url.match(/\/(p|reel)\/([^\/\?#]+)/) - return m ? m[2] : null -} - -const embedUrl = computed(() => { - if (!selectedItem.value) return null - const item = selectedItem.value - if (item.platform === 'substack') { - return item.url.includes('?') ? item.url + '&embed=1' : item.url + '?embed=1' - } - if (item.platform === 'instagram') { - const sc = extractInstaShortcode(item.url) - return sc ? `https://www.instagram.com/p/${sc}/embed/` : null - } - return null -}) - -const platformLabel = (p: string) => { - const labels: Record<string, string> = { - substack: 'Substack', - instagram: 'Instagram', - gitea: 'Gitea', - github: 'GitHub', - castopod: 'Podcast', - blog: 'Blog', - linkedin: 'LinkedIn', - } - return labels[p] || p -} - -const formatDate = (iso: string) => { - try { - const d = new Date(iso) - return isNaN(d.getTime()) - ? '' - : `${String(d.getDate()).padStart(2, '0')}/${String(d.getMonth() + 1).padStart(2, '0')}` - } catch { - return '' - } -} </script> <template> <div class="embed-dynamique h-full flex flex-col relative"> - - <!-- DEFAULT : iframe AEP (aucun item selectionne) --> - <div v-if="!selectedItem" class="h-full"> + <div class="h-full"> <div ref="wrapperRef" class="relative h-full bg-neutral-100 overflow-hidden"> <div v-if="!skeletonHidden" @@ -134,75 +66,5 @@ const formatDate = (iso: string) => { ></iframe> </div> </div> - - <!-- EMBED MODE : teaser + embed live ou carte incitative --> - <div v-else class="h-full flex flex-col overflow-y-auto"> - - <!-- Header : reset + hashtag --> - <div class="flex items-center justify-between px-4 py-2 border-b border-neutral-200 bg-white sticky top-0 z-10"> - <button - class="text-xs text-neutral-500 hover:text-neutral-900 flex items-center gap-1" - @click="reset" - type="button" - > - - Retour a la carte - </button> - <span class="text-xs text-neutral-400" style="font-family: 'Courier New', Courier, monospace;"> - {{ selectedItem.hashtag }} - </span> - </div> - - <!-- Teaser --> - <div class="px-4 py-3 border-b border-neutral-100 bg-neutral-50"> - <p class="text-[11px] text-neutral-500 mb-1">{{ formatDate(selectedItem.date) }} - {{ selectedItem.platform }}</p> - <h3 class="text-sm font-semibold text-neutral-900 leading-snug mb-1">{{ selectedItem.titre }}</h3> - <p v-if="selectedItem.extrait" class="text-xs text-neutral-600 leading-relaxed line-clamp-3"> - {{ selectedItem.extrait }} - </p> - <img - v-if="selectedItem.thumbnail" - :src="selectedItem.thumbnail" - :alt="selectedItem.titre" - class="mt-2 w-full max-h-28 object-cover rounded" - loading="lazy" - /> - </div> - - <!-- Embed live (Substack ou Instagram) --> - <div v-if="embedUrl" class="flex-1 min-h-[200px] bg-white"> - <iframe - :src="embedUrl" - class="w-full h-full border-0 min-h-[300px]" - :title="selectedItem.titre" - loading="lazy" - sandbox="allow-scripts allow-same-origin allow-popups allow-forms allow-top-navigation" - ></iframe> - </div> - - <!-- Carte incitative (autres plateformes) --> - <div v-else class="flex-1 flex flex-col items-start justify-center px-4 py-6"> - <p class="text-xs text-neutral-500 mb-3 italic">Embed non disponible pour cette plateforme.</p> - <a - :href="selectedItem.url" - target="_blank" - rel="noopener noreferrer" - class="inline-block px-4 py-2 bg-neutral-900 text-white text-sm rounded-lg hover:bg-neutral-700 transition-colors" - > - Voir sur {{ platformLabel(selectedItem.platform) }} -> - </a> - </div> - - <!-- CTA propagation universel --> - <div class="px-4 py-3 border-t border-neutral-100"> - <a - :href="selectedItem.url" - target="_blank" - rel="noopener noreferrer" - class="text-xs text-neutral-500 hover:text-neutral-900 underline" - > - Continuer sur {{ platformLabel(selectedItem.platform) }} - commenter, partager - </a> - </div> - </div> </div> </template> diff --git a/src/components/vue/PreviewArticle.vue b/src/components/vue/PreviewArticle.vue new file mode 100644 index 0000000..55853e5 --- /dev/null +++ b/src/components/vue/PreviewArticle.vue @@ -0,0 +1,152 @@ +<script setup lang="ts"> +import { ref, computed, onMounted, onUnmounted } from 'vue' + +interface JournalItem { + id: string + platform: 'substack' | 'gitea' | 'github' | 'instagram' | 'castopod' | 'blog' | 'linkedin' + hashtag: string + date: string + titre: string + extrait: string + url: string + thumbnail: string | null +} + +const selectedItem = ref<JournalItem | null>(null) + +const onJournalItemClick = (e: Event) => { + const ce = e as CustomEvent + if (ce.detail?.item) selectedItem.value = ce.detail.item +} + +const onPreviewCloseExternal = () => { + selectedItem.value = null +} + +onMounted(() => { + window.addEventListener('journal-item-click', onJournalItemClick as EventListener) + window.addEventListener('preview-close-request', onPreviewCloseExternal as EventListener) +}) +onUnmounted(() => { + window.removeEventListener('journal-item-click', onJournalItemClick as EventListener) + window.removeEventListener('preview-close-request', onPreviewCloseExternal as EventListener) +}) + +const close = () => { + selectedItem.value = null + window.dispatchEvent(new CustomEvent('preview-close')) +} + +const extractInstaShortcode = (url: string): string | null => { + const m = url.match(/\/(p|reel)\/([^\/\?#]+)/) + return m ? m[2] : null +} + +const embedUrl = computed(() => { + if (!selectedItem.value) return null + const item = selectedItem.value + if (item.platform === 'substack') { + return item.url.includes('?') ? item.url + '&embed=1' : item.url + '?embed=1' + } + if (item.platform === 'instagram') { + const sc = extractInstaShortcode(item.url) + return sc ? `https://www.instagram.com/p/${sc}/embed/` : null + } + return null +}) + +const platformLabel = (p: string) => { + const labels: Record<string, string> = { + substack: 'Substack', + instagram: 'Instagram', + gitea: 'Gitea', + github: 'GitHub', + castopod: 'Podcast', + blog: 'Blog', + linkedin: 'LinkedIn', + } + return labels[p] || p +} + +const formatDate = (iso: string) => { + try { + const d = new Date(iso) + return isNaN(d.getTime()) + ? '' + : `${String(d.getDate()).padStart(2, '0')}/${String(d.getMonth() + 1).padStart(2, '0')}` + } catch { + return '' + } +} +</script> + +<template> + <div v-if="selectedItem" class="preview-article border border-neutral-200 rounded overflow-hidden bg-white flex flex-col"> + + <!-- Header : reset + hashtag --> + <div class="flex items-center justify-between px-4 py-2 border-b border-neutral-200 bg-white"> + <button + class="text-xs text-neutral-500 hover:text-neutral-900 flex items-center gap-1" + @click="close" + type="button" + > + - Retour a la carte + </button> + <span class="text-xs text-neutral-400" style="font-family: 'Courier New', Courier, monospace;"> + {{ selectedItem.hashtag }} + </span> + </div> + + <!-- Teaser --> + <div class="px-4 py-3 border-b border-neutral-100 bg-neutral-50"> + <p class="text-[11px] text-neutral-500 mb-1">{{ formatDate(selectedItem.date) }} - {{ selectedItem.platform }}</p> + <h3 class="text-sm font-semibold text-neutral-900 leading-snug mb-1">{{ selectedItem.titre }}</h3> + <p v-if="selectedItem.extrait" class="text-xs text-neutral-600 leading-relaxed line-clamp-3"> + {{ selectedItem.extrait }} + </p> + <img + v-if="selectedItem.thumbnail" + :src="selectedItem.thumbnail" + :alt="selectedItem.titre" + class="mt-2 w-full max-h-28 object-cover rounded" + loading="lazy" + /> + </div> + + <!-- Embed live (Substack ou Instagram) --> + <div v-if="embedUrl" class="bg-white" style="height: 60vh; min-height: 400px;"> + <iframe + :src="embedUrl" + class="w-full h-full border-0" + :title="selectedItem.titre" + loading="lazy" + sandbox="allow-scripts allow-same-origin allow-popups allow-forms allow-top-navigation" + ></iframe> + </div> + + <!-- Carte incitative (autres plateformes) --> + <div v-else class="flex flex-col items-start justify-center px-4 py-6"> + <p class="text-xs text-neutral-500 mb-3 italic">Embed non disponible pour cette plateforme.</p> + <a + :href="selectedItem.url" + target="_blank" + rel="noopener noreferrer" + class="inline-block px-4 py-2 bg-neutral-900 text-white text-sm rounded-lg hover:bg-neutral-700 transition-colors" + > + Voir sur {{ platformLabel(selectedItem.platform) }} -> + </a> + </div> + + <!-- CTA propagation universel --> + <div class="px-4 py-3 border-t border-neutral-100"> + <a + :href="selectedItem.url" + target="_blank" + rel="noopener noreferrer" + class="text-xs text-neutral-500 hover:text-neutral-900 underline" + > + Continuer sur {{ platformLabel(selectedItem.platform) }} - commenter, partager + </a> + </div> + </div> +</template> From 103309966339f115996d99ab5fca27db3d3fd6b3 Mon Sep 17 00:00:00 2001 From: Jules Neny <jules@trans-former.fr> Date: Mon, 11 May 2026 19:52:57 +0200 Subject: [PATCH 24/36] fix(v13-p0): iframe AEP skeleton timeout fallback (resolves loading stuck) Le @load event ne fire pas (ou tardivement) sur l'iframe AEP enfermee dans un wrapper avec transform scale(0.42) + viewport simulee 1440px. Resultat : skeleton 'Chargement de la carte AEP...' reste affiche indefiniment, masquant l'iframe meme si elle se charge. Fix : - setTimeout 2.5s dans onMounted qui force revealIframe() inconditionnellement - onIframeLoad clear le timer si l'event fire dans les temps (cas nominal) - retrait du z-10 sur le skeleton (defense en profondeur : si bug residuel, l'iframe sera quand meme visible derriere) - factorisation revealIframe() partagee entre @load et fallback - cleanup du timer dans onUnmounted Build SSR : 5 pages, 0 warning, ~4s. Tests browser manuels a faire par Jules pour confirmer disparition skeleton. --- src/components/vue/EmbedDynamique.vue | 29 ++++++++++++++++++++++----- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/src/components/vue/EmbedDynamique.vue b/src/components/vue/EmbedDynamique.vue index bf53d6e..1be71fc 100644 --- a/src/components/vue/EmbedDynamique.vue +++ b/src/components/vue/EmbedDynamique.vue @@ -23,24 +23,43 @@ const iframeStyle = computed(() => ({ transformOrigin: '0 0', })) +let fallbackTimer: ReturnType<typeof setTimeout> | null = null + +const revealIframe = () => { + if (iframeRef.value) { + iframeRef.value.classList.remove('opacity-0') + iframeRef.value.classList.add('opacity-100') + } + skeletonHidden.value = true +} + onMounted(() => { if (wrapperRef.value && typeof ResizeObserver !== 'undefined') { updateScale() resizeObs = new ResizeObserver(updateScale) resizeObs.observe(wrapperRef.value) } + // Fallback : si @load ne fire pas dans 2.5s (transform/scale peut bloquer l'event), + // on revele quand meme l'iframe pour ne pas laisser le skeleton infini. + fallbackTimer = setTimeout(() => { + revealIframe() + }, 2500) }) onUnmounted(() => { resizeObs?.disconnect() resizeObs = null + if (fallbackTimer) { + clearTimeout(fallbackTimer) + fallbackTimer = null + } }) const onIframeLoad = () => { - if (iframeRef.value) { - iframeRef.value.classList.remove('opacity-0') - iframeRef.value.classList.add('opacity-100') + if (fallbackTimer) { + clearTimeout(fallbackTimer) + fallbackTimer = null } - skeletonHidden.value = true + revealIframe() } </script> @@ -51,7 +70,7 @@ const onIframeLoad = () => { <div v-if="!skeletonHidden" id="embed-skeleton" - class="absolute inset-0 flex items-center justify-center bg-neutral-50 animate-pulse z-10" + class="absolute inset-0 flex items-center justify-center bg-neutral-50 animate-pulse" > <span class="text-neutral-400 text-sm">Chargement de la carte AEP...</span> </div> From e90a7e12ef0c4358fbee862111e3dcc26472b0cd Mon Sep 17 00:00:00 2001 From: Jules Neny <jules@trans-former.fr> Date: Mon, 11 May 2026 19:58:07 +0200 Subject: [PATCH 25/36] feat(v13-c): header 1-line cliquables + phrase intention Roboto Condensed --- package-lock.json | 10 +++ package.json | 1 + src/components/astro/SiteHeader.astro | 90 ++++++++++++++++++--------- 3 files changed, 70 insertions(+), 31 deletions(-) diff --git a/package-lock.json b/package-lock.json index 15bca63..4692d93 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "dependencies": { "@astrojs/node": "^10.1.0", "@astrojs/vue": "^6.0.1", + "@fontsource-variable/roboto-condensed": "^5.2.8", "@tailwindcss/vite": "^4.2.4", "@types/d3": "^7.4.3", "astro": "^6.3.1", @@ -1064,6 +1065,15 @@ "node": ">=18" } }, + "node_modules/@fontsource-variable/roboto-condensed": { + "version": "5.2.8", + "resolved": "https://registry.npmjs.org/@fontsource-variable/roboto-condensed/-/roboto-condensed-5.2.8.tgz", + "integrity": "sha512-aIZ2kYSoJHkTI4z8x/PRgKX6Zb9TTtSE/u+fUYeiwL+5trP9rhYYEEeNjRttaMqRgoDHcSueArdRZ43wf/i2Kw==", + "license": "OFL-1.1", + "funding": { + "url": "https://github.com/sponsors/ayuhito" + } + }, "node_modules/@img/colour": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", diff --git a/package.json b/package.json index 3f9eb4a..d6746a9 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "dependencies": { "@astrojs/node": "^10.1.0", "@astrojs/vue": "^6.0.1", + "@fontsource-variable/roboto-condensed": "^5.2.8", "@tailwindcss/vite": "^4.2.4", "@types/d3": "^7.4.3", "astro": "^6.3.1", diff --git a/src/components/astro/SiteHeader.astro b/src/components/astro/SiteHeader.astro index 38fa6b6..64361b5 100644 --- a/src/components/astro/SiteHeader.astro +++ b/src/components/astro/SiteHeader.astro @@ -1,52 +1,80 @@ --- -// SiteHeader.astro - V1.2-M : bandeau header pleine largeur identite site -// Palette terre figee : papier #FAFAF7, encre #0F172A, encre douce #475569 -// Composition retenue : 2 lignes hierarchique -// ligne 1 : "Trans-Former" wordmark dominant (semibold tracking serre) -// ligne 2 : "Jules Neny" + baseline italique cote a cote (separateur point median) -// Rationale : le wordmark domine sans ecraser ; la baseline reste lisible ; -// composition adaptee a un manifeste (hierarchie typographique forte). -// Hauteur : ~64px desktop / ~48px mobile (compacte) -// Baseline raccourcie mobile : "architecture politique du vivant" +// SiteHeader.astro - V1.3-C : header 1 ligne fine, liens cliquables, phrase intention Roboto Condensed +// Palette V1.3 figee : papier #FAFAF7, encre #0F172A, encre douce #475569, border #E5E7EB +// Composition : +// Desktop (>= md) : 1 ligne ~44px - Trans-Former | Jules Neny | architecture d'ecologie politique [phrase intention right-aligned, Roboto Condensed] +// Mobile (< md) : 2 lignes compactes - ligne 1 Trans-Former / ligne 2 Jules Neny . AEP (cliquables) - phrase intention masquee +// Liens : +// Trans-Former -> / +// Jules Neny -> /a-propos +// architecture d'ecologie politique -> https://aep.trans-former.fr (same-tab, site frere coherent) +// Typo phrase intention : Roboto Condensed Variable @fontsource (weight 400, font-stretch 75%) +import '@fontsource-variable/roboto-condensed/wght.css'; --- <header class="site-header w-full border-b border-[#E5E7EB] bg-[#FAFAF7] text-[#0F172A] px-4 md:px-6 flex items-center" role="banner" > - <a - href="/" - class="flex flex-col md:flex-row md:items-baseline gap-x-3 gap-y-0 no-underline text-[#0F172A] hover:text-[#0F172A]" - aria-label="trans-former.fr - retour accueil" + <!-- Bloc identite (gauche) --> + <nav + class="flex flex-col md:flex-row md:items-baseline gap-x-2 gap-y-0.5 flex-shrink-0" + aria-label="identite site" > - <!-- Ligne 1 : wordmark dominant --> - <span - class="font-semibold tracking-tight text-[#0F172A] text-[17px] md:text-[20px] leading-none" + <!-- Ligne 1 desktop = wordmark inline ; mobile = ligne dediee --> + <a + href="/" + class="font-semibold tracking-tight text-[#0F172A] hover:text-[#0F172A] text-[15px] md:text-[16px] leading-none no-underline hover:underline underline-offset-2 decoration-1" + aria-label="trans-former.fr - accueil" > Trans-Former - </span> + </a> - <!-- Ligne 2 (desktop : inline ; mobile : sous wordmark) : Jules Neny + baseline --> - <span class="flex items-baseline gap-2 text-[#475569] leading-none"> - <span class="text-[11px] md:text-[13px]">Jules Neny</span> + <!-- Bloc secondaire : Jules Neny . AEP (mobile : ligne 2 ; desktop : inline) --> + <span class="flex items-baseline gap-1.5 text-[#475569] leading-none"> + <span class="text-[#94A3B8] hidden md:inline" aria-hidden="true">·</span> + <a + href="/a-propos" + class="text-[12px] md:text-[13px] text-[#475569] hover:text-[#0F172A] no-underline hover:underline underline-offset-2 decoration-1" + > + Jules Neny + </a> <span class="text-[#94A3B8]" aria-hidden="true">·</span> - <!-- Baseline longue : desktop / Baseline courte : mobile --> - <span class="italic text-[11px] md:text-[13px] hidden sm:inline"> - architecture d'ecologie politique - </span> - <span class="italic text-[11px] inline sm:hidden"> - architecture politique du vivant - </span> + <a + href="https://aep.trans-former.fr" + class="text-[12px] md:text-[13px] text-[#475569] hover:text-[#0F172A] no-underline hover:underline underline-offset-2 decoration-1" + > + architecture d'écologie politique + </a> </span> - </a> + </nav> + + <!-- Phrase intention (droite, desktop only) - Roboto Condensed allongee --> + <p + class="intention hidden md:block ml-auto pl-6 text-right text-[#475569] text-[11px] leading-tight max-w-[55%]" + > + Comment créer une pratique systémique, créative et collective de transformation sociale pour répondre à l'effondrement et restaurer notre capacité à habiter la Terre dans l'Anthropocène ? + </p> </header> <style> .site-header { - height: 48px; + height: 44px; } - @media (min-width: 768px) { + @media (max-width: 767px) { .site-header { - height: 64px; + height: auto; + min-height: 48px; + padding-top: 6px; + padding-bottom: 6px; } } + /* Phrase intention : Roboto Condensed Variable, font-stretch 75% (allongement vertical condense) + Cantonnee a .intention pour eviter contagion stack Inter */ + .intention { + font-family: 'Roboto Condensed Variable', 'Roboto Condensed', sans-serif; + font-weight: 400; + font-stretch: 75%; + font-style: italic; + letter-spacing: 0.01em; + } </style> From aa410ce7aa7e6add0b7fe9dd71ac22e20d3a1f00 Mon Sep 17 00:00:00 2001 From: Jules Neny <jules@trans-former.fr> Date: Mon, 11 May 2026 20:00:16 +0200 Subject: [PATCH 26/36] feat(v13-bg): layout 1 ecran fixe + hamburger desktop hide + categorie Pro --- src/components/astro/ColJournal.astro | 18 ++++++++++++++++-- src/components/astro/HamburgerMenu.astro | 2 +- src/layouts/BaseLayout.astro | 14 +++++++++----- src/pages/a-propos.astro | 2 +- src/pages/index.astro | 18 ++++++++---------- src/pages/manifeste.astro | 2 +- src/pages/manifeste/commander.astro | 2 +- src/pages/mentions-legales.astro | 2 +- 8 files changed, 38 insertions(+), 22 deletions(-) diff --git a/src/components/astro/ColJournal.astro b/src/components/astro/ColJournal.astro index 81d1714..037c22b 100644 --- a/src/components/astro/ColJournal.astro +++ b/src/components/astro/ColJournal.astro @@ -34,6 +34,16 @@ const categories = [ ], hasSelector: false, }, + { + id: 'pro', + label: 'Pro', + color: '#0F172A', + hashtags: ['#building-public', '#pro'], + plateformes: [ + { id: 'linkedin', label: 'LinkedIn', url: 'https://www.linkedin.com/in/jules-neny/' }, + ], + hasSelector: false, + }, ]; --- <div class="h-full flex flex-col p-4 pt-20 md:pt-6 gap-5"> @@ -42,7 +52,7 @@ const categories = [ <details id="hashtags-accordion" class="border-t border-neutral-200 pt-4"> <summary class="font-semibold cursor-pointer select-none flex items-center justify-between"> <span>Hashtags</span> - <span class="text-xs text-neutral-400 font-normal">3 categories</span> + <span class="text-xs text-neutral-400 font-normal">4 categories</span> </summary> <div class="mt-3 flex flex-wrap gap-2" id="category-badges"> @@ -126,7 +136,7 @@ const categories = [ const PLATFORM_KEY = 'tf-platform-filter'; // Active state : map categoryId -> boolean - const activeCategories: Record<string, boolean> = { politique: true, art: true, outils: true }; + const activeCategories: Record<string, boolean> = { politique: true, art: true, outils: true, pro: true }; // Platform filter : map categoryId -> platformId | null const platformFilters: Record<string, string | null> = { politique: null }; @@ -138,15 +148,18 @@ const categories = [ const politiqueHashtags = ['#politique', '#aep-politique']; const artHashtags = ['#peinture', '#art']; const outilsHashtags = ['#stack', '#building-public']; + const proHashtags = ['#building-public', '#pro']; const allPolitique = politiqueHashtags.every(h => storedHashtags[h] !== false); const allArt = artHashtags.every(h => storedHashtags[h] !== false); const allOutils = outilsHashtags.every(h => storedHashtags[h] !== false); + const allPro = proHashtags.every(h => storedHashtags[h] !== false); if (Object.keys(storedHashtags).length > 0) { activeCategories['politique'] = allPolitique; activeCategories['art'] = allArt; activeCategories['outils'] = allOutils; + activeCategories['pro'] = allPro; } } catch { /* mode prive */ } @@ -163,6 +176,7 @@ const categories = [ politique: ['#politique', '#aep-politique'], art: ['#peinture', '#art'], outils: ['#stack', '#building-public'], + pro: ['#building-public', '#pro'], }; for (const [catId, tags] of Object.entries(catHashtags)) { const active = activeCategories[catId] ?? true; diff --git a/src/components/astro/HamburgerMenu.astro b/src/components/astro/HamburgerMenu.astro index 66121dc..df838a1 100644 --- a/src/components/astro/HamburgerMenu.astro +++ b/src/components/astro/HamburgerMenu.astro @@ -5,7 +5,7 @@ <button id="hamburger-trigger" type="button" - class="fixed top-4 right-4 z-50 p-3 bg-white/95 border border-neutral-200 rounded-lg shadow-md hover:bg-white transition-colors md:top-6 md:right-6" + class="fixed top-4 right-4 z-50 p-3 bg-white/95 border border-neutral-200 rounded-lg shadow-md hover:bg-white transition-colors md:hidden" aria-label="Ouvrir le menu" aria-expanded="false" aria-controls="hamburger-drawer" diff --git a/src/layouts/BaseLayout.astro b/src/layouts/BaseLayout.astro index 77e6397..3f0224c 100644 --- a/src/layouts/BaseLayout.astro +++ b/src/layouts/BaseLayout.astro @@ -14,7 +14,7 @@ const { } = Astro.props; --- <!doctype html> -<html lang="fr"> +<html lang="fr" class="h-screen"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" /> @@ -29,11 +29,15 @@ const { <meta name="twitter:title" content={title} /> <meta name="twitter:description" content={description} /> </head> - <body class="m-0 bg-white text-neutral-900 antialiased min-h-screen flex flex-col"> - <SiteHeader /> - <div class="flex-1 flex flex-col min-h-0"> + <body class="m-0 bg-white text-neutral-900 antialiased h-screen flex flex-col overflow-hidden"> + <div class="flex-shrink-0"> + <SiteHeader /> + </div> + <div class="flex-1 flex flex-col min-h-0 overflow-hidden"> <slot /> </div> - <Footer /> + <div class="flex-shrink-0"> + <Footer /> + </div> </body> </html> diff --git a/src/pages/a-propos.astro b/src/pages/a-propos.astro index 6a836ad..83d3250 100644 --- a/src/pages/a-propos.astro +++ b/src/pages/a-propos.astro @@ -10,7 +10,7 @@ import HamburgerMenu from '../components/astro/HamburgerMenu.astro'; > <HamburgerMenu /> - <main class="min-h-screen bg-white"> + <main class="h-full overflow-y-auto bg-white"> <article class="max-w-2xl mx-auto px-6 py-16 md:py-24"> <header class="mb-10"> diff --git a/src/pages/index.astro b/src/pages/index.astro index eb3aaf9..17d0213 100644 --- a/src/pages/index.astro +++ b/src/pages/index.astro @@ -15,18 +15,16 @@ import PopupOnboarding from '../components/astro/PopupOnboarding.astro'; <MobileTabBar /> <PopupOnboarding /> - <!-- Desktop : grid 3 colonnes (header 64px deja consommes par SiteHeader, on prend le reste) --> - <div - class="hidden md:grid md:grid-cols-[320px_1fr_320px] overflow-hidden" - style="height: calc(100vh - 64px);" - > - <aside class="border-r border-neutral-200 overflow-y-auto"><ColJournal /></aside> - <main class="overflow-hidden"><ColCentre /></main> - <aside class="border-l border-neutral-200 overflow-y-auto"><ColInsta /></aside> + <!-- Desktop : grid 3 colonnes V1.3-BG : prend toute la place du wrapper flex-1 du BaseLayout. + Header et footer sont fixes (flex-shrink-0), pas besoin de calc(100vh - X). --> + <div class="hidden md:grid md:grid-cols-[320px_1fr_320px] h-full overflow-hidden"> + <aside class="border-r border-neutral-200 overflow-y-auto h-full"><ColJournal /></aside> + <main class="overflow-hidden h-full"><ColCentre /></main> + <aside class="border-l border-neutral-200 overflow-y-auto h-full"><ColInsta /></aside> </div> - <!-- Mobile : SwipeContainer Vue island - header 48px + tabbar 44px = 92px reserves --> - <div class="md:hidden overflow-hidden" style="height: calc(100dvh - 48px - 44px); margin-top: 44px;"> + <!-- Mobile : SwipeContainer Vue island - tabbar 44px reserve dans la zone flex-1 --> + <div class="md:hidden h-full overflow-hidden" style="padding-top: 44px;"> <SwipeContainer client:load> <ColJournal slot="left" /> <ColCentre slot="center" /> diff --git a/src/pages/manifeste.astro b/src/pages/manifeste.astro index ee64a58..2ef5816 100644 --- a/src/pages/manifeste.astro +++ b/src/pages/manifeste.astro @@ -10,7 +10,7 @@ import HamburgerMenu from '../components/astro/HamburgerMenu.astro'; > <HamburgerMenu /> - <main class="min-h-screen bg-white"> + <main class="h-full overflow-y-auto bg-white"> <article class="max-w-2xl mx-auto px-6 py-16 md:py-24"> <!-- En-tete --> diff --git a/src/pages/manifeste/commander.astro b/src/pages/manifeste/commander.astro index 5006043..d2c7dbc 100644 --- a/src/pages/manifeste/commander.astro +++ b/src/pages/manifeste/commander.astro @@ -10,7 +10,7 @@ import HamburgerMenu from '../../components/astro/HamburgerMenu.astro'; > <HamburgerMenu /> - <main class="min-h-screen bg-white"> + <main class="h-full overflow-y-auto bg-white"> <article class="max-w-xl mx-auto px-6 py-16 md:py-24"> <header class="mb-10"> diff --git a/src/pages/mentions-legales.astro b/src/pages/mentions-legales.astro index 9d1c1fb..635a9cc 100644 --- a/src/pages/mentions-legales.astro +++ b/src/pages/mentions-legales.astro @@ -10,7 +10,7 @@ import HamburgerMenu from '../components/astro/HamburgerMenu.astro'; > <HamburgerMenu /> - <main class="min-h-screen bg-white"> + <main class="h-full overflow-y-auto bg-white"> <article class="max-w-2xl mx-auto px-6 py-16 md:py-24"> <header class="mb-10"> From 8f8b0c5f4cf97a0a6ca84f0592279f13058bd981 Mon Sep 17 00:00:00 2001 From: Jules Neny <jules@trans-former.fr> Date: Mon, 11 May 2026 20:00:30 +0200 Subject: [PATCH 27/36] feat(v13-d): Carte O Option B rectangle central + bandeau sommaire + legende + TMIP relie MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - YAML carte-o-source : label central -> 'Une medecine du corps social pour ecrire un nouveau contrat social' (phrase pleine 3 lignes) - YAML : projet TMIP gagne lien_central:true (edge explicite centre <-> projet) - build-carte-o.js : addEdge accepte opts.central=true pour tagger les edges rattachees au noeud central (permet tuning force-link cote Vue) - carte-o.json regenere : 17 nodes, 20 edges (vs 19 V1.2-O), tous les edges central->thematiques + central->tmip portent flag central:true - CarteO.vue : noeud central rendu en RECT 300x64 fill encre (vs cercle r30), label blanc multi-tspan 3 lignes 13px font-weight 500 line-height 1.35 - CarteO.vue : splitCentralLabel reecrit pour wrap intelligent (3 lignes ~30 chars), preserve compat ' + ' (V1.2) - CarteO.vue : force tuning V1.3 -> alphaDecay 0.025, velocityDecay 0.4, forceCollide +12 (CENTRAL_COLLIDE_RADIUS=160 pour le rect), forceX/Y strength 0.05 rappel cadre, link distance/strength differencies (central->projet = 90/0.6, central->essai = 200/0.3) - CarteO.vue : hover handler selector etendu rect|circle - CarteOWrapper.vue : CarteEdge gagne champ central?:boolean - ColCentre.astro : tabs Chatbot retires (ChatbotV2 import retire aussi), remplaces par header bandeau 'Sommaire editorial d'architecture d'ecologie politique' (gauche, monospace 12px) + legende 3 symboles (publie ● / a venir ○ / projet 🟠) en droite Build SSR : 5 pages prerender, 0 warning, 4.35s. --- public/data/carte-o-source.yaml | 4 +- public/data/carte-o.json | 54 ++++++++---- scripts/build-carte-o.js | 14 ++- src/components/astro/ColCentre.astro | 104 ++++++++-------------- src/components/vue/CarteO.vue | 125 +++++++++++++++++++++------ src/components/vue/CarteOWrapper.vue | 1 + 6 files changed, 186 insertions(+), 116 deletions(-) diff --git a/public/data/carte-o-source.yaml b/public/data/carte-o-source.yaml index 40ad47f..711cb7c 100644 --- a/public/data/carte-o-source.yaml +++ b/public/data/carte-o-source.yaml @@ -12,7 +12,7 @@ version: "1.1" centre: id: "contrat-social-medecine-corps-social" - label: "Contrat social + Medecine du corps social" + label: "Une medecine du corps social pour ecrire un nouveau contrat social" niveau: 0 nature: essai statut: gestation @@ -119,6 +119,8 @@ projets: nature: projet statut: gestation resume: "Transport, mobilite, industrie, politique - projet archi. Exemple de projet archi relie aux thematiques AEP." + # V1.3-D : lien explicite au noeud central (pont vision <-> pratique) + lien_central: true liens_thematiques: - "urbanisme" - "justice-securite" diff --git a/public/data/carte-o.json b/public/data/carte-o.json index 6c0f168..f188dfc 100644 --- a/public/data/carte-o.json +++ b/public/data/carte-o.json @@ -1,10 +1,10 @@ { "version": "1.1", - "generatedAt": "2026-05-11T16:47:45.459Z", + "generatedAt": "2026-05-11T17:59:41.381Z", "nodes": [ { "id": "contrat-social-medecine-corps-social", - "label": "Contrat social + Medecine du corps social", + "label": "Une medecine du corps social pour ecrire un nouveau contrat social", "niveau": 0, "nature": "essai", "statut": "gestation", @@ -191,63 +191,83 @@ "edges": [ { "source": "contrat-social-medecine-corps-social", - "target": "systemique" + "target": "systemique", + "central": true }, { "source": "contrat-social-medecine-corps-social", - "target": "pratiques-collectives" + "target": "pratiques-collectives", + "central": true }, { "source": "contrat-social-medecine-corps-social", - "target": "art-narration" + "target": "art-narration", + "central": true }, { "source": "contrat-social-medecine-corps-social", - "target": "pouvoir-domination" + "target": "pouvoir-domination", + "central": true }, { "source": "contrat-social-medecine-corps-social", - "target": "medias-critique" + "target": "medias-critique", + "central": true }, { "source": "contrat-social-medecine-corps-social", - "target": "justice-securite" + "target": "justice-securite", + "central": true }, { "source": "contrat-social-medecine-corps-social", - "target": "sante-globale" + "target": "sante-globale", + "central": true }, { "source": "contrat-social-medecine-corps-social", - "target": "agriculture" + "target": "agriculture", + "central": true }, { "source": "contrat-social-medecine-corps-social", - "target": "post-croissance" + "target": "post-croissance", + "central": true }, { "source": "contrat-social-medecine-corps-social", - "target": "anthropocene" + "target": "anthropocene", + "central": true }, { "source": "contrat-social-medecine-corps-social", - "target": "education" + "target": "education", + "central": true }, { "source": "contrat-social-medecine-corps-social", - "target": "urbanisme" + "target": "urbanisme", + "central": true }, { "source": "contrat-social-medecine-corps-social", - "target": "geopolitique" + "target": "geopolitique", + "central": true }, { "source": "contrat-social-medecine-corps-social", - "target": "ia-technologie" + "target": "ia-technologie", + "central": true }, { "source": "contrat-social-medecine-corps-social", - "target": "spiritualite" + "target": "spiritualite", + "central": true + }, + { + "source": "contrat-social-medecine-corps-social", + "target": "tmip", + "central": true }, { "source": "tmip", diff --git a/scripts/build-carte-o.js b/scripts/build-carte-o.js index ae6180f..b568aed 100644 --- a/scripts/build-carte-o.js +++ b/scripts/build-carte-o.js @@ -35,11 +35,15 @@ async function main() { const edges = [] const edgeSet = new Set() - function addEdge(source, target) { + function addEdge(source, target, opts = {}) { const key = source < target ? `${source}|${target}` : `${target}|${source}` if (edgeSet.has(key)) return edgeSet.add(key) - edges.push({ source, target }) + const edge = { source, target } + // V1.3-D : tag les edges au noeud central pour permettre tuning force-link + // (TMIP relie au centre = link court/fort, autres essais = link standard) + if (opts.central) edge.central = true + edges.push(edge) } function addNode(obj) { @@ -70,11 +74,15 @@ async function main() { // toutes les thematiques rattachees directement au noeud central for (const th of data.thematiques) { addNode(th) - addEdge(centreId, th.id) + addEdge(centreId, th.id, { central: true }) } for (const proj of data.projets) { addNode(proj) + // V1.3-D : edge explicite projet -> central (pont vision <-> pratique) + if (proj.lien_central) { + addEdge(centreId, proj.id, { central: true }) + } for (const thId of (proj.liens_thematiques || [])) { addEdge(proj.id, thId) } diff --git a/src/components/astro/ColCentre.astro b/src/components/astro/ColCentre.astro index 26ceb80..bc898cc 100644 --- a/src/components/astro/ColCentre.astro +++ b/src/components/astro/ColCentre.astro @@ -1,9 +1,9 @@ --- -// Centre - HAUT : tabs (Carte O mindmap | Chatbot RAG branche PC7). +// Centre - HAUT : Carte O mindmap (V1.3-D : onglet Chatbot retire, bandeau "Sommaire editorial" + legende). // MILIEU : preview article (V1.2-P) - inseree au clic journal-item-click. // BAS : iframe carte AEP (toujours visible). +// V1.3-D : ChatbotV2 retire du DOM (backlog V2). Pour reactivation -> reintroduire le tab + panel. import CarteOWrapper from '../vue/CarteOWrapper.vue'; -import ChatbotV2 from '../vue/ChatbotV2.vue'; import EmbedDynamique from '../vue/EmbedDynamique.vue'; import PreviewArticle from '../vue/PreviewArticle.vue'; --- @@ -19,56 +19,49 @@ import PreviewArticle from '../vue/PreviewArticle.vue'; data-preview-open="false" style="height: 100%; overflow-y: hidden;" > - <!-- HAUT (default flex-1 base 33%) : tabs Carte O / Chatbot --> + <!-- HAUT (default flex-1 base 33%) : V1.3-D bandeau "Sommaire editorial" + legende + Carte O plein espace --> <section id="col-centre-haut" class="border border-neutral-200 rounded flex flex-col overflow-hidden bg-white" style="min-height: 0; flex: 1 1 33%;" > - <nav role="tablist" aria-label="Vues centrales" class="flex border-b border-neutral-200 px-1 pt-1"> - <button - type="button" - role="tab" - id="tab-mindmap" - aria-controls="panel-mindmap" - aria-selected="true" - data-tab="mindmap" - class="tab-btn px-3 py-2 text-sm border-b-2 border-neutral-900 font-medium text-neutral-900" - > - Carte O - </button> - <button - type="button" - role="tab" - id="tab-chatbot" - aria-controls="panel-chatbot" - aria-selected="false" - data-tab="chatbot" - class="tab-btn px-3 py-2 text-sm border-b-2 border-transparent text-neutral-500 hover:text-neutral-900" - > - Chatbot - </button> - </nav> + <!-- V1.3-D : bandeau header (titre gauche + legende droite) --> + <header + class="flex items-center justify-between gap-3 px-3 py-2 border-b border-neutral-200 shrink-0" + style="font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;" + > + <span class="text-xs truncate" style="color: #475569;"> + Sommaire éditorial d'architecture d'écologie politique + </span> + <ul class="flex items-center gap-3 shrink-0 text-xs" style="color: #475569;" aria-label="Légende"> + <li class="flex items-center gap-1.5"> + <span + aria-hidden="true" + style="width: 8px; height: 8px; border-radius: 999px; background: #0F172A; display: inline-block;" + ></span> + <span>publié</span> + </li> + <li class="flex items-center gap-1.5"> + <span + aria-hidden="true" + style="width: 8px; height: 8px; border-radius: 999px; background: transparent; border: 1px solid #0F172A; display: inline-block;" + ></span> + <span>à venir</span> + </li> + <li class="flex items-center gap-1.5"> + <span + aria-hidden="true" + style="width: 8px; height: 8px; border-radius: 999px; background: #B45309; display: inline-block;" + ></span> + <span>projet</span> + </li> + </ul> + </header> <div class="flex-1 overflow-hidden relative"> - <div - id="panel-mindmap" - role="tabpanel" - aria-labelledby="tab-mindmap" - data-tab-panel="mindmap" - class="absolute inset-0" - > + <div id="panel-mindmap" class="absolute inset-0"> <CarteOWrapper client:visible /> </div> - <div - id="panel-chatbot" - role="tabpanel" - aria-labelledby="tab-chatbot" - data-tab-panel="chatbot" - class="absolute inset-0 hidden" - > - <ChatbotV2 client:visible /> - </div> </div> </section> @@ -260,28 +253,5 @@ import PreviewArticle from '../vue/PreviewArticle.vue'; }); } - // Tabs toggle. - const tabs = document.querySelectorAll<HTMLButtonElement>('[data-tab]'); - const panels = document.querySelectorAll<HTMLElement>('[data-tab-panel]'); - - tabs.forEach((tab) => { - tab.addEventListener('click', () => { - const target = tab.dataset.tab; - if (!target) return; - - tabs.forEach((t) => { - const active = t.dataset.tab === target; - t.setAttribute('aria-selected', active ? 'true' : 'false'); - t.classList.toggle('border-neutral-900', active); - t.classList.toggle('border-transparent', !active); - t.classList.toggle('font-medium', active); - t.classList.toggle('text-neutral-900', active); - t.classList.toggle('text-neutral-500', !active); - }); - - panels.forEach((p) => { - p.classList.toggle('hidden', p.dataset.tabPanel !== target); - }); - }); - }); + // V1.3-D : tabs Chatbot retires, plus de toggle a gerer (un seul panel Carte O). </script> diff --git a/src/components/vue/CarteO.vue b/src/components/vue/CarteO.vue index c15c457..794cd56 100644 --- a/src/components/vue/CarteO.vue +++ b/src/components/vue/CarteO.vue @@ -25,12 +25,18 @@ interface CarteNode { // V1.2-O : logos plateforme via Brandfetch CDN (visible zoom > 1.5x seulement) const BRANDFETCH_CLIENT_ID = '4ae58bd85c8140eab0cee72f40656120' const LOGO_ZOOM_THRESHOLD = 1.5 + +// V1.3-D : dimensions rectangle central (phrase manifeste 3 lignes) +const CENTRAL_W = 300 +const CENTRAL_H = 64 +const CENTRAL_COLLIDE_RADIUS = 160 // demi-largeur + marge const logoUrl = (domain: string) => `https://cdn.brandfetch.io/${domain}/w/64/h/64?c=${BRANDFETCH_CLIENT_ID}` interface CarteEdge { source: string | CarteNode target: string | CarteNode + central?: boolean // V1.3-D : link au noeud central (court/fort) } const props = withDefaults(defineProps<{ @@ -118,25 +124,38 @@ function truncate(str: string, max: number): string { return str.length > max ? str.slice(0, max - 1) + '…' : str } -// Split intelligent du label central sur 2-3 lignes (autour de "+") -function splitCentralLabel(label: string): string[] { +// V1.3-D : Wrap intelligent du label central sur 3 lignes (rectangle 300x64). +// Phrase cible : "Une medecine du corps social pour ecrire un nouveau contrat social" +// Heuristique : decoupe en mots, repartit sur ~3 lignes de longueur equilibree. +function splitCentralLabel(label: string, maxLines = 3, maxCharsPerLine = 30): string[] { if (!label) return [''] - // priorite : split sur " + " si present + // Fallback : pivot legacy " + " (V1.2 backward compat) if (label.includes(' + ')) { const parts = label.split(' + ') return [parts[0].trim(), '+', parts.slice(1).join(' + ').trim()] } - // fallback : split a peu pres au milieu sur un espace - if (label.length > 14) { - const mid = Math.floor(label.length / 2) - const left = label.lastIndexOf(' ', mid) - const right = label.indexOf(' ', mid) - const cut = (mid - left <= right - mid && left > 0) ? left : right - if (cut > 0) { - return [label.slice(0, cut).trim(), label.slice(cut).trim()] + const words = label.split(/\s+/).filter(Boolean) + if (words.length <= 1) return [label] + + // Repartit en lignes en respectant maxCharsPerLine, max maxLines lignes + const lines: string[] = [] + let current = '' + for (const w of words) { + const next = current ? `${current} ${w}` : w + if (next.length > maxCharsPerLine && current && lines.length < maxLines - 1) { + lines.push(current) + current = w + } else { + current = next } } - return [label] + if (current) lines.push(current) + // Si plus de lignes que prevu (mots tres longs), concatene les dernieres + while (lines.length > maxLines) { + const last = lines.pop()! + lines[lines.length - 1] = `${lines[lines.length - 1]} ${last}` + } + return lines } function buildSimNodes(): SimNode[] { @@ -232,18 +251,33 @@ function render() { .style('cursor', 'pointer') .on('click', (_event, d) => emit('node-click', { node: d as CarteNode, x: (d as SimNode).x || 0, y: (d as SimNode).y || 0 })) .on('mouseover', function (_event, d) { - d3.select(this).select('circle') + // V1.3-D : selecteur etendu rect|circle (rect pour central, circle pour autres) + d3.select(this).select('rect, circle') .transition().duration(120) .attr('stroke-width', strokeWidthFor(d) + 1.5) }) .on('mouseout', function (_event, d) { - d3.select(this).select('circle') + d3.select(this).select('rect, circle') .transition().duration(120) .attr('stroke-width', strokeWidthFor(d)) }) - // Cercle - nodeGroups.append('circle') + // V1.3-D : nœud central = rectangle 300x64 (phrase 3 lignes), autres = cercle + const isCentral = (d: SimNode) => d.niveau === 0 + + nodeGroups.filter(isCentral) + .append('rect') + .attr('x', -CENTRAL_W / 2) + .attr('y', -CENTRAL_H / 2) + .attr('width', CENTRAL_W) + .attr('height', CENTRAL_H) + .attr('rx', 6) + .attr('fill', d => colorFor(d)) + .attr('stroke', d => strokeFor(d)) + .attr('stroke-width', d => strokeWidthFor(d)) + + nodeGroups.filter(d => !isCentral(d)) + .append('circle') .attr('r', d => getRadius(d)) .attr('fill', d => colorFor(d)) .attr('stroke', d => strokeFor(d)) @@ -256,10 +290,10 @@ function render() { const inside = labelInsideFor(d) if (inside && d.niveau === 0) { - // Noeud central : label en 2 lignes inscrites au centre - const parts = splitCentralLabel(d.label) - const lineHeight = isMobile.value ? 10 : 12 - const fs = isMobile.value ? 9 : 11 + // V1.3-D : noeud central = rectangle, label 3 lignes 13px line-height 1.35 + const fs = isMobile.value ? 11 : 13 + const lineHeight = Math.round(fs * 1.35) + const parts = splitCentralLabel(d.label, 3, isMobile.value ? 22 : 30) const startY = -((parts.length - 1) * lineHeight) / 2 parts.forEach((line, i) => { g.append('text') @@ -269,7 +303,7 @@ function render() { .attr('y', startY + i * lineHeight) .attr('font-size', fs) .attr('font-family', 'system-ui, sans-serif') - .attr('font-weight', 'bold') + .attr('font-weight', '500') .attr('fill', '#FFFFFF') .attr('pointer-events', 'none') .text(line) @@ -332,21 +366,54 @@ function render() { nodeGroups.append('title') .text(d => `${d.label}\n[${d.family}]\n${truncate(d.resume || d.intention || '', 200)}`) - // Simulation force avec charges differenciees par niveau + // V1.3-D : Simulation force-directed fit-cadre + animation continue subtile + // - link distance/strength differencies : TMIP (central) court/fort, autres essais souples + // - collide +12 zero overlap labels + // - forceX/Y faibles rappel cadre + // - alphaDecay 0.025 (anime sans vertige) simulation = d3.forceSimulation<SimNode, SimLink>(simNodes) .force('link', d3.forceLink<SimNode, SimLink>(simLinks) .id(d => d.id) - .distance(80) - .strength(0.35)) + .distance(l => { + const link = l as SimLink & { central?: boolean } + // Lien central -> TMIP (projet) = court (90), autres centraux (essais) = standard (200) + if (link.central) { + const tgt = link.target as SimNode + const src = link.source as SimNode + const other = (tgt.niveau === 0 ? src : tgt) as SimNode + if (other.nature === 'projet') return 90 + return 200 + } + return 180 + }) + .strength(l => { + const link = l as SimLink & { central?: boolean } + if (link.central) { + const tgt = link.target as SimNode + const src = link.source as SimNode + const other = (tgt.niveau === 0 ? src : tgt) as SimNode + if (other.nature === 'projet') return 0.6 + return 0.3 + } + return 0.3 + })) .force('charge', d3.forceManyBody<SimNode>().strength(d => { - if (d.niveau === 0) return -800 + if (d.niveau === 0) return -1200 if (d.niveau === 1) return -400 - if (d.niveau === 2) return -150 + if (d.niveau === 2) return -180 return -220 })) .force('center', d3.forceCenter(width.value / 2, height.value / 2)) - .force('collide', d3.forceCollide<SimNode>().radius(d => getRadius(d) + 6)) - .alphaDecay(0.03) + .force('x', d3.forceX<SimNode>(width.value / 2).strength(0.05)) + .force('y', d3.forceY<SimNode>(height.value / 2).strength(0.05)) + .force('collide', d3.forceCollide<SimNode>().radius(d => { + // V1.3-D : central traite comme zone large (rect 300x64 -> rayon equivalent 160) + if (d.niveau === 0) return CENTRAL_COLLIDE_RADIUS + return getRadius(d) + 12 + })) + .alphaDecay(0.025) + .velocityDecay(0.4) + .alphaMin(0.001) .on('tick', tick) // Bind drag once simulation exists. @@ -409,6 +476,8 @@ onMounted(() => { } if (simulation) { simulation.force('center', d3.forceCenter(width.value / 2, height.value / 2)) + simulation.force('x', d3.forceX<SimNode>(width.value / 2).strength(0.05)) + simulation.force('y', d3.forceY<SimNode>(height.value / 2).strength(0.05)) simulation.alpha(0.3).restart() } }) diff --git a/src/components/vue/CarteOWrapper.vue b/src/components/vue/CarteOWrapper.vue index feecad9..af5a014 100644 --- a/src/components/vue/CarteOWrapper.vue +++ b/src/components/vue/CarteOWrapper.vue @@ -19,6 +19,7 @@ interface CarteNode { interface CarteEdge { source: string target: string + central?: boolean // V1.3-D : edges au noeud central (tuning force-link) } interface CarteData { From b4f6d63f33953da79932a862e0e65c8e3a3fd425 Mon Sep 17 00:00:00 2001 From: Jules Neny <jules@trans-former.fr> Date: Mon, 11 May 2026 20:08:20 +0200 Subject: [PATCH 28/36] feat(v13-e): manifeste UX preview centrale (racine) + page /manifeste preserve standalone --- public/data/carte-o.json | 2 +- src/components/astro/ColCentre.astro | 8 +- src/components/astro/ColJournal.astro | 16 ++- src/components/astro/Footer.astro | 14 ++- src/components/vue/ManifesteContent.vue | 156 ++++++++++++++++++++++++ src/components/vue/PreviewArticle.vue | 152 +++++++++++++++-------- 6 files changed, 292 insertions(+), 56 deletions(-) create mode 100644 src/components/vue/ManifesteContent.vue diff --git a/public/data/carte-o.json b/public/data/carte-o.json index f188dfc..3aa3196 100644 --- a/public/data/carte-o.json +++ b/public/data/carte-o.json @@ -1,6 +1,6 @@ { "version": "1.1", - "generatedAt": "2026-05-11T17:59:41.381Z", + "generatedAt": "2026-05-11T18:07:36.883Z", "nodes": [ { "id": "contrat-social-medecine-corps-social", diff --git a/src/components/astro/ColCentre.astro b/src/components/astro/ColCentre.astro index bc898cc..de8a75f 100644 --- a/src/components/astro/ColCentre.astro +++ b/src/components/astro/ColCentre.astro @@ -168,7 +168,9 @@ import PreviewArticle from '../vue/PreviewArticle.vue'; } }; - window.addEventListener('journal-item-click', () => { + // V1.2-P : ouverture preview article via journal-item-click + // V1.3-E : ouverture preview unifiee via preview-open (article OU manifeste) + const openPreview = () => { applyPreviewState(true); // Scroll vers la preview apres mount requestAnimationFrame(() => { @@ -178,7 +180,9 @@ import PreviewArticle from '../vue/PreviewArticle.vue'; grid.scrollTo({ top: Math.max(0, previewTop - 8), behavior: 'smooth' }); } }); - }); + }; + window.addEventListener('journal-item-click', openPreview); + window.addEventListener('preview-open', openPreview); window.addEventListener('preview-close', () => { applyPreviewState(false); }); diff --git a/src/components/astro/ColJournal.astro b/src/components/astro/ColJournal.astro index 037c22b..d835b92 100644 --- a/src/components/astro/ColJournal.astro +++ b/src/components/astro/ColJournal.astro @@ -96,9 +96,10 @@ const categories = [ </button> </div> - <!-- Bouton Manifeste --> + <!-- Bouton Manifeste (V1.3-E : sur "/" -> preview centrale, ailleurs -> page complete) --> <a href="/manifeste" + data-manifeste-link class="block mt-3 px-4 py-2 bg-neutral-900 text-white font-bold text-sm text-center rounded-lg hover:bg-neutral-700 transition-colors" style="font-family:'Courier New',Courier,monospace;" > @@ -289,4 +290,17 @@ const categories = [ if (platformFilters['politique']) { dispatchPlatform(platformFilters['politique']); } + + // V1.3-E : intercept clics liens Manifeste -> preview centrale (uniquement sur racine /) + // Tagger les liens [data-manifeste-link] et router vers event 'preview-open' si on est sur /. + // Sinon : laisser la navigation native vers /manifeste. + document.querySelectorAll<HTMLAnchorElement>('a[data-manifeste-link]').forEach((el) => { + el.addEventListener('click', (e) => { + if (window.location.pathname === '/') { + e.preventDefault(); + window.dispatchEvent(new CustomEvent('preview-open', { detail: { type: 'manifeste' } })); + } + // sinon : navigation normale vers /manifeste (fallback SEO + no-JS) + }); + }); </script> diff --git a/src/components/astro/Footer.astro b/src/components/astro/Footer.astro index 147c35c..8d22c7b 100644 --- a/src/components/astro/Footer.astro +++ b/src/components/astro/Footer.astro @@ -7,7 +7,7 @@ <!-- ZONE GAUCHE : liens nav --> <nav class="flex gap-4 text-xs justify-center md:justify-start"> - <a href="/manifeste" class="opacity-60 hover:opacity-100 transition-opacity">Manifeste</a> + <a href="/manifeste" data-manifeste-link class="opacity-60 hover:opacity-100 transition-opacity">Manifeste</a> <a href="/a-propos" class="opacity-60 hover:opacity-100 transition-opacity">A propos</a> <a href="/mentions-legales" class="opacity-60 hover:opacity-100 transition-opacity">Mentions legales</a> </nav> @@ -125,4 +125,16 @@ msg.textContent = 'erreur reseau - reessaie plus tard'; } }); + + // V1.3-E : intercept clics liens Manifeste depuis le footer -> preview centrale sur racine. + // Sur /a-propos, /mentions-legales, /manifeste lui-meme : navigation normale. + document.querySelectorAll<HTMLAnchorElement>('a[data-manifeste-link]').forEach((el) => { + el.addEventListener('click', (e) => { + if (window.location.pathname === '/') { + e.preventDefault(); + window.dispatchEvent(new CustomEvent('preview-open', { detail: { type: 'manifeste' } })); + } + // sinon : navigation normale vers /manifeste + }); + }); </script> diff --git a/src/components/vue/ManifesteContent.vue b/src/components/vue/ManifesteContent.vue new file mode 100644 index 0000000..b684c4b --- /dev/null +++ b/src/components/vue/ManifesteContent.vue @@ -0,0 +1,156 @@ +<script setup lang="ts"> +// V1.3-E : contenu manifeste pour preview centrale (mode racine /). +// TODO V1.4 : extraire contenu manifeste dans src/content/manifeste.md (content collection) +// pour avoir une source unique partagee avec src/pages/manifeste.astro. +// Actuellement : duplication consciente du texte (~50 lignes) entre les 2 fichiers. +// Si modif texte -> repercuter aux 2 endroits (this file + manifeste.astro). +</script> + +<template> + <article class="manifeste-content px-4 md:px-6 py-6 md:py-8 bg-white"> + + <!-- En-tete --> + <header class="mb-8 md:mb-10"> + <p class="text-[10px] uppercase tracking-widest text-neutral-500 mb-2"> + Manifeste + </p> + <h1 class="text-xl md:text-2xl font-semibold text-neutral-900 leading-tight"> + Architecture d'Écologie Politique + </h1> + <p class="mt-3 text-neutral-600 text-sm md:text-base leading-relaxed"> + Un commun vivant pour bifurquer ; ensemble, lentement, par accumulation de petits gestes situés. + </p> + <div class="mt-5 flex flex-wrap gap-2"> + <a + href="/manifeste/commander" + class="inline-block px-4 py-2 border border-neutral-900 text-neutral-900 rounded-lg font-medium text-sm hover:bg-neutral-900 hover:text-white transition-colors" + > + Commander la version imprimée + </a> + <a + href="/manifeste" + class="inline-block px-4 py-2 border border-neutral-300 text-neutral-700 rounded-lg text-sm hover:border-neutral-900 hover:text-neutral-900 transition-colors" + title="Ouvrir la page complete dans un nouvel onglet" + target="_blank" + rel="noopener" + > + Page complète → + </a> + </div> + </header> + + <!-- Corps du manifeste --> + <div class="text-neutral-800 text-[14px] md:text-[15px] leading-[1.7] space-y-4"> + + <p class="italic text-neutral-700"> + Un quart des architectes vivent sous le seuil de pauvreté. La moitié de nos heures, non facturées. Nos cotisations, parmi les plus lourdes des professions réglementées. Et le secteur du bâtiment, à lui seul, pèse 34% des émissions mondiales de gaz à effet de serre. + </p> + + <p> + Quelque chose s'est rompu ; pas dans nos vies, dans les cadres qui les contiennent. + </p> + + <p> + Notre profession ne traverse pas une simple crise. Elle reflète l'effondrement d'un monde qui confond performance et destruction, signature et silence, expertise et soumission. + </p> + + <hr class="border-neutral-200 my-6" /> + + <h2 class="text-base md:text-lg font-semibold text-neutral-900 mt-8 mb-3"> + Ce que nous voyons. + </h2> + + <p> + À l'échelle du métier, une profession structurellement sous l'eau, qui absorbe les tensions d'un système extractiviste ; et porte la responsabilité quand d'autres captent la valeur. + </p> + + <p> + À l'échelle des corps, une culture qui rend l'exploitation désirable : métier-passion, modèle starchitecte, isolement libéral, moteur critique délégitimant. Nous tenons. Nous payons. + </p> + + <p> + À l'échelle du monde, l'effondrement écologique et social qui avance, pendant que notre voix s'efface du débat public. Notre silence le sert. + </p> + + <hr class="border-neutral-200 my-6" /> + + <h2 class="text-base md:text-lg font-semibold text-neutral-900 mt-8 mb-3"> + Ce que nous refusons. + </h2> + + <p> + Nous ne signerons plus pour des projets qui détruisent.<br /> + Nous n'isolerons plus celles et ceux qui doutent.<br /> + Nous ne porterons plus seul-es ce qui doit se penser, se faire ; et se soigner ; ensemble. + </p> + + <hr class="border-neutral-200 my-6" /> + + <!-- Pivot : centre emotionnel du texte (italique + retrait) --> + <blockquote class="my-8 md:my-10 px-4 md:px-6 py-4 border-l-4 border-neutral-900 italic text-neutral-800 text-sm md:text-base leading-relaxed"> + <p class="font-medium not-italic mb-2 text-neutral-900"> + Et pourtant, quelque chose tient. + </p> + <p> + Pas l'espoir naïf, ni la promesse héroïque. Quelque chose de plus humble : la fatigue commune reconnue, et l'envie qui revient de ne plus économiser sa vie. + </p> + </blockquote> + + <hr class="border-neutral-200 my-6" /> + + <h2 class="text-base md:text-lg font-semibold text-neutral-900 mt-8 mb-3"> + Ce que nous tentons. + </h2> + + <p> + <em class="font-semibold not-italic">Partager.</em> Nos parcours, nos doutes, nos bifurcations. Se former les un-es les autres. Se tendre la main. Documenter ce qui marche, ce qui rate. Le personnel devient politique quand il se met en commun. + </p> + + <p> + <em class="font-semibold not-italic">Construire.</em> L'infrastructure collective qui nous a manqué. Cartes d'entraide, communs documentés, gouvernance horizontale, financement transparent, infra souveraine. <strong>Architecture d'Écologie Politique</strong> : un commun vivant, ouvert, biorégional, ancré. + </p> + + <p> + <em class="font-semibold not-italic">Pratiquer une médecine du corps social.</em> Diagnostiquer les infrastructures qui défaillent ; l'éducation, la justice, la sécurité, l'énergie, la santé, le logement, l'agriculture. Proposer des reconfigurations situées, territoire par territoire. Reprendre le pouvoir par la base. Écrire, lentement, un nouveau contrat social. + </p> + + <p> + <em class="font-semibold not-italic">Commencer par les marges.</em> Là où le corps social souffre le plus, là où il est le plus prêt à changer. Ne pas décider à la place ; faire émerger. Transparence totale, sur le process et sur l'argent. Tendresse militante : la lucidité sans le mépris, l'engagement sans la dureté. + </p> + + <hr class="border-neutral-200 my-6" /> + + <h2 class="text-base md:text-lg font-semibold text-neutral-900 mt-8 mb-3"> + Architectes, allié-es, habitant-es. + </h2> + + <p> + Nous avons un travail à faire ensemble. Lentement, patiemment, par accumulation de petits gestes situés. Pas pour fuir ; pour bifurquer. + </p> + + <!-- Chute : italique, separee --> + <p class="mt-6 md:mt-8 italic text-neutral-900 text-sm md:text-base leading-relaxed"> + Nos métiers sont des médecines. Reprenons-en le pouls ; à mains nues, ensemble. + </p> + </div> + + <!-- Footer simple : lien vers page complete pour les detail (mouvements, etc.) --> + <footer class="mt-10 pt-6 border-t border-neutral-200 text-center"> + <a + href="/manifeste" + target="_blank" + rel="noopener" + class="text-xs text-neutral-500 hover:text-neutral-900 underline" + > + Voir la page complète (mouvements M/S/XL, liens articles) → + </a> + </footer> + + </article> +</template> + +<style scoped> +.manifeste-content { + font-family: ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif; +} +</style> diff --git a/src/components/vue/PreviewArticle.vue b/src/components/vue/PreviewArticle.vue index 55853e5..24eab0f 100644 --- a/src/components/vue/PreviewArticle.vue +++ b/src/components/vue/PreviewArticle.vue @@ -1,5 +1,14 @@ <script setup lang="ts"> +// V1.3-E : PreviewArticle gere 2 modes : +// - 'article' (V1.2-P existant) : teaser + embed Substack / Instagram / CTA propagation +// - 'manifeste' (V1.3-E) : affiche <ManifesteContent /> dans la preview centrale +// Events ecoutes : +// - 'journal-item-click' -> mode 'article' (legacy V1.2-P, conserve tel quel) +// - 'preview-open' -> nouvel event unifie, payload { type: 'article' | 'manifeste', data?: ... } +// - 'preview-close-request'-> close externe +// Event emis : 'preview-close' (utilise par ColCentre pour restaurer la mise en page). import { ref, computed, onMounted, onUnmounted } from 'vue' +import ManifesteContent from './ManifesteContent.vue' interface JournalItem { id: string @@ -12,28 +21,50 @@ interface JournalItem { thumbnail: string | null } +type PreviewMode = 'article' | 'manifeste' | null + +const mode = ref<PreviewMode>(null) const selectedItem = ref<JournalItem | null>(null) const onJournalItemClick = (e: Event) => { const ce = e as CustomEvent - if (ce.detail?.item) selectedItem.value = ce.detail.item + if (ce.detail?.item) { + selectedItem.value = ce.detail.item + mode.value = 'article' + } +} + +const onPreviewOpen = (e: Event) => { + const ce = e as CustomEvent + const type = ce.detail?.type + if (type === 'manifeste') { + mode.value = 'manifeste' + selectedItem.value = null + } else if (type === 'article' && ce.detail?.item) { + selectedItem.value = ce.detail.item + mode.value = 'article' + } } const onPreviewCloseExternal = () => { selectedItem.value = null + mode.value = null } onMounted(() => { window.addEventListener('journal-item-click', onJournalItemClick as EventListener) + window.addEventListener('preview-open', onPreviewOpen as EventListener) window.addEventListener('preview-close-request', onPreviewCloseExternal as EventListener) }) onUnmounted(() => { window.removeEventListener('journal-item-click', onJournalItemClick as EventListener) + window.removeEventListener('preview-open', onPreviewOpen as EventListener) window.removeEventListener('preview-close-request', onPreviewCloseExternal as EventListener) }) const close = () => { selectedItem.value = null + mode.value = null window.dispatchEvent(new CustomEvent('preview-close')) } @@ -81,9 +112,9 @@ const formatDate = (iso: string) => { </script> <template> - <div v-if="selectedItem" class="preview-article border border-neutral-200 rounded overflow-hidden bg-white flex flex-col"> + <div v-if="mode" class="preview-article border border-neutral-200 rounded overflow-hidden bg-white flex flex-col"> - <!-- Header : reset + hashtag --> + <!-- Header : reset + label (hashtag article OU "Manifeste") --> <div class="flex items-center justify-between px-4 py-2 border-b border-neutral-200 bg-white"> <button class="text-xs text-neutral-500 hover:text-neutral-900 flex items-center gap-1" @@ -92,61 +123,80 @@ const formatDate = (iso: string) => { > - Retour a la carte </button> - <span class="text-xs text-neutral-400" style="font-family: 'Courier New', Courier, monospace;"> + <span + v-if="mode === 'article' && selectedItem" + class="text-xs text-neutral-400" + style="font-family: 'Courier New', Courier, monospace;" + > {{ selectedItem.hashtag }} </span> + <span + v-else-if="mode === 'manifeste'" + class="text-xs text-neutral-400" + style="font-family: 'Courier New', Courier, monospace;" + > + #manifeste + </span> </div> - <!-- Teaser --> - <div class="px-4 py-3 border-b border-neutral-100 bg-neutral-50"> - <p class="text-[11px] text-neutral-500 mb-1">{{ formatDate(selectedItem.date) }} - {{ selectedItem.platform }}</p> - <h3 class="text-sm font-semibold text-neutral-900 leading-snug mb-1">{{ selectedItem.titre }}</h3> - <p v-if="selectedItem.extrait" class="text-xs text-neutral-600 leading-relaxed line-clamp-3"> - {{ selectedItem.extrait }} - </p> - <img - v-if="selectedItem.thumbnail" - :src="selectedItem.thumbnail" - :alt="selectedItem.titre" - class="mt-2 w-full max-h-28 object-cover rounded" - loading="lazy" - /> + <!-- Mode 'manifeste' : contenu manifeste --> + <div v-if="mode === 'manifeste'" class="bg-white"> + <ManifesteContent /> </div> - <!-- Embed live (Substack ou Instagram) --> - <div v-if="embedUrl" class="bg-white" style="height: 60vh; min-height: 400px;"> - <iframe - :src="embedUrl" - class="w-full h-full border-0" - :title="selectedItem.titre" - loading="lazy" - sandbox="allow-scripts allow-same-origin allow-popups allow-forms allow-top-navigation" - ></iframe> - </div> + <!-- Mode 'article' : V1.2-P legacy --> + <template v-else-if="mode === 'article' && selectedItem"> + <!-- Teaser --> + <div class="px-4 py-3 border-b border-neutral-100 bg-neutral-50"> + <p class="text-[11px] text-neutral-500 mb-1">{{ formatDate(selectedItem.date) }} - {{ selectedItem.platform }}</p> + <h3 class="text-sm font-semibold text-neutral-900 leading-snug mb-1">{{ selectedItem.titre }}</h3> + <p v-if="selectedItem.extrait" class="text-xs text-neutral-600 leading-relaxed line-clamp-3"> + {{ selectedItem.extrait }} + </p> + <img + v-if="selectedItem.thumbnail" + :src="selectedItem.thumbnail" + :alt="selectedItem.titre" + class="mt-2 w-full max-h-28 object-cover rounded" + loading="lazy" + /> + </div> - <!-- Carte incitative (autres plateformes) --> - <div v-else class="flex flex-col items-start justify-center px-4 py-6"> - <p class="text-xs text-neutral-500 mb-3 italic">Embed non disponible pour cette plateforme.</p> - <a - :href="selectedItem.url" - target="_blank" - rel="noopener noreferrer" - class="inline-block px-4 py-2 bg-neutral-900 text-white text-sm rounded-lg hover:bg-neutral-700 transition-colors" - > - Voir sur {{ platformLabel(selectedItem.platform) }} -> - </a> - </div> + <!-- Embed live (Substack ou Instagram) --> + <div v-if="embedUrl" class="bg-white" style="height: 60vh; min-height: 400px;"> + <iframe + :src="embedUrl" + class="w-full h-full border-0" + :title="selectedItem.titre" + loading="lazy" + sandbox="allow-scripts allow-same-origin allow-popups allow-forms allow-top-navigation" + ></iframe> + </div> - <!-- CTA propagation universel --> - <div class="px-4 py-3 border-t border-neutral-100"> - <a - :href="selectedItem.url" - target="_blank" - rel="noopener noreferrer" - class="text-xs text-neutral-500 hover:text-neutral-900 underline" - > - Continuer sur {{ platformLabel(selectedItem.platform) }} - commenter, partager - </a> - </div> + <!-- Carte incitative (autres plateformes) --> + <div v-else class="flex flex-col items-start justify-center px-4 py-6"> + <p class="text-xs text-neutral-500 mb-3 italic">Embed non disponible pour cette plateforme.</p> + <a + :href="selectedItem.url" + target="_blank" + rel="noopener noreferrer" + class="inline-block px-4 py-2 bg-neutral-900 text-white text-sm rounded-lg hover:bg-neutral-700 transition-colors" + > + Voir sur {{ platformLabel(selectedItem.platform) }} -> + </a> + </div> + + <!-- CTA propagation universel --> + <div class="px-4 py-3 border-t border-neutral-100"> + <a + :href="selectedItem.url" + target="_blank" + rel="noopener noreferrer" + class="text-xs text-neutral-500 hover:text-neutral-900 underline" + > + Continuer sur {{ platformLabel(selectedItem.platform) }} - commenter, partager + </a> + </div> + </template> </div> </template> From e012d11bee46c8b63aa5e22ca53b0ab01b50bf87 Mon Sep 17 00:00:00 2001 From: Jules Neny <jules@trans-former.fr> Date: Mon, 11 May 2026 20:14:28 +0200 Subject: [PATCH 29/36] feat(v13-f): palette desopacifiee pills + toggle cols laterales + footer gap-6 + manifeste CTA bord rouge --- src/components/astro/ColJournal.astro | 22 +++++-- src/components/astro/Footer.astro | 3 +- src/components/vue/ManifesteContent.vue | 23 +++++-- src/pages/index.astro | 87 +++++++++++++++++++++++-- src/pages/manifeste.astro | 23 ++++--- 5 files changed, 132 insertions(+), 26 deletions(-) diff --git a/src/components/astro/ColJournal.astro b/src/components/astro/ColJournal.astro index d835b92..6fe73ff 100644 --- a/src/components/astro/ColJournal.astro +++ b/src/components/astro/ColJournal.astro @@ -1,11 +1,14 @@ --- import JournalList from '../vue/JournalList.vue'; +// V1.3-F : palette desopacifiee — chaque categorie a une pastel pour le fill pill inactive. +// Regle bicouche : pastel = FILL pill inactive uniquement. Border + texte = encre couleur 100%. const categories = [ { id: 'politique', label: 'Politique', color: '#B5443A', + pastel: '#E5C3BE', hashtags: ['#politique', '#aep-politique'], plateformes: [ { id: 'instagram', label: 'Court', url: 'https://www.instagram.com/aep.politique/' }, @@ -18,6 +21,7 @@ const categories = [ id: 'art', label: 'Art', color: '#5B6B3A', + pastel: '#CACFBE', hashtags: ['#peinture', '#art'], plateformes: [ { id: 'instagram', label: '@julesneny', url: 'https://www.instagram.com/julesneny/' }, @@ -28,6 +32,7 @@ const categories = [ id: 'outils', label: 'Outils', color: '#475569', + pastel: '#C4C8CC', hashtags: ['#stack', '#building-public'], plateformes: [ { id: 'gitea', label: 'Gitea', url: 'https://git.trans-former.fr/jules' }, @@ -38,6 +43,7 @@ const categories = [ id: 'pro', label: 'Pro', color: '#0F172A', + pastel: '#C4C8CC', hashtags: ['#building-public', '#pro'], plateformes: [ { id: 'linkedin', label: 'LinkedIn', url: 'https://www.linkedin.com/in/jules-neny/' }, @@ -62,6 +68,7 @@ const categories = [ data-category-id={cat.id} data-hashtags={cat.hashtags.join(',')} data-color={cat.color} + data-pastel={cat.pastel} data-has-selector={cat.hasSelector ? 'true' : 'false'} class="category-badge" style={`background:${cat.color};color:#fff;font-family:'Courier New',Courier,monospace;font-size:13px;padding:3px 10px;border-radius:4px;cursor:pointer;border:1px solid ${cat.color};`} @@ -71,14 +78,14 @@ const categories = [ ))} </div> - <!-- Selecteur plateforme Politique --> + <!-- Selecteur plateforme Politique (V1.3-F : pastel #E5C3BE en fill inactive) --> <div id="politique-selector" class="mt-2 hidden flex gap-2"> {categories[0].plateformes.map((p) => ( <button type="button" data-platform-id={p.id} class="platform-pill" - style="font-family:'Courier New',Courier,monospace;font-size:12px;padding:2px 8px;border-radius:12px;cursor:pointer;border:1px solid #B5443A;background:transparent;color:#B5443A;" + style="font-family:'Courier New',Courier,monospace;font-size:12px;padding:2px 8px;border-radius:12px;cursor:pointer;border:1px solid #B5443A;background:#E5C3BE;color:#B5443A;" > {p.label} </button> @@ -203,14 +210,18 @@ const categories = [ window.dispatchEvent(new CustomEvent('platform-filter-change', { detail: { platform } })); }; + // V1.3-F : palette desopacifiee. + // Active = fill couleur 100% + texte blanc. + // Inactive = fill pastel + texte couleur 100% + border couleur 100% (regle bicouche). const updateBadgeStyle = (btn: HTMLElement, active: boolean) => { const color = btn.dataset.color || '#000'; + const pastel = btn.dataset.pastel || '#E5C3BE'; if (active) { btn.style.background = color; btn.style.color = '#fff'; btn.style.border = `1px solid ${color}`; } else { - btn.style.background = 'transparent'; + btn.style.background = pastel; btn.style.color = color; btn.style.border = `1px solid ${color}`; } @@ -228,6 +239,9 @@ const categories = [ } }; + // V1.3-F : sub-pills Politique, regle bicouche identique. + // Active (ou aucun filtre) = fill brique #B5443A + texte blanc. + // Inactive = fill pastel #E5C3BE + texte brique + border brique 100%. const updatePillStyles = () => { const pills = document.querySelectorAll<HTMLElement>('.platform-pill'); const active = platformFilters['politique']; @@ -237,7 +251,7 @@ const categories = [ pill.style.background = '#B5443A'; pill.style.color = '#fff'; } else { - pill.style.background = 'transparent'; + pill.style.background = '#E5C3BE'; pill.style.color = '#B5443A'; } }); diff --git a/src/components/astro/Footer.astro b/src/components/astro/Footer.astro index 8d22c7b..a1e7a8a 100644 --- a/src/components/astro/Footer.astro +++ b/src/components/astro/Footer.astro @@ -33,7 +33,8 @@ </div> <!-- ZONE DROITE : logos RS cliquables (SVG inline, fill #0F172A 60%) --> - <div class="flex gap-3 items-center justify-center md:justify-end text-[#0F172A]"> + <!-- V1.3-F : gap-4 mobile / gap-6 desktop pour espacer les logos --> + <div class="flex gap-4 md:gap-6 items-center justify-center md:justify-end text-[#0F172A]"> <!-- Instagram --> <a href="https://www.instagram.com/aep.politique/" diff --git a/src/components/vue/ManifesteContent.vue b/src/components/vue/ManifesteContent.vue index b684c4b..1cf9a14 100644 --- a/src/components/vue/ManifesteContent.vue +++ b/src/components/vue/ManifesteContent.vue @@ -20,13 +20,8 @@ <p class="mt-3 text-neutral-600 text-sm md:text-base leading-relaxed"> Un commun vivant pour bifurquer ; ensemble, lentement, par accumulation de petits gestes situés. </p> + <!-- V1.3-F : CTA "commander" deplace tout en bas (apres lecture). Le seul lien d'entete = page complete. --> <div class="mt-5 flex flex-wrap gap-2"> - <a - href="/manifeste/commander" - class="inline-block px-4 py-2 border border-neutral-900 text-neutral-900 rounded-lg font-medium text-sm hover:bg-neutral-900 hover:text-white transition-colors" - > - Commander la version imprimée - </a> <a href="/manifeste" class="inline-block px-4 py-2 border border-neutral-300 text-neutral-700 rounded-lg text-sm hover:border-neutral-900 hover:text-neutral-900 transition-colors" @@ -134,8 +129,22 @@ </p> </div> + <!-- V1.3-F : CTA commander version papier — bord rouge brique #B5443A, signal politique fort. + Place en bas, apres lecture : j'achete, tranquille. --> + <section class="mt-10 pt-6 border-t border-neutral-200 text-center"> + <p class="text-xs italic text-neutral-500 mb-3"> + Manifeste lu. J'achete la version papier, tranquille. + </p> + <a + href="/manifeste/commander" + class="inline-block px-5 py-2.5 border-2 border-[#B5443A] text-[#0F172A] bg-[#FAFAF7] font-medium text-sm hover:bg-[#E5C3BE] transition-colors" + > + Commander la version papier + </a> + </section> + <!-- Footer simple : lien vers page complete pour les detail (mouvements, etc.) --> - <footer class="mt-10 pt-6 border-t border-neutral-200 text-center"> + <footer class="mt-6 pt-4 border-t border-neutral-200 text-center"> <a href="/manifeste" target="_blank" diff --git a/src/pages/index.astro b/src/pages/index.astro index 17d0213..864d9d3 100644 --- a/src/pages/index.astro +++ b/src/pages/index.astro @@ -15,12 +15,39 @@ import PopupOnboarding from '../components/astro/PopupOnboarding.astro'; <MobileTabBar /> <PopupOnboarding /> - <!-- Desktop : grid 3 colonnes V1.3-BG : prend toute la place du wrapper flex-1 du BaseLayout. - Header et footer sont fixes (flex-shrink-0), pas besoin de calc(100vh - X). --> - <div class="hidden md:grid md:grid-cols-[320px_1fr_320px] h-full overflow-hidden"> - <aside class="border-r border-neutral-200 overflow-y-auto h-full"><ColJournal /></aside> - <main class="overflow-hidden h-full"><ColCentre /></main> - <aside class="border-l border-neutral-200 overflow-y-auto h-full"><ColInsta /></aside> + <!-- Desktop : grid 3 colonnes V1.3-F avec toggle replier latéraux. + Layout dynamique pilote par CSS vars --col-left / --col-right (320px par defaut, 0 quand replie). + Header et footer fixes (flex-shrink-0), pas de calc(100vh - X). --> + <div + id="desktop-grid" + class="hidden md:grid h-full overflow-hidden relative" + style="grid-template-columns: var(--col-left, 320px) 1fr var(--col-right, 320px);" + > + <aside id="col-left-aside" class="border-r border-neutral-200 overflow-y-auto h-full"><ColJournal /></aside> + <main class="overflow-hidden h-full relative"> + <ColCentre /> + <!-- Toggle gauche : pose sur le bord gauche de la colonne centrale --> + <button + type="button" + id="toggle-col-left" + aria-label="Replier/deplier colonne gauche" + class="hidden md:flex absolute left-0 top-1/2 -translate-y-1/2 z-20 w-5 h-12 items-center justify-center bg-white border border-neutral-200 border-l-0 rounded-r text-neutral-500 hover:bg-neutral-50 hover:text-neutral-900 transition-colors cursor-pointer" + style="font-family:'Courier New',Courier,monospace;font-size:14px;line-height:1;" + > + <span id="toggle-col-left-icon">‹</span> + </button> + <!-- Toggle droite : pose sur le bord droit de la colonne centrale --> + <button + type="button" + id="toggle-col-right" + aria-label="Replier/deplier colonne droite" + class="hidden md:flex absolute right-0 top-1/2 -translate-y-1/2 z-20 w-5 h-12 items-center justify-center bg-white border border-neutral-200 border-r-0 rounded-l text-neutral-500 hover:bg-neutral-50 hover:text-neutral-900 transition-colors cursor-pointer" + style="font-family:'Courier New',Courier,monospace;font-size:14px;line-height:1;" + > + <span id="toggle-col-right-icon">›</span> + </button> + </main> + <aside id="col-right-aside" class="border-l border-neutral-200 overflow-y-auto h-full"><ColInsta /></aside> </div> <!-- Mobile : SwipeContainer Vue island - tabbar 44px reserve dans la zone flex-1 --> @@ -31,4 +58,52 @@ import PopupOnboarding from '../components/astro/PopupOnboarding.astro'; <ColInsta slot="right" /> </SwipeContainer> </div> + + <script> + // V1.3-F : toggle replier/deplier colonnes laterales desktop. + // Etat persiste en sessionStorage. Quand replie : col = 0px, aside cache, ColCentre s'etend. + const KEY_LEFT = 'tf-col-left-collapsed'; + const KEY_RIGHT = 'tf-col-right-collapsed'; + + const grid = document.getElementById('desktop-grid') as HTMLElement | null; + const asideLeft = document.getElementById('col-left-aside') as HTMLElement | null; + const asideRight = document.getElementById('col-right-aside') as HTMLElement | null; + const btnLeft = document.getElementById('toggle-col-left') as HTMLButtonElement | null; + const btnRight = document.getElementById('toggle-col-right') as HTMLButtonElement | null; + const iconLeft = document.getElementById('toggle-col-left-icon'); + const iconRight = document.getElementById('toggle-col-right-icon'); + + let collapsedLeft = false; + let collapsedRight = false; + + try { + collapsedLeft = sessionStorage.getItem(KEY_LEFT) === '1'; + collapsedRight = sessionStorage.getItem(KEY_RIGHT) === '1'; + } catch { /* mode prive */ } + + const apply = () => { + if (!grid) return; + grid.style.setProperty('--col-left', collapsedLeft ? '0px' : '320px'); + grid.style.setProperty('--col-right', collapsedRight ? '0px' : '320px'); + if (asideLeft) asideLeft.style.display = collapsedLeft ? 'none' : ''; + if (asideRight) asideRight.style.display = collapsedRight ? 'none' : ''; + // Icones : pointe vers la direction d'ouverture (sens depliage). + if (iconLeft) iconLeft.innerHTML = collapsedLeft ? '›' : '‹'; + if (iconRight) iconRight.innerHTML = collapsedRight ? '‹' : '›'; + }; + + apply(); + + btnLeft?.addEventListener('click', () => { + collapsedLeft = !collapsedLeft; + try { sessionStorage.setItem(KEY_LEFT, collapsedLeft ? '1' : '0'); } catch { /* mode prive */ } + apply(); + }); + + btnRight?.addEventListener('click', () => { + collapsedRight = !collapsedRight; + try { sessionStorage.setItem(KEY_RIGHT, collapsedRight ? '1' : '0'); } catch { /* mode prive */ } + apply(); + }); + </script> </BaseLayout> diff --git a/src/pages/manifeste.astro b/src/pages/manifeste.astro index 2ef5816..5fc14cd 100644 --- a/src/pages/manifeste.astro +++ b/src/pages/manifeste.astro @@ -24,14 +24,7 @@ import HamburgerMenu from '../components/astro/HamburgerMenu.astro'; <p class="mt-4 text-neutral-600 text-base md:text-lg leading-relaxed"> Un commun vivant pour bifurquer ; ensemble, lentement, par accumulation de petits gestes situes. </p> - <div class="mt-8"> - <a - href="/manifeste/commander" - class="inline-block px-5 py-2.5 border border-neutral-900 text-neutral-900 rounded-lg font-medium hover:bg-neutral-900 hover:text-white transition-colors" - > - Commander la version imprimee - </a> - </div> + <!-- V1.3-F : CTA "commander" deplace tout en bas de page (apres avoir lu). --> </header> <!-- Corps du manifeste --> @@ -203,6 +196,20 @@ import HamburgerMenu from '../components/astro/HamburgerMenu.astro'; </div> </section> + <!-- V1.3-F : CTA commander version papier — bord rouge brique #B5443A, signal politique fort. + Place en bas : le lecteur a lu, il sait ce qu'il achete. Tranquille. --> + <section class="mt-16 pt-10 border-t border-neutral-200 text-center"> + <p class="text-sm italic text-neutral-500 mb-4"> + Manifeste lu. J'achete la version papier, tranquille. + </p> + <a + href="/manifeste/commander" + class="inline-block px-6 py-3 border-2 border-[#B5443A] text-[#0F172A] bg-[#FAFAF7] font-medium text-base hover:bg-[#E5C3BE] transition-colors" + > + Commander la version papier + </a> + </section> + <!-- En lire plus --> <footer class="mt-16 pt-10 border-t border-neutral-200 text-center"> <p class="text-neutral-600 mb-4">En lire plus</p> From bc7e39491385829958fd6e6f0ad88df901b35479 Mon Sep 17 00:00:00 2001 From: Jules Neny <jules@trans-former.fr> Date: Tue, 12 May 2026 00:28:52 +0200 Subject: [PATCH 30/36] fix(v14-abf): toggle cols cacher seulement bidirectionnel + revert pastels transparent + footer align --- src/components/astro/ColJournal.astro | 21 ++++++++++---------- src/components/astro/Footer.astro | 22 +++++++++++---------- src/pages/index.astro | 28 ++++++++++++++++----------- 3 files changed, 39 insertions(+), 32 deletions(-) diff --git a/src/components/astro/ColJournal.astro b/src/components/astro/ColJournal.astro index 6fe73ff..e8e1126 100644 --- a/src/components/astro/ColJournal.astro +++ b/src/components/astro/ColJournal.astro @@ -1,8 +1,8 @@ --- import JournalList from '../vue/JournalList.vue'; -// V1.3-F : palette desopacifiee — chaque categorie a une pastel pour le fill pill inactive. -// Regle bicouche : pastel = FILL pill inactive uniquement. Border + texte = encre couleur 100%. +// V1.4-B : pill inactive en transparent (revert pastels V1.3-F). +// Active = fill couleur 100% + texte blanc. Inactive = transparent + border + texte couleur 100%. const categories = [ { id: 'politique', @@ -78,14 +78,14 @@ const categories = [ ))} </div> - <!-- Selecteur plateforme Politique (V1.3-F : pastel #E5C3BE en fill inactive) --> + <!-- Selecteur plateforme Politique (V1.4-B : transparent en fill inactive) --> <div id="politique-selector" class="mt-2 hidden flex gap-2"> {categories[0].plateformes.map((p) => ( <button type="button" data-platform-id={p.id} class="platform-pill" - style="font-family:'Courier New',Courier,monospace;font-size:12px;padding:2px 8px;border-radius:12px;cursor:pointer;border:1px solid #B5443A;background:#E5C3BE;color:#B5443A;" + style="font-family:'Courier New',Courier,monospace;font-size:12px;padding:2px 8px;border-radius:12px;cursor:pointer;border:1px solid #B5443A;background:transparent;color:#B5443A;" > {p.label} </button> @@ -210,18 +210,17 @@ const categories = [ window.dispatchEvent(new CustomEvent('platform-filter-change', { detail: { platform } })); }; - // V1.3-F : palette desopacifiee. + // V1.4-B : revert pastels — pill inactive en transparent (papier visible). // Active = fill couleur 100% + texte blanc. - // Inactive = fill pastel + texte couleur 100% + border couleur 100% (regle bicouche). + // Inactive = transparent + texte couleur 100% + border couleur 100%. const updateBadgeStyle = (btn: HTMLElement, active: boolean) => { const color = btn.dataset.color || '#000'; - const pastel = btn.dataset.pastel || '#E5C3BE'; if (active) { btn.style.background = color; btn.style.color = '#fff'; btn.style.border = `1px solid ${color}`; } else { - btn.style.background = pastel; + btn.style.background = 'transparent'; btn.style.color = color; btn.style.border = `1px solid ${color}`; } @@ -239,9 +238,9 @@ const categories = [ } }; - // V1.3-F : sub-pills Politique, regle bicouche identique. + // V1.4-B : sub-pills Politique, transparent au lieu de pastel. // Active (ou aucun filtre) = fill brique #B5443A + texte blanc. - // Inactive = fill pastel #E5C3BE + texte brique + border brique 100%. + // Inactive = transparent + texte brique + border brique 100%. const updatePillStyles = () => { const pills = document.querySelectorAll<HTMLElement>('.platform-pill'); const active = platformFilters['politique']; @@ -251,7 +250,7 @@ const categories = [ pill.style.background = '#B5443A'; pill.style.color = '#fff'; } else { - pill.style.background = '#E5C3BE'; + pill.style.background = 'transparent'; pill.style.color = '#B5443A'; } }); diff --git a/src/components/astro/Footer.astro b/src/components/astro/Footer.astro index a1e7a8a..e4fb521 100644 --- a/src/components/astro/Footer.astro +++ b/src/components/astro/Footer.astro @@ -1,30 +1,32 @@ --- -// Footer.astro - V1.2-L : 1 ligne 3 zones (nav / subscribe / logos RS) -// Style monochrome encre #0F172A palette terre V1.2 +// Footer.astro - V1.4-F : 1 ligne 3 zones (nav / subscribe / logos RS). +// Alignement vertical centre + padding-y reduit (py-2) pour profil plus fin. +// Subscribe input/button compact (py-1) cale sur baseline des autres elements. --- -<footer class="border-t border-neutral-200 px-6 py-4 bg-[#FAFAF7] text-[#0F172A]"> - <div class="flex flex-col md:flex-row md:items-center md:justify-between gap-3 md:gap-6"> +<footer class="border-t border-neutral-200 px-6 py-2 bg-[#FAFAF7] text-[#0F172A]"> + <div class="flex flex-col md:flex-row md:items-center md:justify-between gap-2 md:gap-6"> <!-- ZONE GAUCHE : liens nav --> - <nav class="flex gap-4 text-xs justify-center md:justify-start"> + <nav class="flex gap-4 text-xs items-center justify-center md:justify-start"> <a href="/manifeste" data-manifeste-link class="opacity-60 hover:opacity-100 transition-opacity">Manifeste</a> <a href="/a-propos" class="opacity-60 hover:opacity-100 transition-opacity">A propos</a> <a href="/mentions-legales" class="opacity-60 hover:opacity-100 transition-opacity">Mentions legales</a> </nav> - <!-- ZONE CENTRE : subscribe form compact (endpoint /api/subscribe V1.1-I) --> - <div class="flex flex-col items-center gap-1"> + <!-- ZONE CENTRE : subscribe form compact (endpoint /api/subscribe V1.1-I). + V1.4-F : padding-y reduit (py-1) pour caler input sur la baseline des liens + logos. --> + <div class="flex flex-col items-center gap-0.5"> <form id="subscribe-form" class="flex gap-2 items-center"> <input type="email" name="email" required placeholder="ton@email.fr" - class="px-3 py-1.5 border border-neutral-300 rounded text-xs focus:outline-none focus:border-[#0F172A] bg-white" + class="px-3 py-1 border border-neutral-300 rounded text-xs focus:outline-none focus:border-[#0F172A] bg-white leading-tight" /> <button type="submit" - class="px-3 py-1.5 bg-[#0F172A] text-white rounded text-xs hover:bg-[#475569] transition-colors whitespace-nowrap" + class="px-3 py-1 bg-[#0F172A] text-white rounded text-xs hover:bg-[#475569] transition-colors whitespace-nowrap leading-tight" > s'abonner </button> @@ -33,7 +35,7 @@ </div> <!-- ZONE DROITE : logos RS cliquables (SVG inline, fill #0F172A 60%) --> - <!-- V1.3-F : gap-4 mobile / gap-6 desktop pour espacer les logos --> + <!-- V1.3-F : gap-4 mobile / gap-6 desktop pour espacer les logos. V1.4-F : items-center force baseline. --> <div class="flex gap-4 md:gap-6 items-center justify-center md:justify-end text-[#0F172A]"> <!-- Instagram --> <a diff --git a/src/pages/index.astro b/src/pages/index.astro index 864d9d3..1940cb9 100644 --- a/src/pages/index.astro +++ b/src/pages/index.astro @@ -15,39 +15,43 @@ import PopupOnboarding from '../components/astro/PopupOnboarding.astro'; <MobileTabBar /> <PopupOnboarding /> - <!-- Desktop : grid 3 colonnes V1.3-F avec toggle replier latéraux. - Layout dynamique pilote par CSS vars --col-left / --col-right (320px par defaut, 0 quand replie). - Header et footer fixes (flex-shrink-0), pas de calc(100vh - X). --> + <!-- Desktop : grid 3 colonnes V1.4-A avec toggle CACHER latéraux (bidirectionnel). + Layout dynamique pilote par CSS vars --col-left / --col-right (320px visible, 0 cache). + Aside cache via display:none ET width 0 → centre s'elargit pour occuper la place. + Re-clic = bidirectionnel : col reapparait, centre se retracte. sessionStorage persist. + Pas de full-screen lateral : juste cacher pour focus simple. --> <div id="desktop-grid" class="hidden md:grid h-full overflow-hidden relative" style="grid-template-columns: var(--col-left, 320px) 1fr var(--col-right, 320px);" > - <aside id="col-left-aside" class="border-r border-neutral-200 overflow-y-auto h-full"><ColJournal /></aside> + <aside id="col-left-aside" class="border-r border-neutral-200 overflow-hidden h-full"><div class="h-full overflow-y-auto"><ColJournal /></div></aside> <main class="overflow-hidden h-full relative"> <ColCentre /> - <!-- Toggle gauche : pose sur le bord gauche de la colonne centrale --> + <!-- Toggle gauche : pose sur le bord gauche de la colonne centrale. + Icone < quand col visible (clic = cacher) ; > quand col cachee (clic = rouvrir). --> <button type="button" id="toggle-col-left" - aria-label="Replier/deplier colonne gauche" + aria-label="Cacher/afficher colonne gauche" class="hidden md:flex absolute left-0 top-1/2 -translate-y-1/2 z-20 w-5 h-12 items-center justify-center bg-white border border-neutral-200 border-l-0 rounded-r text-neutral-500 hover:bg-neutral-50 hover:text-neutral-900 transition-colors cursor-pointer" style="font-family:'Courier New',Courier,monospace;font-size:14px;line-height:1;" > <span id="toggle-col-left-icon">‹</span> </button> - <!-- Toggle droite : pose sur le bord droit de la colonne centrale --> + <!-- Toggle droite : pose sur le bord droit de la colonne centrale. + Icone > quand col visible (clic = cacher) ; < quand col cachee (clic = rouvrir). --> <button type="button" id="toggle-col-right" - aria-label="Replier/deplier colonne droite" + aria-label="Cacher/afficher colonne droite" class="hidden md:flex absolute right-0 top-1/2 -translate-y-1/2 z-20 w-5 h-12 items-center justify-center bg-white border border-neutral-200 border-r-0 rounded-l text-neutral-500 hover:bg-neutral-50 hover:text-neutral-900 transition-colors cursor-pointer" style="font-family:'Courier New',Courier,monospace;font-size:14px;line-height:1;" > <span id="toggle-col-right-icon">›</span> </button> </main> - <aside id="col-right-aside" class="border-l border-neutral-200 overflow-y-auto h-full"><ColInsta /></aside> + <aside id="col-right-aside" class="border-l border-neutral-200 overflow-hidden h-full"><div class="h-full overflow-y-auto"><ColInsta /></div></aside> </div> <!-- Mobile : SwipeContainer Vue island - tabbar 44px reserve dans la zone flex-1 --> @@ -60,8 +64,10 @@ import PopupOnboarding from '../components/astro/PopupOnboarding.astro'; </div> <script> - // V1.3-F : toggle replier/deplier colonnes laterales desktop. - // Etat persiste en sessionStorage. Quand replie : col = 0px, aside cache, ColCentre s'etend. + // V1.4-A : toggle CACHER colonnes laterales (bidirectionnel, focus simple). + // - Clic bouton gauche / droite -> cache la col concernee (width 0 + display:none). + // - Re-clic -> re-affiche. ColCentre s'elargit ou se retracte selon le grid recalcul. + // - Etat persiste en sessionStorage (survit aux refresh dans la meme tab). const KEY_LEFT = 'tf-col-left-collapsed'; const KEY_RIGHT = 'tf-col-right-collapsed'; From 5a628fd85bd75ba82785974dea7d08ff451bdf44 Mon Sep 17 00:00:00 2001 From: Jules Neny <jules@trans-former.fr> Date: Tue, 12 May 2026 00:30:42 +0200 Subject: [PATCH 31/36] fix(v14-cde): Carte O cadre + bouton replier desktop + drag mouseup fix + manifeste preview event + transitions - C1 : cadre 1px #CBD5E1 + radius 6px autour section centre-haut (Carte O zone). Force tuning CarteO.vue : forceX/Y strength 0.05 -> 0.08, collide radius +12 -> +14 pour mieux contenir les nodes dans le cadre visible. - C2 : bouton toggle Carte O desktop (icone triangle dans le bandeau, a cote de la legende). Replie a flex 0 0 36px (header reste visible, body masque avec opacity 0). Persistance sessionStorage 'tf-carte-o-collapsed'. - D : fix drag handle qui ne se decliquait pas au mouseup. Listeners poses sur window (vs document) + ajout pointerup/mouseleave/blur/mouseenter-buttons-0 pour couvrir tous les flux utilisateur (sortie iframe, perte focus, relache hors-page). Suspension transitions CSS pendant le drag (pas de lag). - E1 : PreviewArticle hydration client:load (vs client:visible) - le v-if rendait l'IntersectionObserver aveugle, donc les listeners 'preview-open' n'etaient jamais installes. Resultat : clic manifeste -> preview ne s'ouvrait pas. - E2 : Transition Vue preview-fade (opacity + translateY 8px, 250ms ease) + transitions CSS flex-basis 0.3s ease sur sections haut/bas pour smooth UX. --- public/data/carte-o.json | 2 +- src/components/astro/ColCentre.astro | 179 +++++++++++++++++++------- src/components/vue/CarteO.vue | 13 +- src/components/vue/PreviewArticle.vue | 18 +++ 4 files changed, 157 insertions(+), 55 deletions(-) diff --git a/public/data/carte-o.json b/public/data/carte-o.json index 3aa3196..6b38838 100644 --- a/public/data/carte-o.json +++ b/public/data/carte-o.json @@ -1,6 +1,6 @@ { "version": "1.1", - "generatedAt": "2026-05-11T18:07:36.883Z", + "generatedAt": "2026-05-11T22:29:46.546Z", "nodes": [ { "id": "contrat-social-medecine-corps-social", diff --git a/src/components/astro/ColCentre.astro b/src/components/astro/ColCentre.astro index de8a75f..f2ffcea 100644 --- a/src/components/astro/ColCentre.astro +++ b/src/components/astro/ColCentre.astro @@ -3,6 +3,7 @@ // MILIEU : preview article (V1.2-P) - inseree au clic journal-item-click. // BAS : iframe carte AEP (toujours visible). // V1.3-D : ChatbotV2 retire du DOM (backlog V2). Pour reactivation -> reintroduire le tab + panel. +// V1.4-C : cadre Carte O + bouton toggle desktop + drag fix (window listeners) + preview hydration (client:load) + transitions. import CarteOWrapper from '../vue/CarteOWrapper.vue'; import EmbedDynamique from '../vue/EmbedDynamique.vue'; import PreviewArticle from '../vue/PreviewArticle.vue'; @@ -19,11 +20,13 @@ import PreviewArticle from '../vue/PreviewArticle.vue'; data-preview-open="false" style="height: 100%; overflow-y: hidden;" > - <!-- HAUT (default flex-1 base 33%) : V1.3-D bandeau "Sommaire editorial" + legende + Carte O plein espace --> + <!-- HAUT (default flex-1 base 33%) : V1.3-D bandeau "Sommaire editorial" + legende + Carte O plein espace + V1.4-C1 : cadre 1px renforce (border-neutral-300) + transition height pour toggle collapse desktop --> <section id="col-centre-haut" - class="border border-neutral-200 rounded flex flex-col overflow-hidden bg-white" - style="min-height: 0; flex: 1 1 33%;" + class="rounded flex flex-col overflow-hidden bg-white" + style="min-height: 0; flex: 1 1 33%; border: 1px solid #CBD5E1; border-radius: 6px; transition: flex-basis 0.3s ease, min-height 0.3s ease;" + data-collapsed="false" > <!-- V1.3-D : bandeau header (titre gauche + legende droite) --> <header @@ -33,32 +36,45 @@ import PreviewArticle from '../vue/PreviewArticle.vue'; <span class="text-xs truncate" style="color: #475569;"> Sommaire éditorial d'architecture d'écologie politique </span> - <ul class="flex items-center gap-3 shrink-0 text-xs" style="color: #475569;" aria-label="Légende"> - <li class="flex items-center gap-1.5"> - <span - aria-hidden="true" - style="width: 8px; height: 8px; border-radius: 999px; background: #0F172A; display: inline-block;" - ></span> - <span>publié</span> - </li> - <li class="flex items-center gap-1.5"> - <span - aria-hidden="true" - style="width: 8px; height: 8px; border-radius: 999px; background: transparent; border: 1px solid #0F172A; display: inline-block;" - ></span> - <span>à venir</span> - </li> - <li class="flex items-center gap-1.5"> - <span - aria-hidden="true" - style="width: 8px; height: 8px; border-radius: 999px; background: #B45309; display: inline-block;" - ></span> - <span>projet</span> - </li> - </ul> + <div class="flex items-center gap-3 shrink-0"> + <ul class="flex items-center gap-3 shrink-0 text-xs" style="color: #475569;" aria-label="Légende"> + <li class="flex items-center gap-1.5"> + <span + aria-hidden="true" + style="width: 8px; height: 8px; border-radius: 999px; background: #0F172A; display: inline-block;" + ></span> + <span>publié</span> + </li> + <li class="flex items-center gap-1.5"> + <span + aria-hidden="true" + style="width: 8px; height: 8px; border-radius: 999px; background: transparent; border: 1px solid #0F172A; display: inline-block;" + ></span> + <span>à venir</span> + </li> + <li class="flex items-center gap-1.5"> + <span + aria-hidden="true" + style="width: 8px; height: 8px; border-radius: 999px; background: #B45309; display: inline-block;" + ></span> + <span>projet</span> + </li> + </ul> + <!-- V1.4-C2 : bouton toggle Carte O desktop (replier/deployer la zone HAUT) --> + <button + id="col-centre-haut-toggle-desktop" + type="button" + aria-label="Replier la Carte O" + class="hidden md:inline-flex items-center justify-center w-5 h-5 text-neutral-500 hover:text-neutral-900 cursor-pointer transition-colors" + style="font-size: 10px; line-height: 1;" + title="Replier la Carte O" + > + <span id="col-centre-haut-toggle-icon">▼</span> + </button> + </div> </header> - <div class="flex-1 overflow-hidden relative"> + <div id="col-centre-haut-body" class="flex-1 overflow-hidden relative" style="transition: opacity 0.25s ease;"> <div id="panel-mindmap" class="absolute inset-0"> <CarteOWrapper client:visible /> </div> @@ -86,16 +102,19 @@ import PreviewArticle from '../vue/PreviewArticle.vue'; <!-- MILIEU (V1.2-P) : preview article inseree entre Carte O et iframe AEP. Pas de border ici - PreviewArticle.vue gere son propre conteneur. - shrink-0 pour preserver sa taille auto, sinon flex pourrait l'ecraser. --> + shrink-0 pour preserver sa taille auto, sinon flex pourrait l'ecraser. + V1.4-E1 : client:load (vs client:visible) pour garantir hydration immediate des listeners + 'preview-open' / 'journal-item-click' (le v-if rendait l'observer aveugle). --> <div id="col-centre-preview-slot" class="shrink-0" style="display: contents;"> - <PreviewArticle client:visible /> + <PreviewArticle client:load /> </div> - <!-- BAS (default flex-1 base 67%) : iframe carte AEP toujours visible --> + <!-- BAS (default flex-1 base 67%) : iframe carte AEP toujours visible + V1.4-E2 : transition flex-basis 0.3s ease (smooth quand preview ouvre/ferme) --> <section id="col-centre-bas" class="border border-neutral-200 rounded overflow-hidden bg-white" - style="min-height: 0; flex: 1 1 67%;" + style="min-height: 0; flex: 1 1 67%; transition: flex-basis 0.3s ease;" > <div class="h-full min-h-[60vh] md:min-h-[400px]"> <EmbedDynamique client:visible /> @@ -104,11 +123,14 @@ import PreviewArticle from '../vue/PreviewArticle.vue'; </div> <script> - // Poignee repli zone HAUT (mobile only) + // Poignee repli zone HAUT (mobile only) + toggle desktop V1.4-C2 const grid = document.getElementById('col-centre-grid'); const haut = document.getElementById('col-centre-haut'); const bas = document.getElementById('col-centre-bas'); const poignee = document.getElementById('col-centre-poignee'); + const toggleDesktop = document.getElementById('col-centre-haut-toggle-desktop'); + const toggleDesktopIcon = document.getElementById('col-centre-haut-toggle-icon'); + const hautBody = document.getElementById('col-centre-haut-body'); // Sauvegarde flex-basis defaults pour restaure apres fermeture preview let defaultHautFlex = '1 1 33%'; @@ -140,6 +162,44 @@ import PreviewArticle from '../vue/PreviewArticle.vue'; applyRepliState(next); }); + // V1.4-C2 : bouton toggle Carte O desktop (replier/deployer la zone HAUT). + // Etat distinct de l'etat mobile (clef differente) pour permettre persistance par device. + // Replie : flex 0 0 36px (juste le bandeau header reste visible), body masque. + // Deploye : restaure flex-basis precedent (default 33% ou drag-resize sauvegarde). + const applyDesktopCollapseState = (collapsed: boolean) => { + if (!haut) return; + if (grid && grid.dataset.previewOpen === 'true') return; // skip si preview ouverte (preview pilote la mise en page) + if (collapsed) { + haut.style.flex = '0 0 36px'; + haut.dataset.collapsed = 'true'; + if (hautBody) hautBody.style.opacity = '0'; + if (toggleDesktopIcon) toggleDesktopIcon.textContent = '▶'; + toggleDesktop?.setAttribute('aria-label', 'Deployer la Carte O'); + toggleDesktop?.setAttribute('title', 'Deployer la Carte O'); + } else { + // Restaure flex-basis precedent (drag-resize ou default) + haut.style.flex = defaultHautFlex; + haut.dataset.collapsed = 'false'; + if (hautBody) hautBody.style.opacity = '1'; + if (toggleDesktopIcon) toggleDesktopIcon.textContent = '▼'; + toggleDesktop?.setAttribute('aria-label', 'Replier la Carte O'); + toggleDesktop?.setAttribute('title', 'Replier la Carte O'); + } + }; + + const savedDesktopCollapsed = sessionStorage.getItem('tf-carte-o-collapsed') === 'true'; + if (savedDesktopCollapsed) { + // applique apres que defaultHautFlex soit defini (deja le cas) + applyDesktopCollapseState(true); + } + + toggleDesktop?.addEventListener('click', () => { + const current = sessionStorage.getItem('tf-carte-o-collapsed') === 'true'; + const next = !current; + sessionStorage.setItem('tf-carte-o-collapsed', String(next)); + applyDesktopCollapseState(next); + }); + // V1.2-P : preview ouverte = container scrollable, Carte O et iframe AEP figes en vh. const applyPreviewState = (open: boolean) => { if (!grid || !haut || !bas) return; @@ -196,19 +256,27 @@ import PreviewArticle from '../vue/PreviewArticle.vue'; let startHautH = 0; let containerH = 0; + // V1.4-D : fix drag handle qui ne se "decliquait" pas au mouseup. + // - Listeners poses sur window (vs document) pour capter les events meme si la souris quitte l'iframe + // - Ajout mouseleave/blur/pointerup pour robustesse cross-browser / cross-flow utilisateur + // - Desactive aussi les transitions CSS pendant le drag (pas de lag visuel) puis restaure dragHandle.addEventListener('mousedown', (e: MouseEvent) => { if (grid.dataset.previewOpen === 'true') return; if (sessionStorage.getItem('tf-haut-replie') === 'true') return; + if (sessionStorage.getItem('tf-carte-o-collapsed') === 'true') return; isDragging = true; startY = e.clientY; startHautH = haut.getBoundingClientRect().height; containerH = grid.getBoundingClientRect().height; document.body.style.cursor = 'row-resize'; document.body.style.userSelect = 'none'; + // Suspend transitions pendant drag pour responsivite + haut.style.transition = 'none'; + bas.style.transition = 'none'; e.preventDefault(); }); - document.addEventListener('mousemove', (e: MouseEvent) => { + const onMouseMove = (e: MouseEvent) => { if (!isDragging) return; const delta = e.clientY - startY; const newHautH = Math.min(Math.max(startHautH + delta, containerH * 0.2), containerH * 0.8); @@ -216,24 +284,37 @@ import PreviewArticle from '../vue/PreviewArticle.vue'; const basPct = 100 - hautPct; haut.style.flex = `1 1 ${hautPct.toFixed(1)}%`; bas.style.flex = `1 1 ${basPct.toFixed(1)}%`; - }); + }; - document.addEventListener('mouseup', () => { - if (isDragging) { - isDragging = false; - document.body.style.cursor = ''; - document.body.style.userSelect = ''; - const hf = haut.style.flex; - const bf = bas.style.flex; - if (hf) { - sessionStorage.setItem('tf-centre-haut-flex', hf); - defaultHautFlex = hf; - } - if (bf) { - sessionStorage.setItem('tf-centre-bas-flex', bf); - defaultBasFlex = bf; - } + const stopDrag = () => { + if (!isDragging) return; + isDragging = false; + document.body.style.cursor = ''; + document.body.style.userSelect = ''; + // Restaure transitions + haut.style.transition = 'flex-basis 0.3s ease, min-height 0.3s ease'; + bas.style.transition = 'flex-basis 0.3s ease'; + const hf = haut.style.flex; + const bf = bas.style.flex; + if (hf) { + sessionStorage.setItem('tf-centre-haut-flex', hf); + defaultHautFlex = hf; } + if (bf) { + sessionStorage.setItem('tf-centre-bas-flex', bf); + defaultBasFlex = bf; + } + }; + + // Listeners multiples sur window pour couvrir tous les cas (sortie iframe, perte focus, etc.) + window.addEventListener('mousemove', onMouseMove); + window.addEventListener('mouseup', stopDrag); + window.addEventListener('pointerup', stopDrag); + window.addEventListener('mouseleave', stopDrag); + window.addEventListener('blur', stopDrag); + // Si on rentre dans la fenetre sans bouton souris enfonce, on stoppe (l'user a relache hors-page) + window.addEventListener('mouseenter', (e: MouseEvent) => { + if (isDragging && e.buttons === 0) stopDrag(); }); // Restaurer position depuis sessionStorage diff --git a/src/components/vue/CarteO.vue b/src/components/vue/CarteO.vue index 794cd56..a17bf15 100644 --- a/src/components/vue/CarteO.vue +++ b/src/components/vue/CarteO.vue @@ -404,12 +404,14 @@ function render() { return -220 })) .force('center', d3.forceCenter(width.value / 2, height.value / 2)) - .force('x', d3.forceX<SimNode>(width.value / 2).strength(0.05)) - .force('y', d3.forceY<SimNode>(height.value / 2).strength(0.05)) + // V1.4-C1 : rappel horizontal/vertical renforce (0.05 -> 0.08) + collide +14 (vs +12) + // pour mieux contenir les nodes dans le cadre visible de la zone Carte O. + .force('x', d3.forceX<SimNode>(width.value / 2).strength(0.08)) + .force('y', d3.forceY<SimNode>(height.value / 2).strength(0.08)) .force('collide', d3.forceCollide<SimNode>().radius(d => { // V1.3-D : central traite comme zone large (rect 300x64 -> rayon equivalent 160) if (d.niveau === 0) return CENTRAL_COLLIDE_RADIUS - return getRadius(d) + 12 + return getRadius(d) + 14 })) .alphaDecay(0.025) .velocityDecay(0.4) @@ -476,8 +478,9 @@ onMounted(() => { } if (simulation) { simulation.force('center', d3.forceCenter(width.value / 2, height.value / 2)) - simulation.force('x', d3.forceX<SimNode>(width.value / 2).strength(0.05)) - simulation.force('y', d3.forceY<SimNode>(height.value / 2).strength(0.05)) + // V1.4-C1 : aligne avec la simulation initiale (strength 0.08) + simulation.force('x', d3.forceX<SimNode>(width.value / 2).strength(0.08)) + simulation.force('y', d3.forceY<SimNode>(height.value / 2).strength(0.08)) simulation.alpha(0.3).restart() } }) diff --git a/src/components/vue/PreviewArticle.vue b/src/components/vue/PreviewArticle.vue index 24eab0f..007ee59 100644 --- a/src/components/vue/PreviewArticle.vue +++ b/src/components/vue/PreviewArticle.vue @@ -112,6 +112,7 @@ const formatDate = (iso: string) => { </script> <template> + <Transition name="preview-fade"> <div v-if="mode" class="preview-article border border-neutral-200 rounded overflow-hidden bg-white flex flex-col"> <!-- Header : reset + label (hashtag article OU "Manifeste") --> @@ -199,4 +200,21 @@ const formatDate = (iso: string) => { </div> </template> </div> + </Transition> </template> + +<style scoped> +/* V1.4-E2 : animation legere ouverture/fermeture preview (fade + slide-up subtil) */ +.preview-fade-enter-active, +.preview-fade-leave-active { + transition: opacity 0.25s ease, transform 0.25s ease; +} +.preview-fade-enter-from { + opacity: 0; + transform: translateY(8px); +} +.preview-fade-leave-to { + opacity: 0; + transform: translateY(4px); +} +</style> From dbba7dee3af0aeb3713adfe2d913af2ca6f48807 Mon Sep 17 00:00:00 2001 From: Jules Neny <jules@trans-former.fr> Date: Tue, 12 May 2026 00:49:01 +0200 Subject: [PATCH 32/36] fix(v14-bis): drag handle overlay + toggle gauche + manifeste preview + scrollbar visible + couleurs attenuees + 2e logo insta MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit FIX 1 drag handle : overlay full-screen z-9998 capture mouseup garanti (iframe AEP capturait les events — overlay au-dessus de tout au mousedown) FIX 2 toggle gauche : minmax(0,1fr) + visibility hidden au lieu de display:none (conflit display:none + width:0 effondrait la col centre quand 2 cols togglees) FIX 3 manifeste preview : slot flex-col + astro-island display:block (display:contents masquait astro-island au flex layout, preview ne s'inserait pas) FIX 4 scrollbar visible : overflow-y:auto + scrollbar-color custom #94A3B8 (style WebKit + Firefox uniquement quand data-preview-open=true) FIX 5 couleurs attenuees : data-muted #C8867E/#8F9A78/#7B848E/#566375 + texte encre FIX 6 Insta x2 : @aep.politique + @julesneny avec aria-labels et titles distincts FIX 7 SVG Insta : path simple (camera carree + lentille) au lieu du path complexe --- public/data/carte-o.json | 2 +- src/components/astro/ColCentre.astro | 69 +++++++++++++++++++++++---- src/components/astro/ColJournal.astro | 37 ++++++++------ src/components/astro/Footer.astro | 20 +++++++- src/pages/index.astro | 28 ++++++++--- 5 files changed, 123 insertions(+), 33 deletions(-) diff --git a/public/data/carte-o.json b/public/data/carte-o.json index 6b38838..b7b8d71 100644 --- a/public/data/carte-o.json +++ b/public/data/carte-o.json @@ -1,6 +1,6 @@ { "version": "1.1", - "generatedAt": "2026-05-11T22:29:46.546Z", + "generatedAt": "2026-05-11T22:48:32.388Z", "nodes": [ { "id": "contrat-social-medecine-corps-social", diff --git a/src/components/astro/ColCentre.astro b/src/components/astro/ColCentre.astro index f2ffcea..0ecd835 100644 --- a/src/components/astro/ColCentre.astro +++ b/src/components/astro/ColCentre.astro @@ -104,8 +104,12 @@ import PreviewArticle from '../vue/PreviewArticle.vue'; Pas de border ici - PreviewArticle.vue gere son propre conteneur. shrink-0 pour preserver sa taille auto, sinon flex pourrait l'ecraser. V1.4-E1 : client:load (vs client:visible) pour garantir hydration immediate des listeners - 'preview-open' / 'journal-item-click' (le v-if rendait l'observer aveugle). --> - <div id="col-centre-preview-slot" class="shrink-0" style="display: contents;"> + 'preview-open' / 'journal-item-click' (le v-if rendait l'observer aveugle). + V1.4-bis FIX 3 : le wrapper en display:contents masquait l'astro-island au flex layout. + On passe en flex container explicite (column) + l'astro-island prend display:block. + Quand mode=null, PreviewArticle renvoie rien → le slot ne prend pas de hauteur. + Quand mode=manifeste/article, le slot grandit pour contenir la preview. --> + <div id="col-centre-preview-slot" class="shrink-0 flex flex-col"> <PreviewArticle client:load /> </div> @@ -122,6 +126,36 @@ import PreviewArticle from '../vue/PreviewArticle.vue'; </section> </div> +<style> + /* V1.4-bis FIX 3 : force l'astro-island a se comporter comme block dans le slot preview, + sinon le default inline empeche le contenu Vue (border + bg) de se rendre correctement + dans le flex column de la col centre. */ + #col-centre-preview-slot > astro-island { + display: block; + width: 100%; + } + /* V1.4-bis FIX 4 : scrollbar visible sur la col centre quand preview ouverte + (overflow:auto au lieu de natif fin qui se cache). Style WebKit + Firefox. */ + #col-centre-grid[data-preview-open="true"] { + scrollbar-width: thin; + scrollbar-color: #94A3B8 #FAFAF7; + } + #col-centre-grid[data-preview-open="true"]::-webkit-scrollbar { + width: 10px; + } + #col-centre-grid[data-preview-open="true"]::-webkit-scrollbar-track { + background: #FAFAF7; + } + #col-centre-grid[data-preview-open="true"]::-webkit-scrollbar-thumb { + background: #94A3B8; + border-radius: 5px; + border: 2px solid #FAFAF7; + } + #col-centre-grid[data-preview-open="true"]::-webkit-scrollbar-thumb:hover { + background: #64748B; + } +</style> + <script> // Poignee repli zone HAUT (mobile only) + toggle desktop V1.4-C2 const grid = document.getElementById('col-centre-grid'); @@ -256,10 +290,18 @@ import PreviewArticle from '../vue/PreviewArticle.vue'; let startHautH = 0; let containerH = 0; - // V1.4-D : fix drag handle qui ne se "decliquait" pas au mouseup. - // - Listeners poses sur window (vs document) pour capter les events meme si la souris quitte l'iframe - // - Ajout mouseleave/blur/pointerup pour robustesse cross-browser / cross-flow utilisateur - // - Desactive aussi les transitions CSS pendant le drag (pas de lag visuel) puis restaure + // V1.4-bis FIX 1 : overlay full-screen pendant drag. + // ROOT CAUSE : l'iframe AEP (EmbedDynamique) capture les pointer events des qu'on + // depasse sa zone — mouseup tombe dans l'iframe et n'arrive jamais au parent window. + // SOLUTION : creer un <div fixed inset-0 z-9998> AU MOMENT DU mousedown, qui + // intercepte tous les events (mousemove + mouseup) pendant le drag. Au mouseup, + // on retire l'overlay. Garantie : peu importe ce qu'il y a en dessous (iframe, + // Vue island, browser chrome), l'overlay capte la fin du drag. + const overlayDuringDrag = document.createElement('div'); + overlayDuringDrag.style.cssText = 'position:fixed;inset:0;z-index:9998;cursor:row-resize;display:none;background:transparent;'; + overlayDuringDrag.setAttribute('aria-hidden', 'true'); + document.body.appendChild(overlayDuringDrag); + dragHandle.addEventListener('mousedown', (e: MouseEvent) => { if (grid.dataset.previewOpen === 'true') return; if (sessionStorage.getItem('tf-haut-replie') === 'true') return; @@ -273,6 +315,8 @@ import PreviewArticle from '../vue/PreviewArticle.vue'; // Suspend transitions pendant drag pour responsivite haut.style.transition = 'none'; bas.style.transition = 'none'; + // Active l'overlay : capture tous les events tant qu'on draggue + overlayDuringDrag.style.display = 'block'; e.preventDefault(); }); @@ -289,6 +333,8 @@ import PreviewArticle from '../vue/PreviewArticle.vue'; const stopDrag = () => { if (!isDragging) return; isDragging = false; + // Retire l'overlay (rend le reste de la page de nouveau interactif) + overlayDuringDrag.style.display = 'none'; document.body.style.cursor = ''; document.body.style.userSelect = ''; // Restaure transitions @@ -306,13 +352,16 @@ import PreviewArticle from '../vue/PreviewArticle.vue'; } }; - // Listeners multiples sur window pour couvrir tous les cas (sortie iframe, perte focus, etc.) - window.addEventListener('mousemove', onMouseMove); + // V1.4-bis : ecoute mousemove + mouseup SUR L'OVERLAY (le plus haut z-index garantit capture). + overlayDuringDrag.addEventListener('mousemove', onMouseMove); + overlayDuringDrag.addEventListener('mouseup', stopDrag); + overlayDuringDrag.addEventListener('pointerup', stopDrag); + overlayDuringDrag.addEventListener('mouseleave', stopDrag); + // Backup window listeners (pour le cas tres rare ou l'overlay n'aurait pas pris le focus) window.addEventListener('mouseup', stopDrag); window.addEventListener('pointerup', stopDrag); - window.addEventListener('mouseleave', stopDrag); window.addEventListener('blur', stopDrag); - // Si on rentre dans la fenetre sans bouton souris enfonce, on stoppe (l'user a relache hors-page) + // Si la souris revient dans la fenetre sans bouton enfonce, on stoppe window.addEventListener('mouseenter', (e: MouseEvent) => { if (isDragging && e.buttons === 0) stopDrag(); }); diff --git a/src/components/astro/ColJournal.astro b/src/components/astro/ColJournal.astro index e8e1126..cf7479c 100644 --- a/src/components/astro/ColJournal.astro +++ b/src/components/astro/ColJournal.astro @@ -1,13 +1,16 @@ --- import JournalList from '../vue/JournalList.vue'; -// V1.4-B : pill inactive en transparent (revert pastels V1.3-F). -// Active = fill couleur 100% + texte blanc. Inactive = transparent + border + texte couleur 100%. +// V1.4-bis FIX 5 : couleurs pills actives attenuees (60% color + 40% papier). +// Active = fill couleur ATTENUEE (muted) + texte encre #0F172A (contraste WCAG ≥4.5:1). +// Inactive = transparent + border + texte couleur 100% (inchange). +// Mix 60/40 : moins agressif que 100% mais reste lisible (vs 30/70 pastels V1.3 trop dilues). const categories = [ { id: 'politique', label: 'Politique', color: '#B5443A', + muted: '#C8867E', pastel: '#E5C3BE', hashtags: ['#politique', '#aep-politique'], plateformes: [ @@ -21,6 +24,7 @@ const categories = [ id: 'art', label: 'Art', color: '#5B6B3A', + muted: '#8F9A78', pastel: '#CACFBE', hashtags: ['#peinture', '#art'], plateformes: [ @@ -32,6 +36,7 @@ const categories = [ id: 'outils', label: 'Outils', color: '#475569', + muted: '#7B848E', pastel: '#C4C8CC', hashtags: ['#stack', '#building-public'], plateformes: [ @@ -43,6 +48,7 @@ const categories = [ id: 'pro', label: 'Pro', color: '#0F172A', + muted: '#566375', pastel: '#C4C8CC', hashtags: ['#building-public', '#pro'], plateformes: [ @@ -68,10 +74,11 @@ const categories = [ data-category-id={cat.id} data-hashtags={cat.hashtags.join(',')} data-color={cat.color} + data-muted={cat.muted} data-pastel={cat.pastel} data-has-selector={cat.hasSelector ? 'true' : 'false'} class="category-badge" - style={`background:${cat.color};color:#fff;font-family:'Courier New',Courier,monospace;font-size:13px;padding:3px 10px;border-radius:4px;cursor:pointer;border:1px solid ${cat.color};`} + style={`background:${cat.muted};color:#0F172A;font-family:'Courier New',Courier,monospace;font-size:13px;padding:3px 10px;border-radius:4px;cursor:pointer;border:1px solid ${cat.muted};`} > {cat.label} </button> @@ -210,15 +217,17 @@ const categories = [ window.dispatchEvent(new CustomEvent('platform-filter-change', { detail: { platform } })); }; - // V1.4-B : revert pastels — pill inactive en transparent (papier visible). - // Active = fill couleur 100% + texte blanc. - // Inactive = transparent + texte couleur 100% + border couleur 100%. + // V1.4-bis FIX 5 : pill active en couleur ATTENUEE (data-muted) + texte encre #0F172A. + // Inactive : transparent + texte couleur 100% + border couleur 100% (inchange). + // Pourquoi encre #0F172A au lieu de blanc : sur fond attenue (#C8867E etc.), le ratio + // contraste blanc tombe ~3.5:1 (limite). Encre #0F172A donne ratio ≥6:1 sur tous les muted. const updateBadgeStyle = (btn: HTMLElement, active: boolean) => { const color = btn.dataset.color || '#000'; + const muted = btn.dataset.muted || color; if (active) { - btn.style.background = color; - btn.style.color = '#fff'; - btn.style.border = `1px solid ${color}`; + btn.style.background = muted; + btn.style.color = '#0F172A'; + btn.style.border = `1px solid ${muted}`; } else { btn.style.background = 'transparent'; btn.style.color = color; @@ -238,17 +247,17 @@ const categories = [ } }; - // V1.4-B : sub-pills Politique, transparent au lieu de pastel. - // Active (ou aucun filtre) = fill brique #B5443A + texte blanc. - // Inactive = transparent + texte brique + border brique 100%. + // V1.4-bis FIX 5 : sub-pills Politique, active en brique ATTENUEE #C8867E + encre. + // Active (ou aucun filtre) = fill #C8867E + texte encre #0F172A. + // Inactive = transparent + texte brique 100% + border brique 100%. const updatePillStyles = () => { const pills = document.querySelectorAll<HTMLElement>('.platform-pill'); const active = platformFilters['politique']; pills.forEach((pill) => { const pid = pill.dataset.platformId; if (!active || pid === active) { - pill.style.background = '#B5443A'; - pill.style.color = '#fff'; + pill.style.background = '#C8867E'; + pill.style.color = '#0F172A'; } else { pill.style.background = 'transparent'; pill.style.color = '#B5443A'; diff --git a/src/components/astro/Footer.astro b/src/components/astro/Footer.astro index e4fb521..ef96eb4 100644 --- a/src/components/astro/Footer.astro +++ b/src/components/astro/Footer.astro @@ -37,16 +37,32 @@ <!-- ZONE DROITE : logos RS cliquables (SVG inline, fill #0F172A 60%) --> <!-- V1.3-F : gap-4 mobile / gap-6 desktop pour espacer les logos. V1.4-F : items-center force baseline. --> <div class="flex gap-4 md:gap-6 items-center justify-center md:justify-end text-[#0F172A]"> - <!-- Instagram --> + <!-- V1.4-bis FIX 6 : Instagram x2 (AEP politique + Jules perso) avec aria-labels distincts. + V1.4-bis FIX 7 : SVG simplifie (camera carree avec coins arrondis, lentille, point flash) — rendu propre net. --> <a href="https://www.instagram.com/aep.politique/" target="_blank" rel="noopener noreferrer" aria-label="Instagram @aep.politique" + title="@aep.politique" class="opacity-60 hover:opacity-100 transition-opacity" > <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" class="w-[18px] h-[18px] md:w-5 md:h-5" fill="currentColor" aria-hidden="true"> - <path d="M12 2.163c3.204 0 3.584.012 4.85.07 1.366.062 2.633.336 3.608 1.311.975.975 1.249 2.242 1.311 3.608.058 1.266.069 1.646.069 4.85s-.012 3.584-.07 4.85c-.062 1.366-.336 2.633-1.311 3.608-.975.975-2.242 1.249-3.608 1.311-1.266.058-1.646.07-4.85.07s-3.584-.012-4.85-.07c-1.366-.062-2.633-.336-3.608-1.311-.975-.975-1.249-2.242-1.311-3.608C2.175 15.647 2.163 15.267 2.163 12s.012-3.584.07-4.85c.062-1.366.336-2.633 1.311-3.608.975-.975 2.242-1.249 3.608-1.311 1.266-.058 1.646-.07 4.85-.07zm0 1.838c-3.15 0-3.522.012-4.766.069-1.024.047-1.58.218-1.95.362-.49.19-.84.418-1.207.786-.367.367-.595.717-.786 1.207-.144.37-.315.926-.362 1.95-.057 1.244-.069 1.616-.069 4.766s.012 3.522.069 4.766c.047 1.024.218 1.58.362 1.95.19.49.418.84.786 1.207.367.367.717.595 1.207.786.37.144.926.315 1.95.362 1.244.057 1.616.069 4.766.069s3.522-.012 4.766-.069c1.024-.047 1.58-.218 1.95-.362.49-.19.84-.418 1.207-.786.367-.367.595-.717.786-1.207.144-.37.315-.926.362-1.95.057-1.244.069-1.616.069-4.766s-.012-3.522-.069-4.766c-.047-1.024-.218-1.58-.362-1.95-.19-.49-.418-.84-.786-1.207-.367-.367-.717-.595-1.207-.786-.37-.144-.926-.315-1.95-.362C15.522 4.013 15.15 4.001 12 4.001zm0 3.135a4.864 4.864 0 110 9.728 4.864 4.864 0 010-9.728zm0 8.027a3.162 3.162 0 100-6.325 3.162 3.162 0 000 6.325zm6.187-8.249a1.137 1.137 0 11-2.275 0 1.137 1.137 0 012.275 0z"/> + <path d="M7.75 2C4.574 2 2 4.574 2 7.75v8.5C2 19.426 4.574 22 7.75 22h8.5C19.426 22 22 19.426 22 16.25v-8.5C22 4.574 19.426 2 16.25 2h-8.5zM4 7.75A3.75 3.75 0 017.75 4h8.5A3.75 3.75 0 0120 7.75v8.5A3.75 3.75 0 0116.25 20h-8.5A3.75 3.75 0 014 16.25v-8.5zM12 7a5 5 0 100 10 5 5 0 000-10zm0 2a3 3 0 110 6 3 3 0 010-6zm5.5-3.5a1 1 0 100 2 1 1 0 000-2z"/> + </svg> + </a> + + <!-- Instagram @julesneny (compte perso/art) --> + <a + href="https://www.instagram.com/julesneny/" + target="_blank" + rel="noopener noreferrer" + aria-label="Instagram @julesneny" + title="@julesneny" + class="opacity-60 hover:opacity-100 transition-opacity" + > + <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" class="w-[18px] h-[18px] md:w-5 md:h-5" fill="currentColor" aria-hidden="true"> + <path d="M7.75 2C4.574 2 2 4.574 2 7.75v8.5C2 19.426 4.574 22 7.75 22h8.5C19.426 22 22 19.426 22 16.25v-8.5C22 4.574 19.426 2 16.25 2h-8.5zM4 7.75A3.75 3.75 0 017.75 4h8.5A3.75 3.75 0 0120 7.75v8.5A3.75 3.75 0 0116.25 20h-8.5A3.75 3.75 0 014 16.25v-8.5zM12 7a5 5 0 100 10 5 5 0 000-10zm0 2a3 3 0 110 6 3 3 0 010-6zm5.5-3.5a1 1 0 100 2 1 1 0 000-2z"/> </svg> </a> diff --git a/src/pages/index.astro b/src/pages/index.astro index 1940cb9..0c8dcaa 100644 --- a/src/pages/index.astro +++ b/src/pages/index.astro @@ -17,16 +17,19 @@ import PopupOnboarding from '../components/astro/PopupOnboarding.astro'; <!-- Desktop : grid 3 colonnes V1.4-A avec toggle CACHER latéraux (bidirectionnel). Layout dynamique pilote par CSS vars --col-left / --col-right (320px visible, 0 cache). - Aside cache via display:none ET width 0 → centre s'elargit pour occuper la place. Re-clic = bidirectionnel : col reapparait, centre se retracte. sessionStorage persist. - Pas de full-screen lateral : juste cacher pour focus simple. --> + V1.4-bis FIX 2 : suppression du couplage display:none + width:0 (conflit grid). + On utilise EXCLUSIVEMENT --col-left/--col-right (0px = aside disparait visuellement). + Le contenu de l'aside est cache via visibility hidden (preserve la place 0 sans bug grid). + Grid template utilise minmax(0,1fr) pour que la col centre ne s'effondre pas quand les + voisins sont a 0 (cas critique 2 togglees). --> <div id="desktop-grid" class="hidden md:grid h-full overflow-hidden relative" - style="grid-template-columns: var(--col-left, 320px) 1fr var(--col-right, 320px);" + style="grid-template-columns: var(--col-left, 320px) minmax(0, 1fr) var(--col-right, 320px);" > <aside id="col-left-aside" class="border-r border-neutral-200 overflow-hidden h-full"><div class="h-full overflow-y-auto"><ColJournal /></div></aside> - <main class="overflow-hidden h-full relative"> + <main class="overflow-hidden h-full relative" style="min-width: 0;"> <ColCentre /> <!-- Toggle gauche : pose sur le bord gauche de la colonne centrale. Icone < quand col visible (clic = cacher) ; > quand col cachee (clic = rouvrir). --> @@ -87,12 +90,25 @@ import PopupOnboarding from '../components/astro/PopupOnboarding.astro'; collapsedRight = sessionStorage.getItem(KEY_RIGHT) === '1'; } catch { /* mode prive */ } + // V1.4-bis FIX 2 : UNE SEULE technique (CSS var width 0px) — pas de display:none + // qui creait un conflit grid : quand display:none + grid-column 0 cohabitent, + // certains navigateurs ne recalculent pas la col centre correctement → centre disparu. + // Maintenant : aside reste dans le flux avec width 0 (overflow hidden cache le contenu) + // + le contenu interne avec visibility hidden pour ne pas etre focusable au tab. const apply = () => { if (!grid) return; grid.style.setProperty('--col-left', collapsedLeft ? '0px' : '320px'); grid.style.setProperty('--col-right', collapsedRight ? '0px' : '320px'); - if (asideLeft) asideLeft.style.display = collapsedLeft ? 'none' : ''; - if (asideRight) asideRight.style.display = collapsedRight ? 'none' : ''; + // Aside reste en flux (display: '' par defaut) pour ne pas casser le grid template. + // Visibility hidden + tabindex -1 sur le contenu pour empecher interaction inutile. + if (asideLeft) { + asideLeft.style.visibility = collapsedLeft ? 'hidden' : ''; + asideLeft.setAttribute('aria-hidden', collapsedLeft ? 'true' : 'false'); + } + if (asideRight) { + asideRight.style.visibility = collapsedRight ? 'hidden' : ''; + asideRight.setAttribute('aria-hidden', collapsedRight ? 'true' : 'false'); + } // Icones : pointe vers la direction d'ouverture (sens depliage). if (iconLeft) iconLeft.innerHTML = collapsedLeft ? '›' : '‹'; if (iconRight) iconRight.innerHTML = collapsedRight ? '‹' : '›'; From f09946363694720921dd73f9c482a65d89af9104 Mon Sep 17 00:00:00 2001 From: Jules Neny <jules@trans-former.fr> Date: Tue, 12 May 2026 11:29:02 +0200 Subject: [PATCH 33/36] feat(seo): sitemap + robots.txt + meta OG/Twitter/canonical + site URL - Installe @astrojs/sitemap ; integre dans astro.config.mjs avec filtre /api/ - Ajoute site: 'https://trans-former.fr' pour canonical absolues - BaseLayout : props ogImage + canonical + isArticle + articleDate ; meta description/canonical/robots/OG/Twitter Card complets ; suppression doublons - manifeste.astro : passe isArticle=true pour Schema.org Article - public/robots.txt : open index + GPTBot/ClaudeBot/Google-Extended/Applebot-Extended/PerplexityBot explicites --- astro.config.mjs | 10 +++- package-lock.json | 67 ++++++++++++++++++++++++++ package.json | 1 + public/robots.txt | 19 ++++++++ src/layouts/BaseLayout.astro | 93 ++++++++++++++++++++++++++++++++---- src/pages/manifeste.astro | 3 ++ 6 files changed, 183 insertions(+), 10 deletions(-) create mode 100644 public/robots.txt diff --git a/astro.config.mjs b/astro.config.mjs index 6a9ed12..6e4b3b2 100644 --- a/astro.config.mjs +++ b/astro.config.mjs @@ -3,14 +3,22 @@ import { defineConfig } from 'astro/config'; import vue from '@astrojs/vue'; import node from '@astrojs/node'; import tailwindcss from '@tailwindcss/vite'; +import sitemap from '@astrojs/sitemap'; // PC7 — bascule SSR (mode 'server' Astro 6) pour endpoint /api/chatbot proxy. // Toutes les pages publiques restent statiques via `export const prerender = true`. // Coolify deploy (PC8) : `node ./dist/server/entry.mjs` (Node adapter standalone). +// PC8 — sitemap auto-genere + site URL pour canonical + redirects SEO. export default defineConfig({ + site: 'https://trans-former.fr', output: 'server', adapter: node({ mode: 'standalone' }), - integrations: [vue()], + integrations: [ + vue(), + sitemap({ + filter: (page) => !page.includes('/api/'), + }), + ], vite: { plugins: [tailwindcss()], }, diff --git a/package-lock.json b/package-lock.json index 4692d93..0a17e6d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "0.0.1", "dependencies": { "@astrojs/node": "^10.1.0", + "@astrojs/sitemap": "^3.7.2", "@astrojs/vue": "^6.0.1", "@fontsource-variable/roboto-condensed": "^5.2.8", "@tailwindcss/vite": "^4.2.4", @@ -98,6 +99,17 @@ "node": ">=22.12.0" } }, + "node_modules/@astrojs/sitemap": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/@astrojs/sitemap/-/sitemap-3.7.2.tgz", + "integrity": "sha512-PqkzkcZTb5ICiyIR8VoKbIAP/laNRXi5tw616N1Ckk+40oNB8Can1AzVV56lrbC5GKSZFCyJYUVYqVivMisvpA==", + "license": "MIT", + "dependencies": { + "sitemap": "^9.0.0", + "stream-replace-string": "^2.0.0", + "zod": "^4.3.6" + } + }, "node_modules/@astrojs/telemetry": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/@astrojs/telemetry/-/telemetry-3.3.2.tgz", @@ -2661,6 +2673,24 @@ "@types/unist": "*" } }, + "node_modules/@types/node": { + "version": "24.12.4", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.4.tgz", + "integrity": "sha512-GUUEShf+PBCGW2KaXwcIt3Yk+e3pkKwWKb9GSyM9WQVE+ep2jzmHdGsHzu4wgcZy5fN9FBdVzjpBQsYlpfpgLA==", + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/@types/sax": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/sax/-/sax-1.2.7.tgz", + "integrity": "sha512-rO73L89PJxeYM3s3pPPjiPgVVcymqU490g0YO5n5By0k2Erzj6tay/4lr1CHAAU4JyOWd1rpQ8bCf6cZfHU96A==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/unist": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", @@ -2936,6 +2966,12 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "license": "MIT" + }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -6982,6 +7018,25 @@ "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", "license": "MIT" }, + "node_modules/sitemap": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/sitemap/-/sitemap-9.0.1.tgz", + "integrity": "sha512-S6hzjGJSG3d6if0YoF5kTyeRJvia6FSTBroE5fQ0bu1QNxyJqhhinfUsXi9fH3MgtXODWvwo2BDyQSnhPQ88uQ==", + "license": "MIT", + "dependencies": { + "@types/node": "^24.9.2", + "@types/sax": "^1.2.1", + "arg": "^5.0.0", + "sax": "^1.4.1" + }, + "bin": { + "sitemap": "dist/esm/cli.js" + }, + "engines": { + "node": ">=20.19.5", + "npm": ">=10.8.2" + } + }, "node_modules/slash": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/slash/-/slash-5.1.0.tgz", @@ -7040,6 +7095,12 @@ "node": ">= 0.8" } }, + "node_modules/stream-replace-string": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/stream-replace-string/-/stream-replace-string-2.0.0.tgz", + "integrity": "sha512-TlnjJ1C0QrmxRNrON00JvaFFlNh5TTG00APw23j74ET7gkQpTASi6/L2fuiav8pzK715HXtUeClpBTw2NPSn6w==", + "license": "MIT" + }, "node_modules/stringify-entities": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", @@ -7222,6 +7283,12 @@ "integrity": "sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==", "license": "MIT" }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "license": "MIT" + }, "node_modules/unicorn-magic": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.4.0.tgz", diff --git a/package.json b/package.json index d6746a9..c0eec44 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ }, "dependencies": { "@astrojs/node": "^10.1.0", + "@astrojs/sitemap": "^3.7.2", "@astrojs/vue": "^6.0.1", "@fontsource-variable/roboto-condensed": "^5.2.8", "@tailwindcss/vite": "^4.2.4", diff --git a/public/robots.txt b/public/robots.txt new file mode 100644 index 0000000..75a958d --- /dev/null +++ b/public/robots.txt @@ -0,0 +1,19 @@ +User-agent: * +Allow: / + +User-agent: GPTBot +Allow: / + +User-agent: ClaudeBot +Allow: / + +User-agent: Google-Extended +Allow: / + +User-agent: Applebot-Extended +Allow: / + +User-agent: PerplexityBot +Allow: / + +Sitemap: https://trans-former.fr/sitemap-index.xml diff --git a/src/layouts/BaseLayout.astro b/src/layouts/BaseLayout.astro index 3f0224c..673d186 100644 --- a/src/layouts/BaseLayout.astro +++ b/src/layouts/BaseLayout.astro @@ -6,12 +6,63 @@ import SiteHeader from '../components/astro/SiteHeader.astro'; interface Props { title?: string; description?: string; + ogImage?: string; + canonical?: string; + /** Article JSON-LD : passer true sur les pages articles (manifeste, etc.) */ + isArticle?: boolean; + articleDate?: string; + articleDescription?: string; } const { - title = 'trans-former.fr', - description = "Architecture d'ecologie politique - journal, carte conceptuelle, manifeste", + title, + description, + ogImage, + canonical, + isArticle = false, + articleDate, + articleDescription, } = Astro.props; + +const SITE_NAME = 'trans-former.fr'; +const SITE_URL = 'https://trans-former.fr'; +const DEFAULT_DESC = "Architecture, ecologie, politique : un commun a construire ensemble. Manifeste et infrastructure vivante de Jules NENY."; +const DEFAULT_OG = '/og-default.png'; + +const fullTitle = title ? `${title} - ${SITE_NAME}` : SITE_NAME; +const finalDesc = description || DEFAULT_DESC; +const finalCanonical = canonical || new URL(Astro.url.pathname, SITE_URL).href; +const finalOg = ogImage || DEFAULT_OG; +const finalOgAbsolute = finalOg.startsWith('http') ? finalOg : `${SITE_URL}${finalOg}`; + +const websiteSchema = { + "@context": "https://schema.org", + "@type": "WebSite", + "name": SITE_NAME, + "url": SITE_URL, + "author": { + "@type": "Person", + "name": "Jules NENY", + "url": `${SITE_URL}/a-propos`, + "sameAs": [ + "https://www.instagram.com/julesneny/", + "https://www.instagram.com/aep.politique/", + "https://julesneny.substack.com", + "https://git.trans-former.fr/jules" + ] + }, + "description": DEFAULT_DESC +}; + +const articleSchema = isArticle ? { + "@context": "https://schema.org", + "@type": "Article", + "headline": title || SITE_NAME, + "author": { "@type": "Person", "name": "Jules NENY" }, + "datePublished": articleDate || "2026-05-01", + "publisher": { "@type": "Organization", "name": SITE_NAME }, + "description": articleDescription || finalDesc +} : null; --- <!doctype html> <html lang="fr" class="h-screen"> @@ -20,14 +71,38 @@ const { <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" /> <link rel="icon" type="image/svg+xml" href="/favicon.svg" /> <meta name="generator" content={Astro.generator} /> - <title>{title} - - - - + + + {fullTitle} + + + + + + + + + + + + + + - - + + + + + + + + +