Compare commits
12 Commits
v1.2.3
...
a5ced1456e
| Author | SHA1 | Date | |
|---|---|---|---|
| a5ced1456e | |||
| 480defcd82 | |||
| 33179ade9d | |||
| 82ecc9cfe2 | |||
| a9627a6875 | |||
| 71797f0804 | |||
| 65d9060e26 | |||
| 1013c83018 | |||
| 9710fd63d7 | |||
| 75a3912727 | |||
| 2340256ee1 | |||
| ec4c157226 |
23
.gitea/PULL_REQUEST_TEMPLATE.md
Normal file
23
.gitea/PULL_REQUEST_TEMPLATE.md
Normal file
@@ -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é
|
||||||
173
.playground/pages/composant/inputText.vue
Normal file
173
.playground/pages/composant/inputText.vue
Normal file
@@ -0,0 +1,173 @@
|
|||||||
|
<template>
|
||||||
|
<div class="grid grid-cols-1 items-start gap-6 md:grid-cols-2">
|
||||||
|
<div class="rounded-lg border p-4">
|
||||||
|
<h2 class="mb-4 text-xl font-bold">Simple</h2>
|
||||||
|
<MalioInputText v-model="simpleValue"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="rounded-lg border p-4">
|
||||||
|
<h2 class="mb-4 text-xl font-bold">Avec label</h2>
|
||||||
|
<MalioInputText
|
||||||
|
v-model="nameValue"
|
||||||
|
label="Nom d'utilisateur"
|
||||||
|
name="username"
|
||||||
|
autocomplete="username"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="rounded-lg border p-4">
|
||||||
|
<h2 class="mb-4 text-xl font-bold">Avec icône</h2>
|
||||||
|
<MalioInputText
|
||||||
|
v-model="searchValue"
|
||||||
|
label="Recherche"
|
||||||
|
icon-name="mdi:magnify"
|
||||||
|
icon-size="20"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="rounded-lg border p-4">
|
||||||
|
<h2 class="mb-4 text-xl font-bold">Désactivé</h2>
|
||||||
|
<MalioInputText
|
||||||
|
model-value="Valeur verrouillée"
|
||||||
|
disabled
|
||||||
|
label="Champ désactivé"
|
||||||
|
/>
|
||||||
|
<MalioInputText
|
||||||
|
disabled
|
||||||
|
label="Champ désactivé"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="rounded-lg border p-4">
|
||||||
|
<h2 class="mb-4 text-xl font-bold">Readonly</h2>
|
||||||
|
<MalioInputText
|
||||||
|
model-value="Lecture seule"
|
||||||
|
readonly
|
||||||
|
label="Champ readonly"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="rounded-lg border p-4">
|
||||||
|
<h2 class="mb-4 text-xl font-bold">Hint + icône</h2>
|
||||||
|
<MalioInputText
|
||||||
|
v-model="cityValue"
|
||||||
|
label="Ville"
|
||||||
|
icon-name="mdi:map-marker-outline"
|
||||||
|
icon-size="20"
|
||||||
|
hint="Commencez à taper le nom de la ville"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="rounded-lg border p-4">
|
||||||
|
<h2 class="mb-4 text-xl font-bold">Erreur + icône</h2>
|
||||||
|
<MalioInputText
|
||||||
|
model-value="ab"
|
||||||
|
label="Code promo"
|
||||||
|
icon-name="mdi:alert-circle-outline"
|
||||||
|
icon-size="20"
|
||||||
|
error="Le code est invalide"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="rounded-lg border p-4">
|
||||||
|
<h2 class="mb-4 text-xl font-bold">Succès + icône</h2>
|
||||||
|
<MalioInputText
|
||||||
|
label="Code"
|
||||||
|
success="Code valide"
|
||||||
|
icon-name="mdi:alert-circle-outline"
|
||||||
|
icon-size="20"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="rounded-lg border p-4">
|
||||||
|
<h2 class="mb-4 text-xl font-bold">Readonly + icône</h2>
|
||||||
|
<MalioInputText
|
||||||
|
model-value="Commande #A-2048"
|
||||||
|
label="Référence"
|
||||||
|
readonly
|
||||||
|
icon-name="mdi:lock-outline"
|
||||||
|
icon-size="20"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="rounded-lg border p-4">
|
||||||
|
<h2 class="mb-4 text-xl font-bold">Désactivé + icône</h2>
|
||||||
|
<MalioInputText
|
||||||
|
model-value="Compte indisponible"
|
||||||
|
label="Compte"
|
||||||
|
disabled
|
||||||
|
icon-name="mdi:account-off-outline"
|
||||||
|
icon-size="20"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="rounded-lg border p-4">
|
||||||
|
<h2 class="mb-4 text-xl font-bold">Avec masque</h2>
|
||||||
|
<MalioInputText
|
||||||
|
label="Plaque d'immatriculation"
|
||||||
|
:mask="maskOptions"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="rounded-lg border p-4">
|
||||||
|
<h2 class="mb-4 text-xl font-bold">Code dynamique</h2>
|
||||||
|
<MalioInputText
|
||||||
|
v-model="dynamicCodeValue"
|
||||||
|
label="Code d'accès"
|
||||||
|
hint="Format attendu: 6 à 10 caractères alphanumériques"
|
||||||
|
:error="dynamicCodeError"
|
||||||
|
:success="dynamicCodeSuccess"
|
||||||
|
icon-name="mdi:key-outline"
|
||||||
|
icon-size="20"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="rounded-lg border p-4">
|
||||||
|
<h2 class="mb-4 text-xl font-bold">Hint,erreur et succès</h2>
|
||||||
|
<MalioInputText
|
||||||
|
v-model="codeValue"
|
||||||
|
label="Code"
|
||||||
|
hint="Format attendu: 6 à 10 caractères alphanumériques"
|
||||||
|
/>
|
||||||
|
<div class="mt-4">
|
||||||
|
<MalioInputText
|
||||||
|
model-value="invalide"
|
||||||
|
label="Code"
|
||||||
|
error="Le code doit contenir au moins 6 caractères"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="mt-4">
|
||||||
|
<MalioInputText
|
||||||
|
model-value="valide"
|
||||||
|
label="Code"
|
||||||
|
success="Le code est valide"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
const simpleValue = ref('')
|
||||||
|
const nameValue = ref('')
|
||||||
|
const searchValue = ref('')
|
||||||
|
const codeValue = ref('')
|
||||||
|
const cityValue = ref('')
|
||||||
|
const dynamicCodeValue = ref('')
|
||||||
|
|
||||||
|
const codeRegex = /^[A-Z0-9]{6,10}$/
|
||||||
|
const normalizedDynamicCode = computed(() => dynamicCodeValue.value.toUpperCase())
|
||||||
|
const isDynamicCodeValid = computed(() => codeRegex.test(normalizedDynamicCode.value))
|
||||||
|
const dynamicCodeError = computed(() => {
|
||||||
|
if (!dynamicCodeValue.value) return ''
|
||||||
|
return isDynamicCodeValid.value ? '' : 'Code invalide (6 à 10 caractères alphanumériques)'
|
||||||
|
})
|
||||||
|
const dynamicCodeSuccess = computed(() => {
|
||||||
|
if (!dynamicCodeValue.value) return ''
|
||||||
|
return isDynamicCodeValid.value ? 'Code valide' : ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const maskOptions = {
|
||||||
|
mask: '@@-###-@@',
|
||||||
|
tokens: {
|
||||||
|
'@': {
|
||||||
|
pattern: /[A-Za-z]/,
|
||||||
|
transform: (char: string) => char.toUpperCase()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -1,11 +1,107 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="p-6 space-y-4">
|
<div class="flex min-h-screen">
|
||||||
<MalioInput v-model="v" label="Email" placeholder="you@example.com" />
|
<aside class="w-72 bg-m-bg p-6 text-white">
|
||||||
<pre class="text-xs">{{ v }}</pre>
|
<button
|
||||||
|
type="button"
|
||||||
|
class="text-xl text-black font-semibold"
|
||||||
|
@click="clearSelection"
|
||||||
|
>
|
||||||
|
Liste des composants
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<nav class="mt-6 flex flex-col gap-2">
|
||||||
|
<button
|
||||||
|
v-for="item in items"
|
||||||
|
:key="item.name"
|
||||||
|
type="button"
|
||||||
|
class="rounded px-3 py-2 text-left text-black font-bold hover:bg-primary-500 hover:text-white"
|
||||||
|
:class="selectedName === item.name ? 'bg-secondary-500 text-white ' : ''"
|
||||||
|
@click="selectOrToggle(item.name)"
|
||||||
|
>
|
||||||
|
{{ item.label }}
|
||||||
|
</button>
|
||||||
|
</nav>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<main class="flex-1 p-6">
|
||||||
|
<component
|
||||||
|
:is="selectedDemoComponent"
|
||||||
|
v-if="selectedDemoComponent"
|
||||||
|
/>
|
||||||
|
<p
|
||||||
|
v-else-if="selectedName"
|
||||||
|
class="text-gray-700"
|
||||||
|
>
|
||||||
|
Page de demo introuvable: <code>.playground/pages/composant/{{ selectedDemoFileName }}.vue</code>
|
||||||
|
</p>
|
||||||
|
<div v-else>
|
||||||
|
<h1 class="text-2xl font-semibold text-gray-900">Playground composants</h1>
|
||||||
|
<p class="mt-2 text-gray-600">
|
||||||
|
Selectionne un composant dans la liste pour afficher sa page de demo.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
const v = ref('')
|
type LoadedModule = {
|
||||||
</script>
|
default: unknown
|
||||||
|
}
|
||||||
|
|
||||||
|
type Item = {
|
||||||
|
name: string
|
||||||
|
label: string
|
||||||
|
demoComponent?: unknown
|
||||||
|
}
|
||||||
|
|
||||||
|
const componentModules = import.meta.glob('../../app/components/malio/*.vue', { eager: true }) as Record<string, LoadedModule>
|
||||||
|
const demoModules = import.meta.glob('./composant/*.vue', { eager: true }) as Record<string, LoadedModule>
|
||||||
|
|
||||||
|
const demoByName = Object.fromEntries(
|
||||||
|
Object.entries(demoModules).map(([file, mod]) => {
|
||||||
|
const name = file.split('/').pop()?.replace('.vue', '') ?? ''
|
||||||
|
return [name.toLowerCase(), mod.default]
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
const items = computed(() =>
|
||||||
|
Object.entries(componentModules).map(([file]) => {
|
||||||
|
const name = file.split('/').pop()?.replace('.vue', '') ?? ''
|
||||||
|
|
||||||
|
return {
|
||||||
|
name,
|
||||||
|
label: name,
|
||||||
|
demoComponent: demoByName[name.toLowerCase()],
|
||||||
|
}
|
||||||
|
}) as Item[],
|
||||||
|
)
|
||||||
|
|
||||||
|
const selectedName = ref('')
|
||||||
|
const hasInitializedSelection = ref(false)
|
||||||
|
|
||||||
|
watchEffect(() => {
|
||||||
|
if (!hasInitializedSelection.value && items.value.length > 0) {
|
||||||
|
selectedName.value = items.value[0].name
|
||||||
|
hasInitializedSelection.value = true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
function selectOrToggle(name: string) {
|
||||||
|
selectedName.value = selectedName.value === name ? '' : name
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearSelection() {
|
||||||
|
selectedName.value = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectedDemoComponent = computed(() =>
|
||||||
|
items.value.find((item) => item.name === selectedName.value)?.demoComponent,
|
||||||
|
)
|
||||||
|
|
||||||
|
const selectedDemoFileName = computed(() => {
|
||||||
|
const name = selectedName.value
|
||||||
|
if (!name) return ''
|
||||||
|
return name.charAt(0).toLowerCase() + name.slice(1)
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|||||||
13
CHANGELOG.md
Normal file
13
CHANGELOG.md
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
Liste des évolutions de la librairie Malio layer UI
|
||||||
|
|
||||||
|
## [0.0.0]
|
||||||
|
### Parameters
|
||||||
|
|
||||||
|
### Added
|
||||||
|
* [#333] Création d'un composant text
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
### Fixed
|
||||||
55
README.md
55
README.md
@@ -55,26 +55,34 @@ Prévisualiser le build :
|
|||||||
npm run preview
|
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
|
```bash
|
||||||
npm pack --dry-run
|
npm pack --dry-run
|
||||||
```
|
```
|
||||||
|
|
||||||
Publier sur le registry NPM configuré :
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm publish
|
|
||||||
```
|
|
||||||
|
|
||||||
Publier explicitement sur un registry Gitea :
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm publish --registry https://<gitea-host>/api/packages/<owner>/npm/
|
|
||||||
```
|
|
||||||
|
|
||||||
## Tester un composant dans le playground
|
## Tester un composant dans le playground
|
||||||
|
|
||||||
Le playground étend déjà le layer via `.playground/nuxt.config.ts`.
|
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
|
## 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
|
```bash
|
||||||
npm install @malio/layer-ui
|
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
|
```ts
|
||||||
export default defineNuxtConfig({
|
export default defineNuxtConfig({
|
||||||
|
|||||||
19
app/assets/css/malio.css
Normal file
19
app/assets/css/malio.css
Normal file
@@ -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 */
|
||||||
|
}
|
||||||
|
}
|
||||||
319
app/components/malio/Input.test.ts
Normal file
319
app/components/malio/Input.test.ts
Normal file
@@ -0,0 +1,319 @@
|
|||||||
|
import {describe, expect, it} from 'vitest'
|
||||||
|
import {config, mount} from '@vue/test-utils'
|
||||||
|
import type {DefineComponent} from 'vue'
|
||||||
|
import Input from './InputText.vue'
|
||||||
|
|
||||||
|
|
||||||
|
type InputProps = {
|
||||||
|
id?: string
|
||||||
|
label?: string
|
||||||
|
name?: string
|
||||||
|
autocomplete?: string
|
||||||
|
modelValue?: string | null
|
||||||
|
textSize?: string
|
||||||
|
labelClass?: string
|
||||||
|
required?: boolean
|
||||||
|
maxLength?: number | string
|
||||||
|
minLength?: number | string
|
||||||
|
disabled?: boolean
|
||||||
|
readonly?: boolean
|
||||||
|
hint?: string
|
||||||
|
error?: string
|
||||||
|
success?: string
|
||||||
|
iconName?: string
|
||||||
|
iconSize?: string | number
|
||||||
|
iconColor?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const InputForTest = Input as DefineComponent<InputProps>
|
||||||
|
const iconStub = {
|
||||||
|
template: '<span data-test="icon" v-bind="$attrs" />',
|
||||||
|
}
|
||||||
|
config.global.stubs = {
|
||||||
|
...(config.global.stubs ?? {}),
|
||||||
|
Icon: iconStub,
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('MalioInput', () => {
|
||||||
|
// Props de base: valeur, label, name, id, autocomplete
|
||||||
|
it('renders the initial input value', () => {
|
||||||
|
const wrapper = mount(InputForTest, {
|
||||||
|
props: {modelValue: 'initialValueTest'},
|
||||||
|
})
|
||||||
|
expect(wrapper.get('input').element.value).toBe('initialValueTest')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders the label text', () => {
|
||||||
|
const wrapper = mount(InputForTest, {
|
||||||
|
props: {label: 'labelTest'},
|
||||||
|
})
|
||||||
|
expect(wrapper.get('label').text()).toBe('labelTest')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('applies the name attribute', () => {
|
||||||
|
const wrapper = mount(InputForTest, {
|
||||||
|
props: {name: 'nameTest'},
|
||||||
|
})
|
||||||
|
expect(wrapper.get('input').attributes('name')).toBe('nameTest')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('uses provided id on input and label', () => {
|
||||||
|
const wrapper = mount(InputForTest, {
|
||||||
|
props: {id: 'custom-id', label: 'Label'},
|
||||||
|
})
|
||||||
|
expect(wrapper.get('input').attributes('id')).toBe('custom-id')
|
||||||
|
expect(wrapper.get('label').attributes('for')).toBe('custom-id')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('applies a different size of rounded', () => {
|
||||||
|
const wrapper = mount(InputForTest, {
|
||||||
|
props: {rounded: 'rounded-md'},
|
||||||
|
})
|
||||||
|
expect(wrapper.get('input').classes()).toContain('rounded-md')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('generates an id when missing and reuses it on label', () => {
|
||||||
|
const wrapper = mount(InputForTest, {
|
||||||
|
props: {label: 'Label'},
|
||||||
|
})
|
||||||
|
const inputId = wrapper.get('input').attributes('id')
|
||||||
|
expect(inputId).toBeDefined()
|
||||||
|
expect(inputId?.startsWith('malio-input-text-')).toBe(true)
|
||||||
|
expect(wrapper.get('label').attributes('for')).toBe(inputId)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('applies the autocomplete attribute', () => {
|
||||||
|
const wrapper = mount(InputForTest, {
|
||||||
|
props: {autocomplete: 'autocompleteTest'},
|
||||||
|
})
|
||||||
|
expect(wrapper.get('input').attributes('autocomplete')).toBe('autocompleteTest')
|
||||||
|
})
|
||||||
|
|
||||||
|
// États HTML: required, readonly, disabled
|
||||||
|
it('does not set required when false', () => {
|
||||||
|
const wrapper = mount(InputForTest, {
|
||||||
|
props: {required: false},
|
||||||
|
})
|
||||||
|
expect(wrapper.get('input').attributes('required')).toBeUndefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('sets required when true', () => {
|
||||||
|
const wrapper = mount(InputForTest, {
|
||||||
|
props: {required: true},
|
||||||
|
})
|
||||||
|
expect(wrapper.get('input').attributes('required')).toBeDefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('does not set readonly when false', () => {
|
||||||
|
const wrapper = mount(InputForTest, {
|
||||||
|
props: {readonly: false},
|
||||||
|
})
|
||||||
|
expect(wrapper.get('input').attributes('readonly')).toBeUndefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('sets readonly when true', () => {
|
||||||
|
const wrapper = mount(InputForTest, {
|
||||||
|
props: {readonly: true},
|
||||||
|
})
|
||||||
|
expect(wrapper.get('input').attributes('readonly')).toBeDefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('does not set disabled and keeps text cursor when false', () => {
|
||||||
|
const wrapper = mount(InputForTest, {
|
||||||
|
props: {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 = mount(InputForTest, {
|
||||||
|
props: {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')
|
||||||
|
})
|
||||||
|
|
||||||
|
// Émission d'événements
|
||||||
|
it('emits update:modelValue on input change', async () => {
|
||||||
|
const wrapper = mount(InputForTest, {
|
||||||
|
props: {modelValue: ''},
|
||||||
|
})
|
||||||
|
await wrapper.get('input').setValue('new value')
|
||||||
|
|
||||||
|
expect(wrapper.emitted('update:modelValue')?.[0]).toEqual(['new value'])
|
||||||
|
})
|
||||||
|
|
||||||
|
// Contraintes et classes de texte
|
||||||
|
it('applies maxLength to input', () => {
|
||||||
|
const wrapper = mount(InputForTest, {
|
||||||
|
props: {maxLength: 25},
|
||||||
|
})
|
||||||
|
expect(wrapper.get('input').attributes('maxlength')).toBe('25')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('applies minLength to input', () => {
|
||||||
|
const wrapper = mount(InputForTest, {
|
||||||
|
props: {minLength: 25},
|
||||||
|
})
|
||||||
|
expect(wrapper.get('input').attributes('minlength')).toBe('25')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('applies textSize class on label', () => {
|
||||||
|
const wrapper = mount(InputForTest, {
|
||||||
|
props: {label: 'Label', textLabel: 'text-sm'},
|
||||||
|
})
|
||||||
|
expect(wrapper.get('label').classes()).toContain('text-sm')
|
||||||
|
})
|
||||||
|
|
||||||
|
// États visuels: erreur et succès
|
||||||
|
it('shows error message without label and icon', () => {
|
||||||
|
const wrapper = mount(InputForTest, {
|
||||||
|
props: {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('p').classes()).toContain('text-m-error')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('shows error message with label and without icon', () => {
|
||||||
|
const wrapper = mount(InputForTest, {
|
||||||
|
props: {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('label').classes()).toContain('text-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 = mount(InputForTest, {
|
||||||
|
props: {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('label').classes()).toContain('text-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 = mount(InputForTest, {
|
||||||
|
props: {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 = mount(InputForTest, {
|
||||||
|
props: {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 = mount(InputForTest, {
|
||||||
|
props: {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')
|
||||||
|
expect(wrapper.get('label').classes()).toContain('text-m-success')
|
||||||
|
expect(wrapper.get('label').classes()).toContain('text-m-success')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('shows success message with label and icon', () => {
|
||||||
|
const wrapper = mount(InputForTest, {
|
||||||
|
props: {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('label').classes()).toContain('text-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 = mount(InputForTest, {
|
||||||
|
props: {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 = mount(InputForTest, {
|
||||||
|
props: {
|
||||||
|
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')
|
||||||
|
})
|
||||||
|
|
||||||
|
// Aide et classes de label
|
||||||
|
it('shows hint message', () => {
|
||||||
|
const wrapper = mount(InputForTest, {
|
||||||
|
props: {hint: 'Hint message test'},
|
||||||
|
})
|
||||||
|
expect(wrapper.get('p.text-m-muted').text()).toBe('Hint message test')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('applies labelClass on label', () => {
|
||||||
|
const wrapper = mount(InputForTest, {
|
||||||
|
props: {label: 'Label', labelClass: 'text-red-500'},
|
||||||
|
})
|
||||||
|
expect(wrapper.get('label').classes()).toContain('text-red-500')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('does not render label when label prop is missing', () => {
|
||||||
|
const wrapper = mount(InputForTest, {
|
||||||
|
props: {labelClass: 'text-red-500'},
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(wrapper.find('label').exists()).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Icône : rendu et options
|
||||||
|
it('renders icon with default positioning and muted color', () => {
|
||||||
|
const wrapper = mount(InputForTest, {
|
||||||
|
props: {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('passes icon size prop to icon component', () => {
|
||||||
|
const wrapper = mount(InputForTest, {
|
||||||
|
props: {iconName: 'mdi:key-outline', iconSize: 'text-2xl'},
|
||||||
|
})
|
||||||
|
expect(wrapper.get('[data-test="icon"]').attributes('size')).toBe('text-2xl')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('applies icon color class', () => {
|
||||||
|
const wrapper = mount(InputForTest, {
|
||||||
|
props: {iconName: 'mdi:key-outline', iconColor: 'text-m-primary'},
|
||||||
|
})
|
||||||
|
expect(wrapper.get('[data-test="icon"]').classes()).toContain('text-m-primary')
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="space-y-1">
|
|
||||||
<label v-if="label" :for="id" class="text-sm font-medium text-gray-700">{{ label }}</label>
|
|
||||||
<input
|
|
||||||
:id="id"
|
|
||||||
:value="props.modelValue"
|
|
||||||
:type="props.type"
|
|
||||||
:placeholder="props.placeholder"
|
|
||||||
class="w-full rounded-md border border-gray-300 px-3 py-2 text-sm outline-none transition focus:border-gray-500"
|
|
||||||
@input="onInput"
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
const props = withDefaults(defineProps<{
|
|
||||||
modelValue?: string
|
|
||||||
type?: string
|
|
||||||
label?: string
|
|
||||||
placeholder?: string
|
|
||||||
id?: string
|
|
||||||
}>(), {
|
|
||||||
modelValue: '',
|
|
||||||
type: 'text',
|
|
||||||
label: '',
|
|
||||||
placeholder: '',
|
|
||||||
id: undefined,
|
|
||||||
})
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
(e: 'update:modelValue', value: string): void
|
|
||||||
}>()
|
|
||||||
|
|
||||||
function onInput(event: Event) {
|
|
||||||
const target = event.target as HTMLInputElement
|
|
||||||
emit('update:modelValue', target.value)
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
204
app/components/malio/InputText.vue
Normal file
204
app/components/malio/InputText.vue
Normal file
@@ -0,0 +1,204 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="relative mt-4 flex h-12 w-full items-center"
|
||||||
|
:class="[minWidth, maxWidth]"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
:id="inputId"
|
||||||
|
v-maska="mask"
|
||||||
|
:name="name"
|
||||||
|
:autocomplete="autocomplete"
|
||||||
|
class="floating-input grow-height peer min-h-[40px] w-full border bg-white pl-3 pr-3 py-1 outline-none border-m-muted focus:border-2"
|
||||||
|
:class="[
|
||||||
|
disabled ? 'cursor-not-allowed text-black/60 [&:not(:placeholder-shown)]:border-m-muted border-m-muted' : 'cursor-text',
|
||||||
|
hasError
|
||||||
|
? 'border-m-error focus:border-m-error focus:pl-[11px] [&:not(:placeholder-shown)]:border-m-error'
|
||||||
|
: hasSuccess
|
||||||
|
? 'border-m-success focus:border-m-success focus:pl-[11px] [&:not(:placeholder-shown)]:border-m-success'
|
||||||
|
: 'border-m-border focus:border-m-primary focus:pl-[11px]',
|
||||||
|
textInput,
|
||||||
|
iconInputPaddingClass,
|
||||||
|
inputClass,
|
||||||
|
rounded,
|
||||||
|
]"
|
||||||
|
:required="required"
|
||||||
|
:maxlength="maxLength"
|
||||||
|
:minlength="minLength"
|
||||||
|
:disabled="disabled"
|
||||||
|
:value="modelValue ?? ''"
|
||||||
|
:readonly="readonly"
|
||||||
|
:aria-invalid="!!error"
|
||||||
|
:aria-describedby="describedBy"
|
||||||
|
v-bind="attrs"
|
||||||
|
placeholder=" "
|
||||||
|
type="text"
|
||||||
|
@input="onInput"
|
||||||
|
>
|
||||||
|
|
||||||
|
<label
|
||||||
|
v-if="label"
|
||||||
|
:for="inputId"
|
||||||
|
class="floating-label absolute left-3 top-2 mt-1 origin-left transition-transform duration-150 font-medium"
|
||||||
|
:class="[
|
||||||
|
disabled ? 'peer-[&:not(:placeholder-shown):not(:focus)]:text-black/60' : '',
|
||||||
|
hasError
|
||||||
|
? 'text-m-error'
|
||||||
|
: hasSuccess
|
||||||
|
? 'text-m-success'
|
||||||
|
: 'peer-placeholder-shown:text-m-muted peer-[&:not(:placeholder-shown):not(:focus)]:text-black peer-focus:text-m-primary',
|
||||||
|
labelClass,
|
||||||
|
textLabel,
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
{{ label }}
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<Icon
|
||||||
|
v-if="iconName"
|
||||||
|
:name="iconName"
|
||||||
|
:size="iconSize"
|
||||||
|
:class="[
|
||||||
|
hasError
|
||||||
|
? 'text-m-error'
|
||||||
|
: hasSuccess
|
||||||
|
? 'text-m-success'
|
||||||
|
: 'text-m-muted',
|
||||||
|
'pointer-events-none absolute right-2 top-1/2 -translate-y-1/2',
|
||||||
|
iconColor,
|
||||||
|
]"
|
||||||
|
/>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<p
|
||||||
|
v-if="hint || hasError || hasSuccess"
|
||||||
|
:id="`${inputId}-describedby`"
|
||||||
|
:class="[
|
||||||
|
hasError
|
||||||
|
? 'text-m-error'
|
||||||
|
: hasSuccess
|
||||||
|
? 'text-m-success'
|
||||||
|
: 'text-m-muted',
|
||||||
|
'mt-1 text-xs ml-[2px] ',
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
{{ hint || error || successMessage }}
|
||||||
|
</p>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type {MaskInputOptions} from 'maska'
|
||||||
|
import {vMaska} from 'maska/vue'
|
||||||
|
import {computed, useAttrs, useId} from 'vue'
|
||||||
|
|
||||||
|
defineOptions({inheritAttrs: false})
|
||||||
|
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
id?: string
|
||||||
|
label?: string
|
||||||
|
name?: string
|
||||||
|
autocomplete?: string
|
||||||
|
modelValue?: string | null | undefined
|
||||||
|
minWidth?: string
|
||||||
|
maxWidth?: string
|
||||||
|
textInput?: string
|
||||||
|
textLabel?: string
|
||||||
|
inputClass?: string
|
||||||
|
labelClass?: string
|
||||||
|
required?: boolean
|
||||||
|
maxLength?: number | string
|
||||||
|
minLength?: number | string
|
||||||
|
disabled?: boolean
|
||||||
|
readonly?: boolean
|
||||||
|
hint?: string
|
||||||
|
error?: string
|
||||||
|
success?: string
|
||||||
|
succes?: string
|
||||||
|
iconName?: string
|
||||||
|
rounded?: string
|
||||||
|
iconSize?: string | number
|
||||||
|
iconColor?: string
|
||||||
|
mask?: string | MaskInputOptions
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
id: '',
|
||||||
|
name: '',
|
||||||
|
autocomplete: '',
|
||||||
|
modelValue: undefined,
|
||||||
|
iconName: '',
|
||||||
|
label: '',
|
||||||
|
minWidth: 'w-96',
|
||||||
|
maxWidth: '',
|
||||||
|
inputClass: '',
|
||||||
|
labelClass: '',
|
||||||
|
textInput: 'text-lg',
|
||||||
|
required: false,
|
||||||
|
maxLength: undefined,
|
||||||
|
minLength: undefined,
|
||||||
|
readonly: false,
|
||||||
|
textLabel: 'text-sml',
|
||||||
|
disabled: false,
|
||||||
|
rounded: 'rounded-md',
|
||||||
|
hint: '',
|
||||||
|
error: '',
|
||||||
|
success: '',
|
||||||
|
succes: '',
|
||||||
|
iconSize: 24,
|
||||||
|
iconColor: '',
|
||||||
|
mask: undefined,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
const attrs = useAttrs()
|
||||||
|
const generatedId = useId()
|
||||||
|
|
||||||
|
const inputId = computed(() => props.id?.toString() || `malio-input-text-${generatedId}`)
|
||||||
|
const successMessage = computed(() => props.success || props.succes || '')
|
||||||
|
const hasError = computed(() => !!props.error)
|
||||||
|
const hasSuccess = computed(() => !!successMessage.value)
|
||||||
|
|
||||||
|
const describedBy = computed(() => {
|
||||||
|
const ids: string[] = []
|
||||||
|
if (props.hint && !hasSuccess.value && !hasError.value) ids.push(`${inputId.value}-hint`)
|
||||||
|
if (hasError.value) ids.push(`${inputId.value}-error`)
|
||||||
|
if (hasSuccess.value && !hasError.value) ids.push(`${inputId.value}-success`)
|
||||||
|
return ids.length ? ids.join(' ') : undefined
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(event: 'update:modelValue', value: string): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const onInput = (event: Event) => {
|
||||||
|
const target = event.target as HTMLInputElement
|
||||||
|
emit('update:modelValue', target.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
const iconInputPaddingClass = computed(() => {
|
||||||
|
return props.iconName ? 'pr-10' : ''
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.floating-input:focus + label,
|
||||||
|
.floating-input:not(:placeholder-shown) + label {
|
||||||
|
transform: translateY(-1.15rem) scale(0.9);
|
||||||
|
}
|
||||||
|
|
||||||
|
.floating-label {
|
||||||
|
background: white;
|
||||||
|
padding: 0 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grow-height {
|
||||||
|
transition: border-color 160ms ease, box-shadow 160ms ease, padding-top 160ms ease, padding-bottom 160ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grow-height:focus {
|
||||||
|
padding-top: 0.625rem;
|
||||||
|
padding-bottom: 0.625rem;
|
||||||
|
}
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.grow-height { transition: none; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
31
commit-msg
Normal file
31
commit-msg
Normal file
@@ -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 : <type>(<scope optionnel>) : <message>"
|
||||||
|
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
|
||||||
30
makefile
Normal file
30
makefile
Normal file
@@ -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'
|
||||||
@@ -1,9 +1,34 @@
|
|||||||
import { fileURLToPath } from 'node:url'
|
import { fileURLToPath } from 'node:url'
|
||||||
import { dirname, join } from 'node:path'
|
import { dirname, join } from 'node:path'
|
||||||
|
|
||||||
const currentDir = dirname(fileURLToPath(import.meta.url))
|
const dir = dirname(fileURLToPath(import.meta.url))
|
||||||
|
|
||||||
export default defineNuxtConfig({
|
export default defineNuxtConfig({
|
||||||
modules: ['@nuxtjs/tailwindcss'],
|
modules: ['@nuxtjs/tailwindcss','@nuxt/icon'],
|
||||||
css: [join(currentDir, './app/assets/css/tailwind.css')],
|
css: [join(dir, 'app/assets/css/malio.css')],
|
||||||
|
|
||||||
|
tailwindcss: {
|
||||||
|
config: {
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
borderRadius: {
|
||||||
|
malio: 'var(--m-radius)',
|
||||||
|
},
|
||||||
|
colors: {
|
||||||
|
m: {
|
||||||
|
primary: 'rgb(var(--m-primary) / <alpha-value>)',
|
||||||
|
secondary: 'rgb(var(--m-secondary) / <alpha-value>)',
|
||||||
|
tertiary: 'rgb(var(--m-tertiary) / <alpha-value>)',
|
||||||
|
border: 'rgb(var(--m-border) / <alpha-value>)',
|
||||||
|
text: 'rgb(var(--m-text) / <alpha-value>)',
|
||||||
|
muted: 'rgb(var(--m-muted) / <alpha-value>)',
|
||||||
|
bg: 'rgb(var(--m-bg) / <alpha-value>)',
|
||||||
|
error: 'rgb(var(--m-error) / <alpha-value>)',
|
||||||
|
success: 'rgb(var(--m-success) / <alpha-value>)',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
1350
package-lock.json
generated
1350
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
17
package.json
17
package.json
@@ -3,27 +3,38 @@
|
|||||||
"type": "module",
|
"type": "module",
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"main": "./nuxt.config.ts",
|
"main": "./nuxt.config.ts",
|
||||||
"files": ["app/**", "nuxt.config.ts", "README.md"],
|
"files": [
|
||||||
|
"app/**",
|
||||||
|
"nuxt.config.ts",
|
||||||
|
"README.md"
|
||||||
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "nuxi dev .playground",
|
"dev": "nuxi dev .playground",
|
||||||
"dev:prepare": "nuxt prepare .playground",
|
"dev:prepare": "nuxt prepare .playground",
|
||||||
"build": "nuxt build .playground",
|
"build": "nuxt build .playground",
|
||||||
"generate": "nuxt generate .playground",
|
"generate": "nuxt generate .playground",
|
||||||
"preview": "nuxt preview .playground",
|
"preview": "nuxt preview .playground",
|
||||||
"lint": "eslint ."
|
"lint": "eslint .",
|
||||||
|
"test": "vitest run"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"nuxt": "^4.0.0"
|
"nuxt": "^4.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@vitejs/plugin-vue": "^6.0.1",
|
||||||
|
"@vue/test-utils": "^2.4.6",
|
||||||
"@nuxt/eslint": "latest",
|
"@nuxt/eslint": "latest",
|
||||||
"@types/node": "^24.10.13",
|
"@types/node": "^24.10.13",
|
||||||
"eslint": "^10.0.0",
|
"eslint": "^10.0.0",
|
||||||
|
"jsdom": "^27.0.1",
|
||||||
"nuxt": "^4.3.1",
|
"nuxt": "^4.3.1",
|
||||||
"typescript": "^5.9.3",
|
"typescript": "^5.9.3",
|
||||||
|
"vitest": "^3.2.4",
|
||||||
"vue": "latest"
|
"vue": "latest"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nuxtjs/tailwindcss": "^6.14.0"
|
"@nuxt/icon": "^2.2.1",
|
||||||
|
"@nuxtjs/tailwindcss": "^6.14.0",
|
||||||
|
"maska": "^3.2.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
28
pre-commit
Normal file
28
pre-commit
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "######### Pre-commit hook start #############"
|
||||||
|
|
||||||
|
# Prefer the exact Node version from .nvmrc for hooks (IDE + CLI consistency).
|
||||||
|
if [ -f ".nvmrc" ]; then
|
||||||
|
NVM_VERSION="$(tr -d '\r\n' < .nvmrc)"
|
||||||
|
NVM_VERSION="${NVM_VERSION#v}"
|
||||||
|
NVM_BIN="$HOME/.nvm/versions/node/v$NVM_VERSION/bin"
|
||||||
|
if [ -x "$NVM_BIN/node" ] && [ -x "$NVM_BIN/npm" ]; then
|
||||||
|
PATH="$NVM_BIN:$PATH"
|
||||||
|
export PATH
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! command -v npm >/dev/null 2>&1; then
|
||||||
|
echo "npm introuvable dans le hook. Abandon du commit."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Node $(node -v) / npm $(npm -v)"
|
||||||
|
echo "--- make pre-commit start ---"
|
||||||
|
make pre-commit
|
||||||
|
echo "--- make pre-commit finished ---"
|
||||||
|
|
||||||
|
echo "All checks passed. Proceeding with commit."
|
||||||
|
exit 0
|
||||||
22
tailwind.config.ts
Normal file
22
tailwind.config.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import type {Config} from 'tailwindcss'
|
||||||
|
|
||||||
|
export default <Partial<Config>>{
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
fontFamily: {
|
||||||
|
sans: ['"Helvetica Neue"', 'Helvetica', 'Arial', 'sans-serif']
|
||||||
|
},
|
||||||
|
colors: {
|
||||||
|
primary: {
|
||||||
|
500: '#222783',
|
||||||
|
},
|
||||||
|
secondary: {
|
||||||
|
500: '#304998'
|
||||||
|
},
|
||||||
|
5: {
|
||||||
|
500: '#F3F4F8'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
10
vitest.config.ts
Normal file
10
vitest.config.ts
Normal file
@@ -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'],
|
||||||
|
},
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user