From ec4c157226ea23ed03bc1ffb26a7f54ec2f0f282 Mon Sep 17 00:00:00 2001 From: tristan Date: Thu, 19 Feb 2026 11:18:36 +0100 Subject: [PATCH 01/38] fix: readme.md --- README.md | 55 +++++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 39 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index cc1d779..656bfc1 100644 --- a/README.md +++ b/README.md @@ -55,26 +55,34 @@ Prévisualiser le build : npm run preview ``` -### Livraison / publication du layer +### Livraison / publication du layer (CI) -Vérifier le contenu qui sera publié : +La publication est automatique via `.gitea/workflows/release.yml` sur push `main` / `master`. + +Le job CI : + +1. Installe les dépendances +2. Lance `npm run dev:prepare` +3. Lance `npm run lint` +4. Lance `semantic-release` (version automatique + publish sur Gitea Packages) + +Les versions sont calculées via Conventional Commits : + +- `fix: ...` -> patch (`1.0.0` -> `1.0.1`) +- `feat: ...` -> minor (`1.0.0` -> `1.1.0`) +- `feat!: ...` ou `BREAKING CHANGE:` -> major (`1.0.0` -> `2.0.0`) + +Secrets requis dans le repo Gitea : + +- `NPM_TOKEN` : token avec droits publish package +- `RELEASE_TOKEN` : token avec droits write repo (tags/releases) + +Commande locale utile avant push : ```bash npm pack --dry-run ``` -Publier sur le registry NPM configuré : - -```bash -npm publish -``` - -Publier explicitement sur un registry Gitea : - -```bash -npm publish --registry https:///api/packages//npm/ -``` - ## Tester un composant dans le playground Le playground étend déjà le layer via `.playground/nuxt.config.ts`. @@ -117,13 +125,28 @@ npm run dev ## Utiliser ce layer dans un autre projet Nuxt -Installer le package : +### 1) Configurer le `.npmrc` du projet consommateur + +Option simple : + +```ini +@malio:registry=https://gitea.malio.fr/api/packages/MALIO-DEV/npm/ +``` +Puis : + +```bash +export NPM_TOKEN=TON_TOKEN_GITEA +``` + +### 2) Installer le package ```bash npm install @malio/layer-ui ``` -Étendre le layer dans `nuxt.config.ts` du projet consommateur : +### 3) Étendre le layer + +Dans `nuxt.config.ts` du projet consommateur : ```ts export default defineNuxtConfig({ -- 2.39.5 From 65d9060e2626f98eeb94055dcb8077fb55d1d596 Mon Sep 17 00:00:00 2001 From: tristan Date: Mon, 23 Feb 2026 11:11:31 +0100 Subject: [PATCH 02/38] feat : ajout du template de MR + CHANGELOG.md --- .gitea/PULL_REQUEST_TEMPLATE.md | 23 +++++++++++++++++++++++ CHANGELOG.md | 12 ++++++++++++ 2 files changed, 35 insertions(+) create mode 100644 .gitea/PULL_REQUEST_TEMPLATE.md create mode 100644 CHANGELOG.md diff --git a/.gitea/PULL_REQUEST_TEMPLATE.md b/.gitea/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..aed2bc5 --- /dev/null +++ b/.gitea/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,23 @@ +--- + +name: "Merge Request" +about: "Template de MR" +title: "[#NUMERO_TICKET] TITRE TICKET" +ref: "main" + +--- + +| Numéro du ticket | Titre du ticket | +|------------------|-----------------| +| | | + +## Description de la PR + +## Modification du .env + +## Check list + +- [ ] Pas de régression +- [ ] TU/TI/TF rédigée +- [ ] TU/TI/TF OK +- [ ] CHANGELOG modifié diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..f230edc --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,12 @@ +# Changelog + +Liste des évolutions de la librairie Malio layer UI + +## [0.0.0] +### Parameters + +### Added + +### Changed + +### Fixed -- 2.39.5 From 82ecc9cfe2e90e7b7d77cb075068f2aab9970f8a Mon Sep 17 00:00:00 2001 From: tristan Date: Mon, 23 Feb 2026 11:29:16 +0100 Subject: [PATCH 03/38] feat : ajout config vitest/make/pre-commit/commit-msg + un exemple de test vitest --- .nvmrc | 1 + app/components/malio/Input.test.ts | 23 + commit-msg | 31 + makefile | 30 + package-lock.json | 1164 ++++++++++++++++++++++++++++ package.json | 7 +- pre-commit | 36 + vitest.config.ts | 10 + 8 files changed, 1301 insertions(+), 1 deletion(-) create mode 100644 .nvmrc create mode 100644 app/components/malio/Input.test.ts create mode 100644 commit-msg create mode 100644 makefile create mode 100644 pre-commit create mode 100644 vitest.config.ts diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000..32f8c50 --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +24.13.1 diff --git a/app/components/malio/Input.test.ts b/app/components/malio/Input.test.ts new file mode 100644 index 0000000..aefc9fb --- /dev/null +++ b/app/components/malio/Input.test.ts @@ -0,0 +1,23 @@ +import { describe, expect, it } from 'vitest' +import { mount } from '@vue/test-utils' +import Input from './Input.vue' + +describe('MalioInput', () => { + it('affiche la valeur initiale', () => { + const wrapper = mount(Input, { + props: { modelValue: 'hello' }, + }) + + expect(wrapper.get('input').element.value).toBe('hello') + }) + + it('emet update:modelValue au changement', async () => { + const wrapper = mount(Input, { + props: { modelValue: '' }, + }) + + await wrapper.get('input').setValue('new value') + + expect(wrapper.emitted('update:modelValue')?.[0]).toEqual(['new value']) + }) +}) diff --git a/commit-msg b/commit-msg new file mode 100644 index 0000000..901d964 --- /dev/null +++ b/commit-msg @@ -0,0 +1,31 @@ +#!/usr/bin/env bash +set -euo pipefail + +MSG_FILE="${1}" +FIRST_LINE="$(head -n 1 "$MSG_FILE" | tr -d '\r')" + +# Autoriser commits auto-générés par git +if [[ "$FIRST_LINE" =~ ^Merge\ ]]; then + exit 0 +fi + +# Types autorisés (MINUSCULES uniquement) +# Optionnel: scope => feat(auth) : ... +REGEX='^(build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test)(\([a-z0-9._-]+\))?\ :\ .+' + +if [[ ! "$FIRST_LINE" =~ $REGEX ]]; then + echo "❌ Message de commit invalide." + echo "" + echo "➡️ Format attendu : () : " + echo "➡️ Types autorisés (minuscules uniquement) :" + echo " build, chore, ci, docs, feat, fix, perf, refactor, revert, style, test" + echo "" + echo "✅ Exemples :" + echo " feat : add login page" + echo " fix(auth) : prevent null token crash" + echo " docs : update README" + echo "" + echo "❌ Exemple refusé :" + echo " Feat : add login page" + exit 1 +fi diff --git a/makefile b/makefile new file mode 100644 index 0000000..b9e3c60 --- /dev/null +++ b/makefile @@ -0,0 +1,30 @@ +.PHONY: start install dev dev-prepare lint test pre-commit copy-git-hook node-use + +start: copy-git-hook node-use install + +install: + npm install + +dev: + npm run dev + +dev-prepare: + npm run dev:prepare + +lint: dev-prepare + npm run lint + +test: + npm run test + +pre-commit: lint test + +copy-git-hook: + cp pre-commit .git/hooks/pre-commit + cp commit-msg .git/hooks/commit-msg + chmod a+x .git/hooks/pre-commit + chmod a+x .git/hooks/commit-msg + +# Force la version node +node-use: + bash -lc 'source "$$HOME/.nvm/nvm.sh" && nvm install && nvm use' diff --git a/package-lock.json b/package-lock.json index c854fd4..b6bae17 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,15 +13,26 @@ "devDependencies": { "@nuxt/eslint": "latest", "@types/node": "^24.10.13", + "@vitejs/plugin-vue": "^6.0.1", + "@vue/test-utils": "^2.4.6", "eslint": "^10.0.0", + "jsdom": "^27.0.1", "nuxt": "^4.3.1", "typescript": "^5.9.3", + "vitest": "^3.2.4", "vue": "latest" }, "peerDependencies": { "nuxt": "^4.0.0" } }, + "node_modules/@acemir/cssom": { + "version": "0.9.31", + "resolved": "https://registry.npmjs.org/@acemir/cssom/-/cssom-0.9.31.tgz", + "integrity": "sha512-ZnR3GSaH+/vJ0YlHau21FjfLYjMpYVIzTD8M8vIEQvIGxeOXyXdzCI140rrCY862p/C/BbzWsjc1dgnM9mkoTA==", + "dev": true, + "license": "MIT" + }, "node_modules/@alloc/quick-lru": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", @@ -67,6 +78,61 @@ "@types/json-schema": "^7.0.15" } }, + "node_modules/@asamuzakjp/css-color": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-4.1.2.tgz", + "integrity": "sha512-NfBUvBaYgKIuq6E/RBLY1m0IohzNHAYyaJGuTK79Z23uNwmz2jl1mPsC5ZxCCxylinKhT1Amn5oNTlx1wN8cQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@csstools/css-calc": "^3.0.0", + "@csstools/css-color-parser": "^4.0.1", + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0", + "lru-cache": "^11.2.5" + } + }, + "node_modules/@asamuzakjp/css-color/node_modules/lru-cache": { + "version": "11.2.6", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.6.tgz", + "integrity": "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@asamuzakjp/dom-selector": { + "version": "6.8.1", + "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-6.8.1.tgz", + "integrity": "sha512-MvRz1nCqW0fsy8Qz4dnLIvhOlMzqDVBabZx6lH+YywFDdjXhMY37SmpV1XFX3JzG5GWHn63j6HX6QPr3lZXHvQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/nwsapi": "^2.3.9", + "bidi-js": "^1.0.3", + "css-tree": "^3.1.0", + "is-potential-custom-element-name": "^1.0.1", + "lru-cache": "^11.2.6" + } + }, + "node_modules/@asamuzakjp/dom-selector/node_modules/lru-cache": { + "version": "11.2.6", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.6.tgz", + "integrity": "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@asamuzakjp/nwsapi": { + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz", + "integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==", + "dev": true, + "license": "MIT" + }, "node_modules/@babel/code-frame": { "version": "7.29.0", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", @@ -552,6 +618,138 @@ "node": ">=18.0.0" } }, + "node_modules/@csstools/color-helpers": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.0.2.tgz", + "integrity": "sha512-LMGQLS9EuADloEFkcTBR3BwV/CGHV7zyDxVRtVDTwdI2Ca4it0CCVTT9wCkxSgokjE5Ho41hEPgb8OEUwoXr6Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/@csstools/css-calc": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.1.1.tgz", + "integrity": "sha512-HJ26Z/vmsZQqs/o3a6bgKslXGFAungXGbinULZO3eMsOyNJHeBBZfup5FiZInOghgoM4Hwnmw+OgbJCNg1wwUQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.0.2.tgz", + "integrity": "sha512-0GEfbBLmTFf0dJlpsNU7zwxRIH0/BGEMuXLTCvFYxuL1tNhqzTbtnFICyJLTNK4a+RechKP75e7w42ClXSnJQw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^6.0.2", + "@csstools/css-calc": "^3.1.1" + }, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz", + "integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-syntax-patches-for-csstree": { + "version": "1.0.28", + "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.0.28.tgz", + "integrity": "sha512-1NRf1CUBjnr3K7hu8BLxjQrKCxEe8FP/xmPTenAxCRZWVLbmGotkFvG9mfNpjA6k7Bw1bw4BilZq9cu19RA5pg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0" + }, + "node_modules/@csstools/css-tokenizer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz", + "integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + } + }, "node_modules/@csstools/selector-resolve-nested": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/@csstools/selector-resolve-nested/-/selector-resolve-nested-3.1.0.tgz", @@ -1266,6 +1464,24 @@ "node": "^20.19.0 || ^22.13.0 || >=24" } }, + "node_modules/@exodus/bytes": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.14.1.tgz", + "integrity": "sha512-OhkBFWI6GcRMUroChZiopRiSp2iAMvEBK47NhJooDqz1RERO4QuZIZnjP63TXX8GAiLABkYmX+fuQsdJ1dd2QQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "@noble/hashes": "^1.8.0 || ^2.0.0" + }, + "peerDependenciesMeta": { + "@noble/hashes": { + "optional": true + } + } + }, "node_modules/@humanfs/core": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", @@ -2342,6 +2558,13 @@ "node": ">=18.12.0" } }, + "node_modules/@one-ini/wasm": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@one-ini/wasm/-/wasm-0.1.1.tgz", + "integrity": "sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==", + "dev": true, + "license": "MIT" + }, "node_modules/@oxc-minify/binding-android-arm-eabi": { "version": "0.112.0", "resolved": "https://registry.npmjs.org/@oxc-minify/binding-android-arm-eabi/-/binding-android-arm-eabi-0.112.0.tgz", @@ -4414,6 +4637,24 @@ "tslib": "^2.4.0" } }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/esrecurse": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", @@ -5055,6 +5296,131 @@ "vue": "^3.0.0" } }, + "node_modules/@vitest/expect": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.4.tgz", + "integrity": "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", + "chai": "^5.2.0", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.4.tgz", + "integrity": "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "3.2.4", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.17" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/mocker/node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/@vitest/pretty-format": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.4.tgz", + "integrity": "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.4.tgz", + "integrity": "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "3.2.4", + "pathe": "^2.0.3", + "strip-literal": "^3.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.4.tgz", + "integrity": "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.4", + "magic-string": "^0.30.17", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.4.tgz", + "integrity": "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^4.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.4.tgz", + "integrity": "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.4", + "loupe": "^3.1.4", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, "node_modules/@volar/language-core": { "version": "2.4.27", "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.27.tgz", @@ -5356,6 +5722,17 @@ "dev": true, "license": "MIT" }, + "node_modules/@vue/test-utils": { + "version": "2.4.6", + "resolved": "https://registry.npmjs.org/@vue/test-utils/-/test-utils-2.4.6.tgz", + "integrity": "sha512-FMxEjOpYNYiFe0GkaHsnJPXFHxQ6m4t8vI/ElPGpMWxZKpmRvQ33OIrvRXemy6yha03RxhOlQuy+gZMC3CQSow==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-beautify": "^1.14.9", + "vue-component-type-helpers": "^2.0.0" + } + }, "node_modules/abbrev": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-3.0.1.tgz", @@ -5664,6 +6041,16 @@ "dev": true, "license": "Python-2.0" }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, "node_modules/ast-kit": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/ast-kit/-/ast-kit-2.2.0.tgz", @@ -5822,6 +6209,16 @@ "baseline-browser-mapping": "dist/cli.js" } }, + "node_modules/bidi-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", + "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "require-from-string": "^2.0.2" + } + }, "node_modules/binary-extensions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", @@ -6156,6 +6553,23 @@ ], "license": "CC-BY-4.0" }, + "node_modules/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -6191,6 +6605,16 @@ "dev": true, "license": "MIT" }, + "node_modules/check-error": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, "node_modules/chokidar": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", @@ -6407,6 +6831,24 @@ "integrity": "sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==", "license": "MIT" }, + "node_modules/config-chain": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.13.tgz", + "integrity": "sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ini": "^1.3.4", + "proto-list": "~1.2.1" + } + }, + "node_modules/config-chain/node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true, + "license": "ISC" + }, "node_modules/consola": { "version": "3.4.2", "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz", @@ -6755,6 +7197,32 @@ "dev": true, "license": "CC0-1.0" }, + "node_modules/cssstyle": { + "version": "5.3.7", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-5.3.7.tgz", + "integrity": "sha512-7D2EPVltRrsTkhpQmksIu+LxeWAIEk6wRDMJ1qljlv+CKHJM+cJLlfhWIzNA44eAsHXSNe3+vO6DW1yCYx8SuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^4.1.1", + "@csstools/css-syntax-patches-for-csstree": "^1.0.21", + "css-tree": "^3.1.0", + "lru-cache": "^11.2.4" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/cssstyle/node_modules/lru-cache": { + "version": "11.2.6", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.6.tgz", + "integrity": "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, "node_modules/csstype": { "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", @@ -6762,6 +7230,67 @@ "dev": true, "license": "MIT" }, + "node_modules/data-urls": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-6.0.1.tgz", + "integrity": "sha512-euIQENZg6x8mj3fO6o9+fOW8MimUI4PpD/fZBhJfeioZVy9TUpM4UY7KjQNVZFlqwJ0UdzRDzkycB997HEq1BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^15.1.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/data-urls/node_modules/tr46": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz", + "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/data-urls/node_modules/webidl-conversions": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz", + "integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20" + } + }, + "node_modules/data-urls/node_modules/whatwg-mimetype": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz", + "integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/data-urls/node_modules/whatwg-url": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-15.1.0.tgz", + "integrity": "sha512-2ytDk0kiEj/yu90JOAp44PVPUkO9+jVhyf+SybKlRHSDlvOOZhdPIrr7xTH64l4WixO2cP+wQIcgujkGBPPz6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "^6.0.0", + "webidl-conversions": "^8.0.0" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/db0": { "version": "0.3.4", "resolved": "https://registry.npmjs.org/db0/-/db0-0.3.4.tgz", @@ -6814,6 +7343,23 @@ } } }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "dev": true, + "license": "MIT" + }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/deep-equal": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.0.1.tgz", @@ -7091,6 +7637,51 @@ "dev": true, "license": "MIT" }, + "node_modules/editorconfig": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/editorconfig/-/editorconfig-1.0.4.tgz", + "integrity": "sha512-L9Qe08KWTlqYMVvMcTIvMAdl1cDUubzRNYL+WfA4bLDMHe4nemKkpmYzkznE1FwLKu0EEmy6obgQKzMJrg4x9Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@one-ini/wasm": "0.1.1", + "commander": "^10.0.0", + "minimatch": "9.0.1", + "semver": "^7.5.3" + }, + "bin": { + "editorconfig": "bin/editorconfig" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/editorconfig/node_modules/commander": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", + "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/editorconfig/node_modules/minimatch": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.1.tgz", + "integrity": "sha512-0jWhJpD/MdhPXwPuiRkCbfYfSKp2qnn2eOc279qI7f+osl/l+prKSrvhg157zSYvx/1nmgn2NqdT6k2Z7zSH9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -7840,6 +8431,16 @@ "url": "https://github.com/sindresorhus/execa?sponsor=1" } }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/exsolve": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.8.tgz", @@ -8481,6 +9082,19 @@ "dev": true, "license": "MIT" }, + "node_modules/html-encoding-sniffer": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz", + "integrity": "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.6.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, "node_modules/html-entities": { "version": "2.6.0", "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.6.0.tgz", @@ -8565,6 +9179,20 @@ "url": "https://opencollective.com/express" } }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/http-shutdown": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/http-shutdown/-/http-shutdown-1.2.2.tgz", @@ -8944,6 +9572,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT" + }, "node_modules/is-reference": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-1.2.1.tgz", @@ -9069,6 +9704,110 @@ "jiti": "lib/jiti-cli.mjs" } }, + "node_modules/js-beautify": { + "version": "1.15.4", + "resolved": "https://registry.npmjs.org/js-beautify/-/js-beautify-1.15.4.tgz", + "integrity": "sha512-9/KXeZUKKJwqCXUdBxFJ3vPh467OCckSBmYDwSK/EtV090K+iMJ7zx2S3HLVDIWFQdqMIsZWbnaGiba18aWhaA==", + "dev": true, + "license": "MIT", + "dependencies": { + "config-chain": "^1.1.13", + "editorconfig": "^1.0.4", + "glob": "^10.4.2", + "js-cookie": "^3.0.5", + "nopt": "^7.2.1" + }, + "bin": { + "css-beautify": "js/bin/css-beautify.js", + "html-beautify": "js/bin/html-beautify.js", + "js-beautify": "js/bin/js-beautify.js" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/js-beautify/node_modules/abbrev": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-2.0.0.tgz", + "integrity": "sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/js-beautify/node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/js-beautify/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/js-beautify/node_modules/nopt": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-7.2.1.tgz", + "integrity": "sha512-taM24ViiimT/XntxbPyJQzCG+p4EKOpgD3mxFwW38mGjVUrfERQOeY4EDHjdnptttfHuHQXFx+lTP08Q+mLa/w==", + "dev": true, + "license": "ISC", + "dependencies": { + "abbrev": "^2.0.0" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/js-beautify/node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/js-cookie": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz", + "integrity": "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -9099,6 +9838,93 @@ "node": ">=20.0.0" } }, + "node_modules/jsdom": { + "version": "27.4.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-27.4.0.tgz", + "integrity": "sha512-mjzqwWRD9Y1J1KUi7W97Gja1bwOOM5Ug0EZ6UDK3xS7j7mndrkwozHtSblfomlzyB4NepioNt+B2sOSzczVgtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@acemir/cssom": "^0.9.28", + "@asamuzakjp/dom-selector": "^6.7.6", + "@exodus/bytes": "^1.6.0", + "cssstyle": "^5.3.4", + "data-urls": "^6.0.0", + "decimal.js": "^10.6.0", + "html-encoding-sniffer": "^6.0.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.6", + "is-potential-custom-element-name": "^1.0.1", + "parse5": "^8.0.0", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^6.0.0", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^8.0.0", + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^15.1.0", + "ws": "^8.18.3", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jsdom/node_modules/tr46": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz", + "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/jsdom/node_modules/webidl-conversions": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz", + "integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20" + } + }, + "node_modules/jsdom/node_modules/whatwg-url": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-15.1.0.tgz", + "integrity": "sha512-2ytDk0kiEj/yu90JOAp44PVPUkO9+jVhyf+SybKlRHSDlvOOZhdPIrr7xTH64l4WixO2cP+wQIcgujkGBPPz6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "^6.0.0", + "webidl-conversions": "^8.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/jsdom/node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, "node_modules/jsesc": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", @@ -9597,6 +10423,13 @@ "dev": true, "license": "MIT" }, + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true, + "license": "MIT" + }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -10729,6 +11562,32 @@ "dev": true, "license": "MIT" }, + "node_modules/parse5": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.0.tgz", + "integrity": "sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5/node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -10819,6 +11678,16 @@ "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", "license": "MIT" }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, "node_modules/perfect-debounce": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-2.1.0.tgz", @@ -11601,6 +12470,13 @@ "node": ">= 6" } }, + "node_modules/proto-list": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", + "integrity": "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==", + "dev": true, + "license": "ISC" + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -11888,6 +12764,16 @@ "node": ">=0.10.0" } }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/reserved-identifiers": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/reserved-identifiers/-/reserved-identifiers-1.2.0.tgz", @@ -12189,6 +13075,19 @@ "node": ">=11.0.0" } }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, "node_modules/scslre": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/scslre/-/scslre-0.3.0.tgz", @@ -12341,6 +13240,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, "node_modules/signal-exit": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", @@ -12513,6 +13419,13 @@ "node": ">=12.0.0" } }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, "node_modules/standard-as-callback": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz", @@ -12789,6 +13702,13 @@ "node": ">=16" } }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true, + "license": "MIT" + }, "node_modules/system-architecture": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/system-architecture/-/system-architecture-0.1.0.tgz", @@ -13115,6 +14035,13 @@ "dev": true, "license": "MIT" }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, "node_modules/tinyexec": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.2.tgz", @@ -13140,6 +14067,56 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", + "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.4.tgz", + "integrity": "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tldts": { + "version": "7.0.23", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.23.tgz", + "integrity": "sha512-ASdhgQIBSay0R/eXggAkQ53G4nTJqTXqC2kbaBbdDwM7SkjyZyO0OaaN1/FH7U/yCeqOHDwFO5j8+Os/IS1dXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^7.0.23" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "7.0.23", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.23.tgz", + "integrity": "sha512-0g9vrtDQLrNIiCj22HSe9d4mLVG3g5ph5DZ8zCKBr4OtrspmNB6ss7hVyzArAeE88ceZocIEGkyW1Ime7fxPtQ==", + "dev": true, + "license": "MIT" + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -13188,6 +14165,19 @@ "node": ">=6" } }, + "node_modules/tough-cookie": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.0.tgz", + "integrity": "sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^7.0.5" + }, + "engines": { + "node": ">=16" + } + }, "node_modules/tr46": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", @@ -14243,6 +15233,116 @@ "@types/estree": "^1.0.0" } }, + "node_modules/vitest": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz", + "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/expect": "3.2.4", + "@vitest/mocker": "3.2.4", + "@vitest/pretty-format": "^3.2.4", + "@vitest/runner": "3.2.4", + "@vitest/snapshot": "3.2.4", + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", + "chai": "^5.2.0", + "debug": "^4.4.1", + "expect-type": "^1.2.1", + "magic-string": "^0.30.17", + "pathe": "^2.0.3", + "picomatch": "^4.0.2", + "std-env": "^3.9.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.14", + "tinypool": "^1.1.1", + "tinyrainbow": "^2.0.0", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", + "vite-node": "3.2.4", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/debug": "^4.1.12", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@vitest/browser": "3.2.4", + "@vitest/ui": "3.2.4", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/debug": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/vitest/node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/vitest/node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/vitest/node_modules/vite-node": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz", + "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.4.1", + "es-module-lexer": "^1.7.0", + "pathe": "^2.0.3", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, "node_modules/vscode-uri": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.1.0.tgz", @@ -14282,6 +15382,13 @@ "ufo": "^1.6.1" } }, + "node_modules/vue-component-type-helpers": { + "version": "2.2.12", + "resolved": "https://registry.npmjs.org/vue-component-type-helpers/-/vue-component-type-helpers-2.2.12.tgz", + "integrity": "sha512-YbGqHZ5/eW4SnkPNR44mKVc6ZKQoRs/Rux1sxC6rdwXb4qpbOSYfDr9DsTHolOTGmIKgM9j141mZbBeg05R1pw==", + "dev": true, + "license": "MIT" + }, "node_modules/vue-devtools-stub": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/vue-devtools-stub/-/vue-devtools-stub-0.1.0.tgz", @@ -14342,6 +15449,29 @@ "vue": "^3.5.0" } }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/w3c-xmlserializer/node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, "node_modules/webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", @@ -14355,6 +15485,16 @@ "integrity": "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==", "license": "MIT" }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/whatwg-url": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", @@ -14382,6 +15522,23 @@ "node": ">= 8" } }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", @@ -14482,6 +15639,13 @@ "node": ">=12" } }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT" + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", diff --git a/package.json b/package.json index 6167cc3..005d5b3 100644 --- a/package.json +++ b/package.json @@ -10,17 +10,22 @@ "build": "nuxt build .playground", "generate": "nuxt generate .playground", "preview": "nuxt preview .playground", - "lint": "eslint ." + "lint": "eslint .", + "test": "vitest run" }, "peerDependencies": { "nuxt": "^4.0.0" }, "devDependencies": { + "@vitejs/plugin-vue": "^6.0.1", + "@vue/test-utils": "^2.4.6", "@nuxt/eslint": "latest", "@types/node": "^24.10.13", "eslint": "^10.0.0", + "jsdom": "^27.0.1", "nuxt": "^4.3.1", "typescript": "^5.9.3", + "vitest": "^3.2.4", "vue": "latest" }, "dependencies": { diff --git a/pre-commit b/pre-commit new file mode 100644 index 0000000..b616324 --- /dev/null +++ b/pre-commit @@ -0,0 +1,36 @@ +#!/bin/sh +set -e + +echo "######### Pre-commit hook start #############" + +if ! command -v npm >/dev/null 2>&1; then + if [ -f ".nvmrc" ]; then + NVM_VERSION="$(tr -d '\r\n' < .nvmrc)" + NVM_VERSION="${NVM_VERSION#v}" + NPM_BIN="$HOME/.nvm/versions/node/v$NVM_VERSION/bin" + if [ -x "$NPM_BIN/npm" ]; then + PATH="$NPM_BIN:$PATH" + export PATH + fi + fi +fi + +if ! command -v npm >/dev/null 2>&1; then + if [ -s "$HOME/.nvm/nvm.sh" ]; then + # shellcheck disable=SC1090 + . "$HOME/.nvm/nvm.sh" + nvm use >/dev/null 2>&1 || true + fi +fi + +if ! command -v npm >/dev/null 2>&1; then + echo "npm introuvable dans le hook. Abandon du commit." + exit 1 +fi + +echo "--- make pre-commit start ---" +make pre-commit +echo "--- make pre-commit finished ---" + +echo "All checks passed. Proceeding with commit." +exit 0 diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..a35ba47 --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from 'vitest/config' +import vue from '@vitejs/plugin-vue' + +export default defineConfig({ + plugins: [vue()], + test: { + environment: 'jsdom', + include: ['app/**/*.test.ts'], + }, +}) -- 2.39.5 From 1ab7b2427a93db2b2421bfc56a4f746734b8d375 Mon Sep 17 00:00:00 2001 From: kevin Date: Mon, 2 Mar 2026 13:24:58 +0000 Subject: [PATCH 04/38] =?UTF-8?q?[#337]=20Cr=C3=A9ation=20d'un=20composant?= =?UTF-8?q?=20Select=20(#3)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit | Numéro du ticket | Titre du ticket | |------------------|-----------------| | #337 | Création d'un composant Select | ## Description de la PR ## Modification du .env ## Check list - [x] Pas de régression - [ ] TU/TI/TF rédigée - [x] TU/TI/TF OK - [x] CHANGELOG modifié Co-authored-by: tristan Reviewed-on: https://gitea.malio.fr/MALIO-DEV/malio-layer-ui/pulls/3 Reviewed-by: Autin Co-authored-by: kevin Co-committed-by: kevin --- .playground/pages/composant/inputText.vue | 184 ++ .playground/pages/composant/inputTextArea.vue | 104 + .playground/pages/composant/select.vue | 149 ++ .playground/pages/index.vue | 127 +- CHANGELOG.md | 1 + app/assets/css/malio.css | 19 + app/components/malio/Input.test.ts | 300 ++- app/components/malio/Input.vue | 38 - app/components/malio/InputText.vue | 235 +++ app/components/malio/InputTextArea.test.ts | 152 ++ app/components/malio/InputTextArea.vue | 186 ++ app/components/malio/Select.test.ts | 177 ++ app/components/malio/Select.vue | 333 ++++ app/story/InputSelect.story.vue | 148 ++ app/story/inputText.story.vue | 200 ++ app/story/inputTextArea.story.vue | 192 ++ histoire.config.ts | 24 + histoire.setup.ts | 4 + makefile | 3 + nuxt.config.ts | 31 +- package-lock.json | 1688 +++++++++++++++-- package.json | 22 +- pre-commit | 26 +- tailwind.config.ts | 33 + tsconfig.json | 20 +- 25 files changed, 4208 insertions(+), 188 deletions(-) create mode 100644 .playground/pages/composant/inputText.vue create mode 100644 .playground/pages/composant/inputTextArea.vue create mode 100644 .playground/pages/composant/select.vue create mode 100644 app/assets/css/malio.css delete mode 100644 app/components/malio/Input.vue create mode 100644 app/components/malio/InputText.vue create mode 100644 app/components/malio/InputTextArea.test.ts create mode 100644 app/components/malio/InputTextArea.vue create mode 100644 app/components/malio/Select.test.ts create mode 100644 app/components/malio/Select.vue create mode 100644 app/story/InputSelect.story.vue create mode 100644 app/story/inputText.story.vue create mode 100644 app/story/inputTextArea.story.vue create mode 100644 histoire.config.ts create mode 100644 histoire.setup.ts create mode 100644 tailwind.config.ts diff --git a/.playground/pages/composant/inputText.vue b/.playground/pages/composant/inputText.vue new file mode 100644 index 0000000..e329107 --- /dev/null +++ b/.playground/pages/composant/inputText.vue @@ -0,0 +1,184 @@ + + + diff --git a/.playground/pages/composant/inputTextArea.vue b/.playground/pages/composant/inputTextArea.vue new file mode 100644 index 0000000..e093158 --- /dev/null +++ b/.playground/pages/composant/inputTextArea.vue @@ -0,0 +1,104 @@ + + + diff --git a/.playground/pages/composant/select.vue b/.playground/pages/composant/select.vue new file mode 100644 index 0000000..8b4cc0c --- /dev/null +++ b/.playground/pages/composant/select.vue @@ -0,0 +1,149 @@ + + + diff --git a/.playground/pages/index.vue b/.playground/pages/index.vue index 257a509..17e7186 100644 --- a/.playground/pages/index.vue +++ b/.playground/pages/index.vue @@ -1,11 +1,128 @@ +import { computed, ref, watch, shallowRef } from 'vue' +type LoadedModule = { + default: unknown +} + +type Item = { + name: string + label: string +} + +const componentModules = import.meta.glob('../../app/components/malio/*.vue') +const demoModules = import.meta.glob('./composant/*.vue') + +const demoByName: Record Promise> = + Object.fromEntries( + Object.entries(demoModules).map(([file, loader]) => { + const name = file.split('/').pop()?.replace('.vue', '') ?? '' + return [name.toLowerCase(), loader as () => Promise] + }), + ) + +const items = computed(() => + Object.keys(componentModules).map((file) => { + const name = file.split('/').pop()?.replace('.vue', '') ?? '' + return { + name, + label: name, + } + }), +) + +const selectedName = ref('') +const hasInitializedSelection = ref(false) + +watch( + items, + (val) => { + if (!hasInitializedSelection.value && val.length > 0) { + selectedName.value = val[0].name + hasInitializedSelection.value = true + } + }, + { immediate: true }, +) + +function selectOrToggle(name: string) { + selectedName.value = selectedName.value === name ? '' : name +} + +function clearSelection() { + selectedName.value = '' +} + +const selectedDemoComponent = shallowRef(null) + +watch(selectedName, async (name) => { + if (!name) { + selectedDemoComponent.value = null + return + } + + const loader = demoByName[name.toLowerCase()] + if (!loader) { + selectedDemoComponent.value = null + return + } + + const mod = await loader() + selectedDemoComponent.value = mod.default +}) + +const selectedDemoFileName = computed(() => { + const name = selectedName.value + if (!name) return '' + return name.charAt(0).toLowerCase() + name.slice(1) +}) + diff --git a/CHANGELOG.md b/CHANGELOG.md index f230edc..4b688f4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ Liste des évolutions de la librairie Malio layer UI ### Parameters ### Added +* [#333] Création d'un composant text ### Changed diff --git a/app/assets/css/malio.css b/app/assets/css/malio.css new file mode 100644 index 0000000..0fa1352 --- /dev/null +++ b/app/assets/css/malio.css @@ -0,0 +1,19 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +@layer base { + :root { + /* Couleurs en RGB “space separated” pour Tailwind */ + --m-primary: 34 39 131; /* Couleur principal*/ + --m-secondary: 48 73 152; /* Couleur secondaire */ + --m-tertiary: 243 244 248; /* Couleur tertiaire (background) */ + --m-border: 203 213 225; /* Couleur des bordures */ + --m-text: 15 23 42; /* Couleur du texte */ + --m-muted: 100 116 139; /* Couleur pour les éléments désactivés ou secondaires */ + --m-bg: 243 244 248; /* Couleur de fond générale */ + + --m-error: 155 17 30; /* rouge pour les erreurs */ + --m-success: 15 149 70; /* vert pour les succès */ + } +} diff --git a/app/components/malio/Input.test.ts b/app/components/malio/Input.test.ts index aefc9fb..317dcf5 100644 --- a/app/components/malio/Input.test.ts +++ b/app/components/malio/Input.test.ts @@ -1,23 +1,297 @@ -import { describe, expect, it } from 'vitest' -import { mount } from '@vue/test-utils' -import Input from './Input.vue' +import {describe, expect, it} from 'vitest' +import {mount} from '@vue/test-utils' +import type {DefineComponent} from 'vue' +import Input from './InputText.vue' -describe('MalioInput', () => { - it('affiche la valeur initiale', () => { - const wrapper = mount(Input, { - props: { modelValue: 'hello' }, - }) +type InputProps = { + id?: string + label?: string + name?: string + autocomplete?: string + modelValue?: string | null + inputClass?: string + labelClass?: string + groupClass?: string + required?: boolean + maxLength?: number | string + minLength?: number | string + disabled?: boolean + readonly?: boolean + hint?: string + error?: string + success?: string + iconName?: string + iconPosition?: 'left' | 'right' + iconSize?: string | number + iconColor?: string +} - expect(wrapper.get('input').element.value).toBe('hello') +const InputForTest = Input as DefineComponent + +const mountInput = (props: InputProps = {}) => + mount(InputForTest, { + props, + global: { + stubs: { + IconifyIcon: { + template: '', + }, + }, + }, }) - it('emet update:modelValue au changement', async () => { - const wrapper = mount(Input, { - props: { modelValue: '' }, - }) +describe('MalioInputText', () => { + it('renders the initial input value', () => { + const wrapper = mountInput({modelValue: 'initialValueTest'}) + + expect(wrapper.get('input').element.value).toBe('initialValueTest') + }) + + it('renders the label text', () => { + const wrapper = mountInput({label: 'labelTest'}) + + expect(wrapper.get('label').text()).toBe('labelTest') + }) + + it('applies the name attribute', () => { + const wrapper = mountInput({name: 'nameTest'}) + + expect(wrapper.get('input').attributes('name')).toBe('nameTest') + }) + + it('uses provided id on input and label', () => { + const wrapper = mountInput({id: 'custom-id', label: 'Label'}) + + expect(wrapper.get('input').attributes('id')).toBe('custom-id') + expect(wrapper.get('label').attributes('for')).toBe('custom-id') + }) + + it('keeps the default rounded class on input', () => { + const wrapper = mountInput() + + expect(wrapper.get('input').classes()).toContain('rounded-md') + }) + + it('generates an id when missing and reuses it on label', () => { + const wrapper = mountInput({label: 'Label'}) + + const inputId = wrapper.get('input').attributes('id') + + expect(inputId?.startsWith('malio-input-text-')).toBe(true) + expect(wrapper.get('label').attributes('for')).toBe(inputId) + }) + + it('applies the autocomplete attribute', () => { + const wrapper = mountInput({autocomplete: 'autocompleteTest'}) + + expect(wrapper.get('input').attributes('autocomplete')).toBe('autocompleteTest') + }) + + it('does not set required when false', () => { + const wrapper = mountInput({required: false}) + + expect(wrapper.get('input').attributes('required')).toBeUndefined() + }) + + it('sets required when true', () => { + const wrapper = mountInput({required: true}) + + expect(wrapper.get('input').attributes('required')).toBeDefined() + }) + + it('does not set readonly when false', () => { + const wrapper = mountInput({readonly: false}) + + expect(wrapper.get('input').attributes('readonly')).toBeUndefined() + }) + + it('sets readonly when true', () => { + const wrapper = mountInput({readonly: true}) + + expect(wrapper.get('input').attributes('readonly')).toBeDefined() + }) + + it('does not set disabled and keeps text cursor when false', () => { + const wrapper = mountInput({disabled: false}) + + expect(wrapper.get('input').attributes('disabled')).toBeUndefined() + expect(wrapper.get('input').classes()).toContain('cursor-text') + }) + + it('sets disabled styles when true', () => { + const wrapper = mountInput({disabled: true}) + + expect(wrapper.get('input').attributes('disabled')).toBeDefined() + expect(wrapper.get('input').classes()).toContain('cursor-not-allowed') + expect(wrapper.get('input').classes()).toContain('text-black/60') + }) + + it('emits update:modelValue on input change', async () => { + const wrapper = mountInput({modelValue: ''}) await wrapper.get('input').setValue('new value') expect(wrapper.emitted('update:modelValue')?.[0]).toEqual(['new value']) }) + + it('applies maxLength to input', () => { + const wrapper = mountInput({maxLength: 25}) + + expect(wrapper.get('input').attributes('maxlength')).toBe('25') + }) + + it('applies minLength to input', () => { + const wrapper = mountInput({minLength: 25}) + + expect(wrapper.get('input').attributes('minlength')).toBe('25') + }) + + it('applies labelClass on label', () => { + const wrapper = mountInput({label: 'Label', labelClass: 'text-red-500'}) + + expect(wrapper.get('label').classes()).toContain('text-red-500') + }) + + it('applies inputClass on input', () => { + const wrapper = mountInput({inputClass: 'text-sm'}) + + expect(wrapper.get('input').classes()).toContain('text-sm') + }) + + it('shows error message without label and icon', () => { + const wrapper = mountInput({error: 'Error message test'}) + + expect(wrapper.get('p.text-m-error').text()).toBe('Error message test') + expect(wrapper.get('input').classes()).toContain('border-m-error') + expect(wrapper.get('input').attributes('aria-invalid')).toBe('true') + expect(wrapper.get('p').classes()).toContain('text-m-error') + }) + + it('shows error message with label and without icon', () => { + const wrapper = mountInput({error: 'Error message test', label: 'Error message'}) + + expect(wrapper.get('p.text-m-error').text()).toBe('Error message test') + expect(wrapper.get('input').classes()).toContain('border-m-error') + expect(wrapper.get('label').classes()).toContain('text-m-error') + expect(wrapper.get('p').classes()).toContain('text-m-error') + }) + + it('shows error message with label and icon', () => { + const wrapper = mountInput({ + error: 'Error message test', + label: 'Error message', + iconName: 'mdi:key-outline', + }) + + expect(wrapper.get('p.text-m-error').text()).toBe('Error message test') + expect(wrapper.get('input').classes()).toContain('border-m-error') + expect(wrapper.get('label').classes()).toContain('text-m-error') + expect(wrapper.get('[data-test="icon"]').classes()).toContain('text-m-error') + expect(wrapper.get('p').classes()).toContain('text-m-error') + }) + + it('shows error message with icon and without label', () => { + const wrapper = mountInput({error: 'Error message test', iconName: 'mdi:key-outline'}) + + expect(wrapper.get('p.text-m-error').text()).toBe('Error message test') + expect(wrapper.get('input').classes()).toContain('border-m-error') + expect(wrapper.get('[data-test="icon"]').classes()).toContain('text-m-error') + }) + + it('shows success message without label and icon', () => { + const wrapper = mountInput({success: 'Success message test'}) + + expect(wrapper.get('p.text-m-success').text()).toBe('Success message test') + expect(wrapper.get('input').classes()).toContain('border-m-success') + }) + + it('shows success message with label and without icon', () => { + const wrapper = mountInput({success: 'Success message test', label: 'Success message'}) + + expect(wrapper.get('p.text-m-success').text()).toBe('Success message test') + expect(wrapper.get('input').classes()).toContain('border-m-success') + expect(wrapper.get('label').classes()).toContain('text-m-success') + }) + + it('shows success message with label and icon', () => { + const wrapper = mountInput({ + success: 'Success message test', + label: 'Success message', + iconName: 'mdi:key-outline', + }) + + expect(wrapper.get('p.text-m-success').text()).toBe('Success message test') + expect(wrapper.get('input').classes()).toContain('border-m-success') + expect(wrapper.get('label').classes()).toContain('text-m-success') + expect(wrapper.get('[data-test="icon"]').classes()).toContain('text-m-success') + }) + + it('shows success message with icon and without label', () => { + const wrapper = mountInput({success: 'Success message test', iconName: 'mdi:key-outline'}) + + expect(wrapper.get('p.text-m-success').text()).toBe('Success message test') + expect(wrapper.get('input').classes()).toContain('border-m-success') + expect(wrapper.get('[data-test="icon"]').classes()).toContain('text-m-success') + }) + + it('prioritizes error over success when both are provided', () => { + const wrapper = mountInput({ + error: 'Error message test', + success: 'Success message test', + }) + + expect(wrapper.find('p.text-m-error').exists()).toBe(true) + expect(wrapper.get('p.text-m-error').text()).toBe('Error message test') + expect(wrapper.find('p.text-m-success').exists()).toBe(false) + expect(wrapper.get('input').classes()).toContain('border-m-error') + expect(wrapper.get('input').classes()).not.toContain('border-m-success') + }) + + it('shows hint message', () => { + const wrapper = mountInput({hint: 'Hint message test'}) + + expect(wrapper.get('p.text-m-muted').text()).toBe('Hint message test') + }) + + it('does not render label when label prop is missing', () => { + const wrapper = mountInput({labelClass: 'text-red-500'}) + + expect(wrapper.find('label').exists()).toBe(false) + }) + + it('renders icon with default positioning and muted color', () => { + const wrapper = mountInput({iconName: 'mdi:key-outline'}) + + expect(wrapper.get('[data-test="icon"]').classes()).toContain('text-m-muted') + expect(wrapper.get('[data-test="icon"]').classes()).toContain('pointer-events-none') + expect(wrapper.get('[data-test="icon"]').classes()).toContain('absolute') + expect(wrapper.get('[data-test="icon"]').classes()).toContain('right-2') + expect(wrapper.get('[data-test="icon"]').classes()).toContain('top-1/2') + expect(wrapper.get('[data-test="icon"]').classes()).toContain('-translate-y-1/2') + }) + + it('renders icon on the left when requested', () => { + const wrapper = mountInput({ + iconName: 'mdi:key-outline', + iconPosition: 'left', + label: 'Password', + }) + + expect(wrapper.get('[data-test="icon"]').classes()).toContain('left-2') + expect(wrapper.get('input').classes()).toContain('!pl-11') + expect(wrapper.get('label').classes()).toContain('left-8') + }) + + it('passes icon size props to icon component', () => { + const wrapper = mountInput({iconName: 'mdi:key-outline', iconSize: '24'}) + + expect(wrapper.get('[data-test="icon"]').attributes('width')).toBe('24') + expect(wrapper.get('[data-test="icon"]').attributes('height')).toBe('24') + }) + + it('applies icon color class', () => { + const wrapper = mountInput({iconName: 'mdi:key-outline', iconColor: 'text-m-primary'}) + + expect(wrapper.get('[data-test="icon"]').classes()).toContain('text-m-primary') + }) }) diff --git a/app/components/malio/Input.vue b/app/components/malio/Input.vue deleted file mode 100644 index 17119af..0000000 --- a/app/components/malio/Input.vue +++ /dev/null @@ -1,38 +0,0 @@ - - - diff --git a/app/components/malio/InputText.vue b/app/components/malio/InputText.vue new file mode 100644 index 0000000..d1dc77d --- /dev/null +++ b/app/components/malio/InputText.vue @@ -0,0 +1,235 @@ + + + + + diff --git a/app/components/malio/InputTextArea.test.ts b/app/components/malio/InputTextArea.test.ts new file mode 100644 index 0000000..5398e6c --- /dev/null +++ b/app/components/malio/InputTextArea.test.ts @@ -0,0 +1,152 @@ +import {describe, expect, it} from 'vitest' +import {mount} from '@vue/test-utils' +import type {DefineComponent} from 'vue' +import InputTextArea from './InputTextArea.vue' + +type InputTextAreaProps = { + id?: string + label?: string + name?: string + autocomplete?: string + modelValue?: string | null + size?: number | string + textInput?: string + textLabel?: string + required?: boolean + maxLength?: number + showCounter?: boolean + disabled?: boolean + readonly?: boolean + hint?: string + error?: string + success?: string + rounded?: string +} + +const InputTextAreaForTest = InputTextArea as DefineComponent + +describe('MalioInputTextArea', () => { + it('renders the initial textarea value', () => { + const wrapper = mount(InputTextAreaForTest, { + props: {modelValue: 'initial textarea value'}, + }) + + expect(wrapper.get('textarea').element.value).toBe('initial textarea value') + }) + + it('renders the label text and reuses a provided id', () => { + const wrapper = mount(InputTextAreaForTest, { + props: {id: 'custom-textarea-id', label: 'Description'}, + }) + + expect(wrapper.get('textarea').attributes('id')).toBe('custom-textarea-id') + expect(wrapper.get('label').attributes('for')).toBe('custom-textarea-id') + expect(wrapper.get('label').text()).toBe('Description') + }) + + it('generates an id when missing', () => { + const wrapper = mount(InputTextAreaForTest, { + props: {label: 'Description'}, + }) + + const textareaId = wrapper.get('textarea').attributes('id') + expect(textareaId?.startsWith('malio-input-textarea-')).toBe(true) + expect(wrapper.get('label').attributes('for')).toBe(textareaId) + }) + + it('applies name, autocomplete and rows attributes', () => { + const wrapper = mount(InputTextAreaForTest, { + props: {name: 'bio', autocomplete: 'on', size: 4}, + }) + + expect(wrapper.get('textarea').attributes('name')).toBe('bio') + expect(wrapper.get('textarea').attributes('autocomplete')).toBe('on') + expect(wrapper.get('textarea').attributes('rows')).toBe('4') + }) + + it('sets required, readonly and disabled attributes', () => { + const wrapper = mount(InputTextAreaForTest, { + props: { + required: true, + readonly: true, + disabled: true, + }, + }) + + expect(wrapper.get('textarea').attributes('required')).toBeDefined() + expect(wrapper.get('textarea').attributes('readonly')).toBeDefined() + expect(wrapper.get('textarea').attributes('disabled')).toBeDefined() + expect(wrapper.get('textarea').classes()).toContain('cursor-not-allowed') + }) + + it('emits update:modelValue on input change', async () => { + const wrapper = mount(InputTextAreaForTest, { + props: {modelValue: ''}, + }) + + await wrapper.get('textarea').setValue('new textarea value') + + expect(wrapper.emitted('update:modelValue')?.[0]).toEqual(['new textarea value']) + }) + + it('shows the character counter when enabled', () => { + const wrapper = mount(InputTextAreaForTest, { + props: { + modelValue: 'hello', + showCounter: true, + maxLength: 20, + }, + }) + + expect(wrapper.get('span.text-xs').text()).toBe('5/20') + expect(wrapper.get('textarea').classes()).toContain('pb-6') + }) + + it('shows hint message in muted color', () => { + const wrapper = mount(InputTextAreaForTest, { + props: {hint: 'Helpful hint'}, + }) + + expect(wrapper.get('p.text-m-muted').text()).toBe('Helpful hint') + }) + + it('shows error state on textarea and label', () => { + const wrapper = mount(InputTextAreaForTest, { + props: { + label: 'Description', + error: 'Textarea error', + }, + }) + + expect(wrapper.get('textarea').classes()).toContain('border-m-error') + expect(wrapper.get('label').classes()).toContain('text-m-error') + expect(wrapper.get('p.text-m-error').text()).toBe('Textarea error') + expect(wrapper.get('textarea').attributes('aria-invalid')).toBe('true') + }) + + it('shows success state on textarea and label', () => { + const wrapper = mount(InputTextAreaForTest, { + props: { + label: 'Description', + success: 'Textarea success', + }, + }) + + expect(wrapper.get('textarea').classes()).toContain('border-m-success') + expect(wrapper.get('label').classes()).toContain('text-m-success') + expect(wrapper.get('p.text-m-success').text()).toBe('Textarea success') + }) + + it('prioritizes error over success', () => { + const wrapper = mount(InputTextAreaForTest, { + props: { + error: 'Textarea error', + success: 'Textarea success', + }, + }) + + expect(wrapper.get('textarea').classes()).toContain('border-m-error') + expect(wrapper.find('p.text-m-success').exists()).toBe(false) + expect(wrapper.get('p.text-m-error').text()).toBe('Textarea error') + }) +}) diff --git a/app/components/malio/InputTextArea.vue b/app/components/malio/InputTextArea.vue new file mode 100644 index 0000000..c3294f5 --- /dev/null +++ b/app/components/malio/InputTextArea.vue @@ -0,0 +1,186 @@ +