diff --git a/.playground/pages/composant/buttonIcon.vue b/.playground/pages/composant/buttonIcon.vue
new file mode 100644
index 0000000..a32423a
--- /dev/null
+++ b/.playground/pages/composant/buttonIcon.vue
@@ -0,0 +1,155 @@
+
+
+
+
+
+
Icônes variées
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Taille personnalisée
+
+
+
+
+
+
+
+
+
+
+
Ghost désactivé
+
+
+
+
+
+
+
+
Avec événement click
+
+
+ {{ counter }}
+
+
+
+
+
+
+
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 67e93d2..f89f059 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -17,6 +17,7 @@ Liste des évolutions de la librairie Malio layer UI
* Création d'un composant textarea
* [#MUI-8] Création d'un composant mot de passe
* [#MUI-9] Création d'un composant upload
+* [#MUI-14] Création d'un composant bouton icône
### Changed
diff --git a/app/assets/css/malio.css b/app/assets/css/malio.css
index 0fa1352..5843a6d 100644
--- a/app/assets/css/malio.css
+++ b/app/assets/css/malio.css
@@ -15,5 +15,10 @@
--m-error: 155 17 30; /* rouge pour les erreurs */
--m-success: 15 149 70; /* vert pour les succès */
+
+ --m-btn-default: 34 39 131; /* #222783 - bouton par défaut */
+ --m-btn-hover: 18 28 219; /* #121CDB - bouton hover */
+ --m-btn-active: 33 37 103; /* #212567 - bouton active */
+ --m-btn-disabled: 204 204 223; /* #CCCCDF - bouton désactivé */
}
}
diff --git a/app/components/malio/ButtonIcon.test.ts b/app/components/malio/ButtonIcon.test.ts
new file mode 100644
index 0000000..0778727
--- /dev/null
+++ b/app/components/malio/ButtonIcon.test.ts
@@ -0,0 +1,151 @@
+import {describe, expect, it} from 'vitest'
+import {mount} from '@vue/test-utils'
+import type {DefineComponent} from 'vue'
+import {Icon as IconifyIcon} from '@iconify/vue'
+import ButtonIcon from './ButtonIcon.vue'
+
+type ButtonIconProps = {
+ id?: string
+ icon: string
+ ariaLabel: string
+ disabled?: boolean
+ buttonClass?: string
+ iconSize?: string | number
+ variant?: 'filled' | 'ghost'
+}
+
+const ButtonIconForTest = ButtonIcon as DefineComponent
+
+const mountComponent = (props: ButtonIconProps = {icon: 'mdi:arrow-left', ariaLabel: 'Retour'}) =>
+ mount(ButtonIconForTest, {
+ props,
+ global: {
+ stubs: {
+ IconifyIcon: {
+ template: '',
+ },
+ },
+ },
+ })
+
+describe('MalioButtonIcon', () => {
+ it('renders a button with the icon', () => {
+ const wrapper = mountComponent()
+
+ expect(wrapper.find('button').exists()).toBe(true)
+ expect(wrapper.find('[data-test="icon"]').exists()).toBe(true)
+ })
+
+ it('uses provided id on button', () => {
+ const wrapper = mountComponent({id: 'custom-id', icon: 'mdi:arrow-left', ariaLabel: 'Retour'})
+
+ expect(wrapper.get('button').attributes('id')).toBe('custom-id')
+ })
+
+ it('generates an id when missing', () => {
+ const wrapper = mountComponent()
+
+ const buttonId = wrapper.get('button').attributes('id')
+ expect(buttonId?.startsWith('malio-button-icon-')).toBe(true)
+ })
+
+ it('sets aria-label on button', () => {
+ const wrapper = mountComponent({icon: 'mdi:arrow-left', ariaLabel: 'Retour'})
+
+ expect(wrapper.get('button').attributes('aria-label')).toBe('Retour')
+ })
+
+ it('sets type="button" on the button', () => {
+ const wrapper = mountComponent()
+
+ expect(wrapper.get('button').attributes('type')).toBe('button')
+ })
+
+ it('passes icon name to icon component', () => {
+ const wrapper = mount(ButtonIconForTest, {
+ props: {icon: 'mdi:pencil-outline', ariaLabel: 'Modifier'},
+ })
+
+ const iconComponent = wrapper.findComponent(IconifyIcon)
+ expect(iconComponent.props('icon')).toBe('mdi:pencil-outline')
+ })
+
+ it('passes icon size to icon component', () => {
+ const wrapper = mount(ButtonIconForTest, {
+ props: {icon: 'mdi:arrow-left', ariaLabel: 'Retour', iconSize: 32},
+ })
+
+ const iconComponent = wrapper.findComponent(IconifyIcon)
+ expect(iconComponent.props('width')).toBe(32)
+ expect(iconComponent.props('height')).toBe(32)
+ })
+
+ it('emits click event when clicked', async () => {
+ const wrapper = mountComponent()
+
+ await wrapper.get('button').trigger('click')
+
+ expect(wrapper.emitted('click')).toHaveLength(1)
+ })
+
+ it('does not emit click when disabled', async () => {
+ const wrapper = mountComponent({icon: 'mdi:arrow-left', ariaLabel: 'Retour', disabled: true})
+
+ await wrapper.get('button').trigger('click')
+
+ expect(wrapper.emitted('click')).toBeUndefined()
+ })
+
+ it('sets disabled attribute when disabled', () => {
+ const wrapper = mountComponent({icon: 'mdi:arrow-left', ariaLabel: 'Retour', disabled: true})
+
+ expect(wrapper.get('button').attributes('disabled')).toBeDefined()
+ })
+
+ it('applies disabled styles when disabled', () => {
+ const wrapper = mountComponent({icon: 'mdi:arrow-left', ariaLabel: 'Retour', disabled: true})
+
+ expect(wrapper.get('button').classes()).toContain('cursor-not-allowed')
+ expect(wrapper.get('button').classes()).toContain('bg-m-btn-disabled')
+ })
+
+ it('applies cursor-pointer when not disabled', () => {
+ const wrapper = mountComponent()
+
+ expect(wrapper.get('button').classes()).toContain('cursor-pointer')
+ })
+
+ it('applies white text color for icon visibility', () => {
+ const wrapper = mountComponent()
+
+ expect(wrapper.get('button').classes()).toContain('text-white')
+ })
+
+ it('applies default background color', () => {
+ const wrapper = mountComponent()
+
+ expect(wrapper.get('button').classes()).toContain('bg-m-btn-default')
+ })
+
+ it('applies buttonClass', () => {
+ const wrapper = mountComponent({icon: 'mdi:arrow-left', ariaLabel: 'Retour', buttonClass: 'rounded-full'})
+
+ expect(wrapper.get('button').classes()).toContain('rounded-full')
+ })
+
+ it('applies ghost variant with no background and colored icon', () => {
+ const wrapper = mountComponent({icon: 'mdi:arrow-left', ariaLabel: 'Retour', variant: 'ghost'})
+
+ expect(wrapper.get('button').classes()).toContain('text-m-btn-default')
+ expect(wrapper.get('button').classes()).not.toContain('bg-m-btn-default')
+ expect(wrapper.get('button').classes()).not.toContain('text-white')
+ })
+
+ it('applies ghost disabled styles with no background', () => {
+ const wrapper = mountComponent({icon: 'mdi:arrow-left', ariaLabel: 'Retour', variant: 'ghost', disabled: true})
+
+ expect(wrapper.get('button').classes()).toContain('text-m-btn-disabled')
+ expect(wrapper.get('button').classes()).toContain('cursor-not-allowed')
+ expect(wrapper.get('button').classes()).not.toContain('bg-m-btn-disabled')
+ })
+})
diff --git a/app/components/malio/ButtonIcon.vue b/app/components/malio/ButtonIcon.vue
new file mode 100644
index 0000000..8b121d6
--- /dev/null
+++ b/app/components/malio/ButtonIcon.vue
@@ -0,0 +1,76 @@
+
+
+
+
+
diff --git a/app/story/buttonIcon.story.vue b/app/story/buttonIcon.story.vue
new file mode 100644
index 0000000..7e948a7
--- /dev/null
+++ b/app/story/buttonIcon.story.vue
@@ -0,0 +1,242 @@
+
+
+
+
+
+
+
Icônes variées
+
+
+
+
+
+
+
+
+
+
+
Désactivé
+
+
+
+
+
+
+
+
+
+
+
+
Ghost désactivé
+
+
+
+
+
+
+
+
Taille personnalisée
+
+
+
+
+
+
+
+
+
+
+
+# MalioButtonIcon
+
+Bouton contenant uniquement une icône, sans texte. Utilisé pour des actions
+rapides et compactes (retour, modifier, supprimer, etc.).
+
+------------------------------------------------------------------------
+
+## Props détaillées
+
+### id
+
+- Type: string
+- Description: Identifiant HTML du bouton.
+- Comportement: Si non fourni, un id unique est généré automatiquement.
+
+### icon
+
+- Type: string
+- **Requis**
+- Description: Nom de l'icône Iconify (ex: `mdi:arrow-left`).
+
+### ariaLabel
+
+- Type: string
+- **Requis**
+- Description: Label d'accessibilité du bouton. Obligatoire car le bouton
+ n'a pas de texte visible.
+
+### iconSize
+
+- Type: string | number
+- Défaut: 24
+- Description: Taille de l'icône en pixels.
+
+### buttonClass
+
+- Type: string
+- Description: Classes CSS additionnelles appliquées au bouton.
+
+### disabled
+
+- Type: boolean
+- Description: Désactive le bouton.
+
+### variant
+
+- Type: `'filled' | 'ghost'`
+- Défaut: `filled`
+- Description: Variante visuelle du bouton.
+- `filled` : fond coloré, icône blanche.
+- `ghost` : sans fond, icône colorée.
+
+------------------------------------------------------------------------
+
+## Comportement visuel
+
+### Variante `filled` (défaut)
+
+- **Default** : fond `#222783`, icône blanche
+- **Hover** : fond `#121CDB`
+- **Active** : fond `#212567`
+- **Disabled** : fond `#CCCCDF`
+
+### Variante `ghost`
+
+- **Default** : icône `#222783`, sans fond
+- **Hover** : icône `#121CDB`
+- **Active** : icône `#212567`
+- **Disabled** : icône `#CCCCDF`
+
+------------------------------------------------------------------------
+
+## Accessibilité
+
+- `aria-label` est requis pour décrire l'action du bouton.
+- `type="button"` pour éviter les soumissions de formulaire accidentelles.
+
+------------------------------------------------------------------------
+
+## Events
+
+### click
+
+- Émis au clic sur le bouton.
+- Non émis si le bouton est `disabled`.
+- Retourne l'événement `MouseEvent` natif.
+
+
+
+
diff --git a/tailwind.config.ts b/tailwind.config.ts
index b8521e4..4d289d1 100644
--- a/tailwind.config.ts
+++ b/tailwind.config.ts
@@ -23,6 +23,10 @@ export default {
bg: 'rgb(var(--m-bg) / )',
error: 'rgb(var(--m-error) / )',
success: 'rgb(var(--m-success) / )',
+ 'btn-default': 'rgb(var(--m-btn-default) / )',
+ 'btn-hover': 'rgb(var(--m-btn-hover) / )',
+ 'btn-active': 'rgb(var(--m-btn-active) / )',
+ 'btn-disabled': 'rgb(var(--m-btn-disabled) / )',
},
},
fontFamily: {