diff --git a/.playground/pages/composant/button/button.vue b/.playground/pages/composant/button/button.vue
new file mode 100644
index 0000000..76c4edc
--- /dev/null
+++ b/.playground/pages/composant/button/button.vue
@@ -0,0 +1,112 @@
+
+
+
+
+
Primary
+
+
Default
+
Default
+
+
+
Hover
+
Hover
+
+
+
Active
+
Active
+
+
+
Disabled
+
Disabled
+
+
+
+
+
+
+
Secondary
+
+
Default
+
Default + icon
+
+
+
Hover
+
Hover + icon
+
+
+
Active
+
Active + icon
+
+
+
Disabled
+
Disabled + icon
+
+
+
+
+
+
+
Tertiary
+
+
Default
+
Default
+
+
+
Hover
+
Hover
+
+
+
Active
+
Active
+
+
+
Disabled
+
Disabled
+
+
+
+
+
+
+
Danger
+
+
Default
+
Default
+
+
+
Hover
+
Hover
+
+
+
Active
+
Active
+
+
+
Disabled
+
Disabled
+
+
+
+
+
+
+
Avec icône
+
+
+
+
+
+
+
+
+
+
+
Largeur personnalisée
+
+
+
+
+
+
+
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6b6191a..309707e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -21,6 +21,7 @@ Liste des évolutions de la librairie Malio layer UI
* [#MUI-11] Création d'un composant navigation par onglets
* [#MUI-20] Création d'un composant sidebar
* [#MUI-23] Revoir la config couleur tailwind
+* [#MUI-10] Création d'un composant bouton
### Changed
diff --git a/app/components/malio/button/Button.test.ts b/app/components/malio/button/Button.test.ts
new file mode 100644
index 0000000..2534828
--- /dev/null
+++ b/app/components/malio/button/Button.test.ts
@@ -0,0 +1,218 @@
+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 Button from './Button.vue'
+
+type ButtonProps = {
+ id?: string
+ label?: string
+ disabled?: boolean
+ buttonClass?: string
+ variant?: 'primary' | 'secondary' | 'tertiary' | 'danger'
+ iconName?: string
+ iconPosition?: 'left' | 'right'
+ iconSize?: string | number
+}
+
+const ButtonForTest = Button as DefineComponent
+
+const mountComponent = (props: ButtonProps = {}, slots?: Record) =>
+ mount(ButtonForTest, {
+ props,
+ slots,
+ global: {
+ stubs: {
+ IconifyIcon: {
+ template: '',
+ },
+ },
+ },
+ })
+
+describe('MalioButton', () => {
+ it('renders a button with label', () => {
+ const wrapper = mountComponent({ label: 'Valider' })
+
+ expect(wrapper.find('button').exists()).toBe(true)
+ expect(wrapper.text()).toContain('Valider')
+ })
+
+ it('renders slot content over label prop', () => {
+ const wrapper = mountComponent({ label: 'Prop' }, { default: 'Slot content' })
+
+ expect(wrapper.text()).toContain('Slot content')
+ expect(wrapper.text()).not.toContain('Prop')
+ })
+
+ it('uses provided id on button', () => {
+ const wrapper = mountComponent({ id: 'custom-id' })
+
+ 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-')).toBe(true)
+ })
+
+ it('sets type="button" on the button', () => {
+ const wrapper = mountComponent()
+
+ expect(wrapper.get('button').attributes('type')).toBe('button')
+ })
+
+ 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({ disabled: true })
+
+ await wrapper.get('button').trigger('click')
+
+ expect(wrapper.emitted('click')).toBeUndefined()
+ })
+
+ it('sets disabled attribute when disabled', () => {
+ const wrapper = mountComponent({ disabled: true })
+
+ expect(wrapper.get('button').attributes('disabled')).toBeDefined()
+ })
+
+ // --- Variant: Primary (default) ---
+
+ it('applies primary variant by default', () => {
+ const wrapper = mountComponent()
+
+ expect(wrapper.get('button').classes()).toContain('bg-m-btn-primary')
+ expect(wrapper.get('button').classes()).toContain('text-white')
+ expect(wrapper.get('button').classes()).toContain('cursor-pointer')
+ })
+
+ it('applies primary disabled styles', () => {
+ const wrapper = mountComponent({ disabled: true })
+
+ expect(wrapper.get('button').classes()).toContain('bg-m-disabled')
+ expect(wrapper.get('button').classes()).toContain('text-white')
+ expect(wrapper.get('button').classes()).toContain('cursor-not-allowed')
+ })
+
+ // --- Variant: Secondary ---
+
+ it('applies secondary variant', () => {
+ const wrapper = mountComponent({ variant: 'secondary' })
+
+ expect(wrapper.get('button').classes()).toContain('bg-m-btn-secondary')
+ expect(wrapper.get('button').classes()).toContain('text-white')
+ })
+
+ it('applies secondary disabled styles', () => {
+ const wrapper = mountComponent({ variant: 'secondary', disabled: true })
+
+ expect(wrapper.get('button').classes()).toContain('bg-m-disabled')
+ expect(wrapper.get('button').classes()).toContain('cursor-not-allowed')
+ })
+
+ // --- Variant: Tertiary ---
+
+ it('applies tertiary variant with border and no background', () => {
+ const wrapper = mountComponent({ variant: 'tertiary' })
+
+ expect(wrapper.get('button').classes()).toContain('border')
+ expect(wrapper.get('button').classes()).toContain('border-m-btn-primary')
+ expect(wrapper.get('button').classes()).toContain('text-m-btn-primary')
+ expect(wrapper.get('button').classes()).toContain('bg-transparent')
+ expect(wrapper.get('button').classes()).not.toContain('text-white')
+ })
+
+ it('applies tertiary disabled styles with border', () => {
+ const wrapper = mountComponent({ variant: 'tertiary', disabled: true })
+
+ expect(wrapper.get('button').classes()).toContain('border')
+ expect(wrapper.get('button').classes()).toContain('border-m-disabled')
+ expect(wrapper.get('button').classes()).toContain('text-m-disabled')
+ expect(wrapper.get('button').classes()).toContain('bg-transparent')
+ })
+
+ // --- Variant: Danger ---
+
+ it('applies danger variant', () => {
+ const wrapper = mountComponent({ variant: 'danger' })
+
+ expect(wrapper.get('button').classes()).toContain('bg-m-btn-danger')
+ expect(wrapper.get('button').classes()).toContain('text-white')
+ })
+
+ it('applies danger disabled styles', () => {
+ const wrapper = mountComponent({ variant: 'danger', disabled: true })
+
+ expect(wrapper.get('button').classes()).toContain('bg-m-disabled')
+ expect(wrapper.get('button').classes()).toContain('cursor-not-allowed')
+ })
+
+ // --- Sizing ---
+
+ it('applies correct dimensions', () => {
+ const wrapper = mountComponent()
+
+ expect(wrapper.get('button').classes()).toContain('w-[240px]')
+ expect(wrapper.get('button').classes()).toContain('h-[40px]')
+ })
+
+ it('applies font styles', () => {
+ const wrapper = mountComponent()
+
+ expect(wrapper.get('button').classes()).toContain('text-base')
+ expect(wrapper.get('button').classes()).toContain('font-bold')
+ })
+
+ // --- buttonClass override ---
+
+ it('applies buttonClass', () => {
+ const wrapper = mountComponent({ buttonClass: 'w-full rounded-full' })
+
+ expect(wrapper.get('button').classes()).toContain('w-full')
+ expect(wrapper.get('button').classes()).toContain('rounded-full')
+ })
+
+ // --- Icon ---
+
+ it('renders icon on the right by default', () => {
+ const wrapper = mountComponent({ iconName: 'mdi:arrow-right' })
+
+ expect(wrapper.find('[data-test="icon-right"]').exists()).toBe(true)
+ expect(wrapper.find('[data-test="icon-left"]').exists()).toBe(false)
+ })
+
+ it('renders icon on the left when specified', () => {
+ const wrapper = mountComponent({ iconName: 'mdi:arrow-left', iconPosition: 'left' })
+
+ expect(wrapper.find('[data-test="icon-left"]').exists()).toBe(true)
+ expect(wrapper.find('[data-test="icon-right"]').exists()).toBe(false)
+ })
+
+ it('does not render icon when iconName is empty', () => {
+ const wrapper = mountComponent()
+
+ expect(wrapper.find('[data-test="icon-left"]').exists()).toBe(false)
+ expect(wrapper.find('[data-test="icon-right"]').exists()).toBe(false)
+ })
+
+ it('passes icon name and size to icon component', () => {
+ const wrapper = mount(ButtonForTest, {
+ props: { iconName: 'mdi:check', iconSize: 18 },
+ })
+
+ const iconComponent = wrapper.findComponent(IconifyIcon)
+ expect(iconComponent.props('icon')).toBe('mdi:check')
+ expect(iconComponent.props('width')).toBe(18)
+ expect(iconComponent.props('height')).toBe(18)
+ })
+})
diff --git a/app/components/malio/button/Button.vue b/app/components/malio/button/Button.vue
new file mode 100644
index 0000000..c6e3205
--- /dev/null
+++ b/app/components/malio/button/Button.vue
@@ -0,0 +1,102 @@
+
+
+
+
+
diff --git a/app/components/malio/checkbox/Checkbox.vue b/app/components/malio/checkbox/Checkbox.vue
index dac4e76..f4a3a43 100644
--- a/app/components/malio/checkbox/Checkbox.vue
+++ b/app/components/malio/checkbox/Checkbox.vue
@@ -110,7 +110,7 @@ const mergedLabelClass = computed(() =>
twMerge(
'cbx text-black',
disabled.value ? 'cursor-not-allowed text-black/60' : '',
- hasError.value ? 'text-m-error' : '',
+ hasError.value ? 'text-m-danger' : '',
hasSuccess.value ? 'text-m-success' : '',
props.labelClass,
),
@@ -120,7 +120,7 @@ const mergedMessageClass = computed(() =>
twMerge(
'text-xs',
hasError.value
- ? 'text-m-error'
+ ? 'text-m-danger'
: hasSuccess.value
? 'text-m-success'
: 'text-m-muted',
@@ -200,14 +200,14 @@ const onChange = (event: Event) => {
stroke-dashoffset: 0;
}
-.inp-cbx + .cbx.text-m-error span:first-child {
- border-color: rgb(var(--m-error) / 1);
+.inp-cbx + .cbx.text-m-danger span:first-child {
+ border-color: rgb(var(--m-danger) / 1);
}
-.cbx.text-m-error span:first-child svg {
- stroke: rgb(var(--m-error) / 1);
+.cbx.text-m-danger span:first-child svg {
+ stroke: rgb(var(--m-danger) / 1);
}
-.inp-cbx:checked + .cbx.text-m-error span:first-child {
- border-color: rgb(var(--m-error) / 1);
+.inp-cbx:checked + .cbx.text-m-danger span:first-child {
+ border-color: rgb(var(--m-danger) / 1);
}
.inp-cbx + .cbx.text-m-success span:first-child {
diff --git a/app/components/malio/input/InputAmount.vue b/app/components/malio/input/InputAmount.vue
index 2dc23a7..4012ea0 100644
--- a/app/components/malio/input/InputAmount.vue
+++ b/app/components/malio/input/InputAmount.vue
@@ -40,7 +40,7 @@
data-test="icon"
:class="[
hasError
- ? 'text-m-error'
+ ? 'text-m-danger'
: hasSuccess
? 'text-m-success' : iconColor,
iconPositionClass,
@@ -53,7 +53,7 @@
:id="`${inputId}-describedby`"
:class="[
hasError
- ? 'text-m-error'
+ ? 'text-m-danger'
: hasSuccess
? 'text-m-success'
: 'text-m-muted',
@@ -143,7 +143,7 @@ const mergedInputClass = computed(() =>
isFilled.value ? 'border-black' : 'border-m-muted',
disabled.value ? 'cursor-not-allowed text-black/60 [&:not(:placeholder-shown)]:border-m-muted border-m-muted' : 'cursor-text',
hasError.value
- ? 'border-m-error focus:border-m-error [&:not(:placeholder-shown)]:border-m-error'
+ ? 'border-m-danger focus:border-m-danger [&:not(:placeholder-shown)]:border-m-danger'
: hasSuccess.value
? 'border-m-success focus:border-m-success [&:not(:placeholder-shown)]:border-m-success'
: 'focus:border-m-primary',
@@ -159,7 +159,7 @@ const mergedLabelClass = computed(() =>
shouldFloatLabel.value ? '-translate-y-[1.25rem] peer-focus:-translate-y-[1.55rem] scale-90' : '',
disabled.value ? 'peer-[&:not(:placeholder-shown):not(:focus)]:text-black/60' : '',
hasError.value
- ? 'text-m-error'
+ ? 'text-m-danger'
: hasSuccess.value
? 'text-m-success'
: 'peer-placeholder-shown:text-m-muted peer-[&:not(:placeholder-shown):not(:focus)]:text-black peer-focus:text-m-primary',
diff --git a/app/components/malio/input/InputNumber.vue b/app/components/malio/input/InputNumber.vue
index a7cd8cb..0146d72 100644
--- a/app/components/malio/input/InputNumber.vue
+++ b/app/components/malio/input/InputNumber.vue
@@ -54,7 +54,7 @@
:id="`${inputId}-describedby`"
:class="[
hasError
- ? 'text-m-error'
+ ? 'text-m-danger'
: hasSuccess
? 'text-m-success'
: 'text-m-muted',
@@ -180,7 +180,7 @@ const mergedInputClass = computed(() =>
' peer h-[22px] min-w-0 border bg-white text-center outline-none placeholder:text-transparent text-lg border-x-0 border-black',
props.disabled ? 'cursor-not-allowed text-black/60' : 'cursor-text',
hasError.value
- ? 'border-m-error focus:border-m-error [&:not(:placeholder-shown)]:border-m-error'
+ ? 'border-m-danger focus:border-m-danger [&:not(:placeholder-shown)]:border-m-danger'
: hasSuccess.value
? 'border-m-success focus:border-m-success [&:not(:placeholder-shown)]:border-m-success'
: '',
@@ -191,7 +191,7 @@ const mergedInputClass = computed(() =>
const mergedLabelClass = computed(() =>
twMerge(
'cursor-pointer text-black mr-4 text-[18px]',
- hasError.value ? 'text-m-error' : '',
+ hasError.value ? 'text-m-danger' : '',
hasSuccess.value ? 'text-m-success' : '',
props.disabled ? 'cursor-not-allowed text-black/60' : '',
props.labelClass,
@@ -203,7 +203,7 @@ const mergedButtonMinusClass = computed(() =>
'h-[22px] w-[40px] border border-black rounded-s-[3px]',
isMinusDisabled.value ? 'cursor-not-allowed text-black/60' : 'cursor-pointer',
hasError.value
- ? 'border-m-error'
+ ? 'border-m-danger'
: hasSuccess.value
? 'border-m-success'
: '',
@@ -215,7 +215,7 @@ const mergedButtonPlusClass = computed(() =>
'h-[22px] w-[40px] border border-black rounded-e-[3px]',
isPlusDisabled.value ? 'cursor-not-allowed text-black/60' : 'cursor-pointer',
hasError.value
- ? 'border-m-error'
+ ? 'border-m-danger'
: hasSuccess.value
? 'border-m-success'
: '',
diff --git a/app/components/malio/input/InputPassword.vue b/app/components/malio/input/InputPassword.vue
index 4c12513..1f90287 100644
--- a/app/components/malio/input/InputPassword.vue
+++ b/app/components/malio/input/InputPassword.vue
@@ -39,7 +39,7 @@
data-test="icon"
:class="[
hasError
- ? 'text-m-error'
+ ? 'text-m-danger'
: hasSuccess
? 'text-m-success' : 'text-m-muted',
'cursor-pointer absolute right-[10px] top-1/2 -translate-y-1/2',
@@ -53,7 +53,7 @@
:id="`${inputId}-describedby`"
:class="[
hasError
- ? 'text-m-error'
+ ? 'text-m-danger'
: hasSuccess
? 'text-m-success'
: 'text-m-muted',
@@ -142,7 +142,7 @@ const mergedInputClass = computed(() =>
isFilled.value ? 'border-black' : 'border-m-muted',
disabled.value ? 'cursor-not-allowed text-black/60 [&:not(:placeholder-shown)]:border-m-muted border-m-muted' : 'cursor-text',
hasError.value
- ? 'border-m-error focus:border-m-error [&:not(:placeholder-shown)]:border-m-error'
+ ? 'border-m-danger focus:border-m-danger [&:not(:placeholder-shown)]:border-m-danger'
: hasSuccess.value
? 'border-m-success focus:border-m-success [&:not(:placeholder-shown)]:border-m-success'
: 'focus:border-m-primary',
@@ -158,7 +158,7 @@ const mergedLabelClass = computed(() =>
shouldFloatLabel.value ? '-translate-y-[1.25rem] peer-focus:-translate-y-[1.55rem] scale-90' : '',
disabled.value ? 'peer-[&:not(:placeholder-shown):not(:focus)]:text-black/60' : '',
hasError.value
- ? 'text-m-error'
+ ? 'text-m-danger'
: hasSuccess.value
? 'text-m-success'
: 'peer-placeholder-shown:text-m-muted peer-[&:not(:placeholder-shown):not(:focus)]:text-black peer-focus:text-m-primary',
diff --git a/app/components/malio/input/InputTextArea.vue b/app/components/malio/input/InputTextArea.vue
index c3294f5..d8900b6 100644
--- a/app/components/malio/input/InputTextArea.vue
+++ b/app/components/malio/input/InputTextArea.vue
@@ -12,7 +12,7 @@
isFilled ? 'border-black' : 'border-m-muted',
disabled ? 'cursor-not-allowed text-black/60 border-m-muted' : 'cursor-text',
hasError
- ? 'border-m-error focus:border-m-error focus:pl-[11px]'
+ ? 'border-m-danger focus:border-m-danger focus:pl-[11px]'
: hasSuccess
? 'border-m-success focus:border-m-success focus:pl-[11px]'
: 'focus:border-m-primary focus:pl-[11px]',
@@ -43,7 +43,7 @@
shouldFloatLabel ? '-translate-y-[1.30rem] scale-90' : '',
disabled ? 'text-black/60' : '',
hasError
- ? 'text-m-error'
+ ? 'text-m-danger'
: hasSuccess
? 'text-m-success'
: isFocused ? 'text-m-primary' : shouldFloatLabel ? 'text-black' : 'text-m-muted',
@@ -67,7 +67,7 @@
:id="`${inputId}-describedby`"
:class="[
hasError
- ? 'text-m-error'
+ ? 'text-m-danger'
: hasSuccess
? 'text-m-success'
: 'text-m-muted',
diff --git a/app/components/malio/input/InputUpload.vue b/app/components/malio/input/InputUpload.vue
index e9b988d..d46b25e 100644
--- a/app/components/malio/input/InputUpload.vue
+++ b/app/components/malio/input/InputUpload.vue
@@ -43,7 +43,7 @@
data-test="icon"
:class="[
hasError
- ? 'text-m-error'
+ ? 'text-m-danger'
: hasSuccess
? 'text-m-success' : 'text-m-muted',
'pointer-events-none absolute right-[10px] top-1/2 -translate-y-1/2',
@@ -56,7 +56,7 @@
:id="`${inputId}-describedby`"
:class="[
hasError
- ? 'text-m-error'
+ ? 'text-m-danger'
: hasSuccess
? 'text-m-success'
: 'text-m-muted',
@@ -131,7 +131,7 @@ const mergedInputClass = computed(() =>
isFilled.value ? 'border-black' : 'border-m-muted',
disabled.value ? 'cursor-not-allowed text-black/60 [&:not(:placeholder-shown)]:border-m-muted border-m-muted' : 'cursor-pointer',
hasError.value
- ? 'border-m-error focus:border-m-error [&:not(:placeholder-shown)]:border-m-error'
+ ? 'border-m-danger focus:border-m-danger [&:not(:placeholder-shown)]:border-m-danger'
: hasSuccess.value
? 'border-m-success focus:border-m-success [&:not(:placeholder-shown)]:border-m-success'
: 'focus:border-m-primary',
@@ -147,7 +147,7 @@ const mergedLabelClass = computed(() =>
shouldFloatLabel.value ? '-translate-y-[1.25rem] peer-focus:-translate-y-[1.55rem] scale-90' : '',
disabled.value ? 'peer-[&:not(:placeholder-shown):not(:focus)]:text-black/60' : '',
hasError.value
- ? 'text-m-error'
+ ? 'text-m-danger'
: hasSuccess.value
? 'text-m-success'
: 'peer-placeholder-shown:text-m-muted peer-[&:not(:placeholder-shown):not(:focus)]:text-black peer-focus:text-m-primary',
diff --git a/app/components/malio/radio/RadioButton.vue b/app/components/malio/radio/RadioButton.vue
index 06fac78..d2c457d 100644
--- a/app/components/malio/radio/RadioButton.vue
+++ b/app/components/malio/radio/RadioButton.vue
@@ -125,7 +125,7 @@ const mergedInputClass = computed(() =>
const mergedLabelClass = computed(() =>
twMerge(
'radio-text mt-px cursor-pointer text-black',
- hasError.value ? 'text-m-error' : '',
+ hasError.value ? 'text-m-danger' : '',
hasSuccess.value ? 'text-m-success' : '',
disabled.value ? 'cursor-not-allowed text-black/60' : '',
props.labelClass,
@@ -136,7 +136,7 @@ const mergedMessageClass = computed(() =>
twMerge(
'radio-message ml-3 -mt-1 text-xs',
hasError.value
- ? 'text-m-error'
+ ? 'text-m-danger'
: hasSuccess.value
? 'text-m-success'
: 'text-m-muted',
@@ -170,11 +170,11 @@ const onChange = (event: Event) => {
}
.radio-control.is-error input[type='radio'] {
- border-color: rgb(var(--m-error) / 1);
+ border-color: rgb(var(--m-danger) / 1);
}
.radio-control.is-error .radio-dot {
- color: rgb(var(--m-error) / 1);
+ color: rgb(var(--m-danger) / 1);
}
.radio-control.is-success input[type='radio'] {
diff --git a/app/components/malio/select/Select.vue b/app/components/malio/select/Select.vue
index 67a5215..a4244b9 100644
--- a/app/components/malio/select/Select.vue
+++ b/app/components/malio/select/Select.vue
@@ -12,9 +12,9 @@
hasError
? isOpen
? openDirection === 'down'
- ? 'rounded-b-none !border-2 !border-m-error !border-b-0'
- : 'rounded-t-none !border-2 !border-m-error !border-t-0'
- : 'border-m-error'
+ ? 'rounded-b-none !border-2 !border-m-danger !border-b-0'
+ : 'rounded-t-none !border-2 !border-m-danger !border-t-0'
+ : 'border-m-danger'
: hasSuccess
? isOpen
? openDirection === 'down'
@@ -46,7 +46,7 @@
:class="[
isOpen ? 'top-2 z-30' : 'top-2',
hasError
- ? 'text-m-error'
+ ? 'text-m-danger'
: hasSuccess
? 'text-m-success'
: isOpen
@@ -75,7 +75,7 @@
class="absolute right-3 top-1/2 -translate-y-1/2"
:class="[
hasError
- ? 'text-m-error'
+ ? 'text-m-danger'
: hasSuccess
? 'text-m-success'
: 'text-current'
@@ -109,7 +109,7 @@
? 'select-scrollbar-success'
: 'select-scrollbar-primary',
hasError
- ? 'border-m-error'
+ ? 'border-m-danger'
: hasSuccess
? 'border-m-success'
: 'border-m-primary'
@@ -140,7 +140,7 @@
:id="`${buttonId}-describedby`"
:class="[
hasError
- ? 'text-m-error'
+ ? 'text-m-danger'
: hasSuccess
? 'text-m-success'
: 'text-m-muted',
diff --git a/app/components/malio/select/SelectCheckbox.vue b/app/components/malio/select/SelectCheckbox.vue
index 4364e21..2f8547d 100644
--- a/app/components/malio/select/SelectCheckbox.vue
+++ b/app/components/malio/select/SelectCheckbox.vue
@@ -12,9 +12,9 @@
hasError
? isOpen
? openDirection === 'down'
- ? 'rounded-b-none !border-2 !border-m-error !border-b-0'
- : 'rounded-t-none !border-2 !border-m-error !border-t-0'
- : 'border-m-error'
+ ? 'rounded-b-none !border-2 !border-m-danger !border-b-0'
+ : 'rounded-t-none !border-2 !border-m-danger !border-t-0'
+ : 'border-m-danger'
: hasSuccess
? isOpen
? openDirection === 'down'
@@ -46,7 +46,7 @@
:class="[
shouldFloatLabel ? 'top-2 z-30' : 'top-1/2 -translate-y-1/2',
hasError
- ? 'text-m-error'
+ ? 'text-m-danger'
: hasSuccess
? 'text-m-success'
: isOpen
@@ -103,7 +103,7 @@
class="absolute right-3 top-1/2 -translate-y-1/2"
:class="[
hasError
- ? 'text-m-error'
+ ? 'text-m-danger'
: hasSuccess
? 'text-m-success'
: 'text-current'
@@ -137,7 +137,7 @@
? 'select-scrollbar-success'
: 'select-scrollbar-primary',
hasError
- ? 'border-m-error'
+ ? 'border-m-danger'
: hasSuccess
? 'border-m-success'
: 'border-m-primary'
@@ -190,7 +190,7 @@
:id="`${buttonId}-describedby`"
:class="[
hasError
- ? 'text-m-error'
+ ? 'text-m-danger'
: hasSuccess
? 'text-m-success'
: 'text-m-muted',
diff --git a/app/components/malio/time/Time.vue b/app/components/malio/time/Time.vue
index 7ef04a8..61060e9 100644
--- a/app/components/malio/time/Time.vue
+++ b/app/components/malio/time/Time.vue
@@ -62,7 +62,7 @@
:id="`${inputId}-describedby`"
:class="[
hasError
- ? 'text-m-error'
+ ? 'text-m-danger'
: hasSuccess
? 'text-m-success'
: 'text-m-muted',
@@ -185,7 +185,7 @@ const mergedGroupClass = computed(() =>
const mergedLabelClass = computed(() =>
twMerge(
'mt-px mr-4 cursor-pointer text-black text-[18px]',
- hasError.value ? 'text-m-error' : '',
+ hasError.value ? 'text-m-danger' : '',
hasSuccess.value ? 'text-m-success' : '',
props.disabled ? 'cursor-not-allowed text-black/60' : '',
props.labelClass
@@ -197,7 +197,7 @@ const mergedInputClass = (field: 'hours' | 'minutes') =>
'h-[30px] w-10 border bg-white text-center text-[18px] outline-none rounded-md placeholder:text-m-muted',
props.disabled ? 'cursor-not-allowed text-black/60 border-m-muted' : 'cursor-text',
hasError.value
- ? 'focus:border-2 border-m-error focus:border-m-error'
+ ? 'focus:border-2 border-m-danger focus:border-m-danger'
: hasSuccess.value
? 'focus:border-2 border-m-success focus:border-m-success'
: activeField.value === field
diff --git a/app/story/button/button.story.vue b/app/story/button/button.story.vue
new file mode 100644
index 0000000..a1ecf04
--- /dev/null
+++ b/app/story/button/button.story.vue
@@ -0,0 +1,148 @@
+
+
+
+
+
Default
+
Default + icon
+
+
+
Hover
+
Hover + icon
+
+
+
Active
+
Active + icon
+
+
+
Disabled
+
Disabled + icon
+
+
+
+
+
+
+
+
Default
+
Default + icon
+
+
+
Hover
+
Hover + icon
+
+
+
Active
+
Active + icon
+
+
+
Disabled
+
Disabled + icon
+
+
+
+
+
+
+
+
Default
+
Default + icon
+
+
+
Hover
+
Hover + icon
+
+
+
Active
+
Active + icon
+
+
+
Disabled
+
Disabled + icon
+
+
+
+
+
+
+
+
Default
+
Default + icon
+
+
+
Hover
+
Hover + icon
+
+
+
Active
+
Active + icon
+
+
+
Disabled
+
Disabled + icon
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+# MalioButton
+
+Bouton d'action avec 4 variantes visuelles et support d'icône optionnelle.
+
+## Props
+
+| Prop | Type | Défaut | Description |
+|------|------|--------|-------------|
+| `id` | `string` | auto-généré | Identifiant HTML du bouton |
+| `label` | `string` | `''` | Texte du bouton (peut aussi être fourni via le slot par défaut) |
+| `variant` | `'primary' \| 'secondary' \| 'tertiary' \| 'danger'` | `'primary'` | Variante visuelle |
+| `disabled` | `boolean` | `false` | Désactive le bouton |
+| `buttonClass` | `string` | `''` | Classes CSS additionnelles (fusionnées via `twMerge`) |
+| `iconName` | `string` | `''` | Nom de l'icône Iconify (ex: `mdi:check`) |
+| `iconPosition` | `'left' \| 'right'` | `'right'` | Position de l'icône par rapport au texte |
+| `iconSize` | `string \| number` | `20` | Taille de l'icône en pixels |
+
+## Variantes
+
+- **Primary** : Fond `m-btn-primary`, texte blanc — action principale
+- **Secondary** : Fond `m-btn-secondary`, texte blanc — action secondaire
+- **Tertiary** : Bordure et texte `m-btn-primary`, fond transparent — action tertiaire
+- **Danger** : Fond `m-btn-danger`, texte blanc — action destructrice
+
+## États
+
+Chaque variante a 4 états visuels : Default, Hover, Active, Disabled.
+
+## Dimensions par défaut
+
+- Largeur : 240px (`w-[240px]`), personnalisable via `buttonClass`
+- Hauteur : 40px (`h-[40px]`)
+- Police : 16px bold, line-height 150%
+
+## Accessibilité
+
+- `type="button"` évite la soumission de formulaire involontaire
+- Support `disabled` natif
+- Focus visible avec `focus-visible:ring-2`
+
+## Events
+
+| Event | Payload | Description |
+|-------|---------|-------------|
+| `click` | `MouseEvent` | Émis au clic (pas émis si `disabled`) |
+
+
+
diff --git a/app/story/input/inputUpload.story.vue b/app/story/input/inputUpload.story.vue
index bc674fd..87439a5 100644
--- a/app/story/input/inputUpload.story.vue
+++ b/app/story/input/inputUpload.story.vue
@@ -180,7 +180,7 @@ et accessibilité.
### Couleur de l'icône
- `text-m-muted` par défaut.
-- `text-m-error` si la prop `error` est renseignée.
+- `text-m-danger` si la prop `error` est renseignée.
- `text-m-success` si la prop `success` est renseignée.
------------------------------------------------------------------------