Files
malio-layer-ui/docs/superpowers/plans/2026-05-22-datetime.md
tristan 7d7b2fb720
All checks were successful
Release / release (push) Successful in 1m24s
feat: Développer le composant Datepicker (#52)
| 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é

Co-authored-by: matthieu <matthieu@yuno.malio.fr>
Co-authored-by: THOLOT DECHENE Matthieu <matthieu@yuno.malio.fr>
Reviewed-on: #52
Co-authored-by: tristan <tristan@yuno.malio.fr>
Co-committed-by: tristan <tristan@yuno.malio.fr>
2026-05-22 08:08:50 +00:00

23 KiB

MalioDateTime Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Ajouter un composant MalioDateTime (date + heure dans un seul champ) à la famille temporelle de @malio/layer-ui, en version intérimaire avec <input type="time"> natif.

Architecture: Fine enveloppe autour du shell internal/CalendarField.vue (comme MalioDate). Le slot popover contient MonthGrid (jour) + un <input type="time"> (heure) sous la grille. La valeur émise est l'ISO naïf "YYYY-MM-DDTHH:MM:00". Logique de découpe/recomposition dans un nouveau composable datetimeFormat.ts.

Tech Stack: Vue 3 <script setup lang="ts">, TypeScript strict, Tailwind (m-*), tailwind-merge, Vitest + @vue/test-utils (jsdom).

Conventions (rappel) : Conventional Commits avec espace avant :, type minuscule, suffixe ticket (#MUI-33), pas de préfixe [#...]. Terminer par Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>. Le hook pre-commit est flaky (timeouts WSL2) → après vérification ciblée npx vitest run <chemin>, committer avec --no-verify.

Réfs spec : docs/superpowers/specs/2026-05-22-datetime-design.md.


Task 1 : Composable datetimeFormat.ts

Files:

  • Create: app/components/malio/date/composables/datetimeFormat.ts

  • Test: app/components/malio/date/composables/datetimeFormat.test.ts

  • Step 1 : Écrire les tests qui échouent

Créer app/components/malio/date/composables/datetimeFormat.test.ts :

import {describe, expect, it} from 'vitest'
import {
  composeDateTime,
  formatIsoDateTimeToDisplay,
  isValidIsoDateTime,
  splitDateTime,
} from './datetimeFormat'

describe('datetimeFormat', () => {
  describe('isValidIsoDateTime', () => {
    it('accepte un datetime ISO complet valide', () => {
      expect(isValidIsoDateTime('2026-05-20T14:30:00')).toBe(true)
      expect(isValidIsoDateTime('2026-01-01T00:00:00')).toBe(true)
      expect(isValidIsoDateTime('2026-12-31T23:59:59')).toBe(true)
    })

    it('rejette une date seule, des composants invalides ou une chaîne vide', () => {
      expect(isValidIsoDateTime('2026-05-20')).toBe(false)
      expect(isValidIsoDateTime('2026-13-01T00:00:00')).toBe(false)
      expect(isValidIsoDateTime('2026-05-20T24:00:00')).toBe(false)
      expect(isValidIsoDateTime('2026-05-20T14:60:00')).toBe(false)
      expect(isValidIsoDateTime('2026-05-20T14:30:60')).toBe(false)
      expect(isValidIsoDateTime('2026-05-20T14:30')).toBe(false)
      expect(isValidIsoDateTime('')).toBe(false)
    })
  })

  describe('formatIsoDateTimeToDisplay', () => {
    it('formate un datetime valide en JJ/MM/AAAA HH:MM', () => {
      expect(formatIsoDateTimeToDisplay('2026-05-20T14:30:00')).toBe('20/05/2026 14:30')
    })

    it('renvoie une chaîne vide pour nul ou invalide', () => {
      expect(formatIsoDateTimeToDisplay(null)).toBe('')
      expect(formatIsoDateTimeToDisplay('2026-05-20')).toBe('')
      expect(formatIsoDateTimeToDisplay('nope')).toBe('')
    })
  })

  describe('splitDateTime', () => {
    it('découpe un datetime valide', () => {
      expect(splitDateTime('2026-05-20T14:30:00')).toEqual({date: '2026-05-20', time: '14:30'})
    })

    it('renvoie date null et time vide pour nul, date seule ou invalide', () => {
      expect(splitDateTime(null)).toEqual({date: null, time: ''})
      expect(splitDateTime('2026-05-20')).toEqual({date: null, time: ''})
      expect(splitDateTime('nope')).toEqual({date: null, time: ''})
    })
  })

  describe('composeDateTime', () => {
    it('recompose un datetime ISO avec secondes à 00', () => {
      expect(composeDateTime('2026-05-20', '14:30')).toBe('2026-05-20T14:30:00')
    })

    it('utilise 00:00 quand l\\'heure est vide', () => {
      expect(composeDateTime('2026-05-20', '')).toBe('2026-05-20T00:00:00')
    })
  })
})
  • Step 2 : Lancer les tests pour vérifier qu'ils échouent

Run: npx vitest run app/components/malio/date/composables/datetimeFormat.test.ts Expected: FAIL (module ./datetimeFormat introuvable).

  • Step 3 : Écrire l'implémentation

Créer app/components/malio/date/composables/datetimeFormat.ts :

import {isValidIso} from './dateFormat'

const DATETIME_RE = /^(\d{4}-\d{2}-\d{2})T(\d{2}):(\d{2}):(\d{2})$/

export function isValidIsoDateTime(s: string): boolean {
  const m = DATETIME_RE.exec(s)
  if (!m) return false
  const [, date, hh, mm, ss] = m
  if (!isValidIso(date)) return false
  const h = Number(hh)
  const min = Number(mm)
  const sec = Number(ss)
  return h >= 0 && h <= 23 && min >= 0 && min <= 59 && sec >= 0 && sec <= 59
}

export function formatIsoDateTimeToDisplay(s: string | null): string {
  if (!s || !isValidIsoDateTime(s)) return ''
  const [date, time] = s.split('T')
  const [y, mo, d] = date.split('-')
  const [hh, mm] = time.split(':')
  return `${d}/${mo}/${y} ${hh}:${mm}`
}

export function splitDateTime(s: string | null): {date: string | null; time: string} {
  if (!s || !isValidIsoDateTime(s)) return {date: null, time: ''}
  const [date, time] = s.split('T')
  return {date, time: time.slice(0, 5)}
}

export function composeDateTime(date: string, time: string): string {
  const t = time || '00:00'
  return `${date}T${t}:00`
}
  • Step 4 : Lancer les tests pour vérifier qu'ils passent

Run: npx vitest run app/components/malio/date/composables/datetimeFormat.test.ts Expected: PASS (tous verts).

  • Step 5 : Commit
git add app/components/malio/date/composables/datetimeFormat.ts app/components/malio/date/composables/datetimeFormat.test.ts
git commit --no-verify -m "feat : composable datetimeFormat pour MalioDateTime (#MUI-33)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"

Task 2 : Composant DateTime.vue

Files:

  • Create: app/components/malio/date/DateTime.vue

  • Test: app/components/malio/date/DateTime.test.ts

  • Step 1 : Écrire les tests qui échouent

Créer app/components/malio/date/DateTime.test.ts :

import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest'
import {mount} from '@vue/test-utils'
import type {DefineComponent} from 'vue'
import DateTime_ from './DateTime.vue'

type DateTimeProps = {
  id?: string
  name?: string
  label?: string
  modelValue?: string | null
  placeholder?: string
  required?: boolean
  disabled?: boolean
  readonly?: boolean
  hint?: string
  error?: string
  success?: string
  min?: string
  max?: string
  clearable?: boolean
  inputClass?: string
  labelClass?: string
  groupClass?: string
}

const DateTimeForTest = DateTime_ as DefineComponent<DateTimeProps>
const mountDateTime = (props: DateTimeProps = {}) =>
  mount(DateTimeForTest, {props, attachTo: document.body})

describe('MalioDateTime', () => {
  beforeEach(() => {
    vi.useFakeTimers()
    vi.setSystemTime(new Date(2026, 4, 19)) // 19 mai 2026
  })
  afterEach(() => vi.useRealTimers())

  describe('rendu', () => {
    it('affiche le label et l\\'icône calendrier', () => {
      const wrapper = mountDateTime({label: 'Rendez-vous'})
      expect(wrapper.get('label').text()).toBe('Rendez-vous')
      expect(wrapper.find('[data-test="calendar-icon"]').exists()).toBe(true)
    })

    it('affiche la valeur formatée date + heure dans le champ', () => {
      const wrapper = mountDateTime({modelValue: '2026-05-20T14:30:00'})
      const input = wrapper.get('[data-test="date-input"]').element as HTMLInputElement
      expect(input.value).toBe('20/05/2026 14:30')
    })
  })

  describe('popover', () => {
    it('ouvre la grille et l\\'input heure au clic', async () => {
      const wrapper = mountDateTime()
      await wrapper.get('[data-test="date-input"]').trigger('click')
      expect(wrapper.find('[data-test="month-grid"]').exists()).toBe(true)
      expect(wrapper.find('[data-test="time-input"]').exists()).toBe(true)
    })
  })

  describe('sélection', () => {
    it('émet le jour à 00:00 et garde le popover ouvert', async () => {
      const wrapper = mountDateTime()
      await wrapper.get('[data-test="date-input"]').trigger('click')
      await wrapper.get('[data-test="day"][data-iso="2026-05-19"]').trigger('click')
      expect(wrapper.emitted('update:modelValue')?.at(-1)).toEqual(['2026-05-19T00:00:00'])
      expect(wrapper.find('[data-test="popover"]').exists()).toBe(true)
    })

    it('applique l\\'heure réglée avant le clic du jour', async () => {
      const wrapper = mountDateTime()
      await wrapper.get('[data-test="date-input"]').trigger('click')
      const time = wrapper.get('[data-test="time-input"]')
      ;(time.element as HTMLInputElement).value = '09:15'
      await time.trigger('input')
      // pas d'émission tant qu'aucun jour n'est choisi
      expect(wrapper.emitted('update:modelValue')).toBeUndefined()
      await wrapper.get('[data-test="day"][data-iso="2026-05-19"]').trigger('click')
      expect(wrapper.emitted('update:modelValue')?.at(-1)).toEqual(['2026-05-19T09:15:00'])
    })

    it('met à jour l\\'heure quand une date est déjà choisie', async () => {
      const wrapper = mountDateTime({modelValue: '2026-05-20T14:30:00'})
      await wrapper.get('[data-test="date-input"]').trigger('click')
      const time = wrapper.get('[data-test="time-input"]')
      ;(time.element as HTMLInputElement).value = '08:45'
      await time.trigger('input')
      expect(wrapper.emitted('update:modelValue')?.at(-1)).toEqual(['2026-05-20T08:45:00'])
    })

    it('initialise l\\'input heure depuis la valeur', async () => {
      const wrapper = mountDateTime({modelValue: '2026-05-20T14:30:00'})
      await wrapper.get('[data-test="date-input"]').trigger('click')
      const time = wrapper.get('[data-test="time-input"]').element as HTMLInputElement
      expect(time.value).toBe('14:30')
    })
  })

  describe('bornes min/max', () => {
    it('désactive les jours hors bornes (datetime borné sur la date)', async () => {
      const wrapper = mountDateTime({min: '2026-05-10T00:00:00', max: '2026-05-20T00:00:00'})
      await wrapper.get('[data-test="date-input"]').trigger('click')
      const outside = wrapper.get('[data-test="day"][data-iso="2026-05-05"]')
      expect((outside.element as HTMLButtonElement).disabled).toBe(true)
    })
  })

  describe('effacement', () => {
    it('émet null au clic sur la croix', async () => {
      const wrapper = mountDateTime({modelValue: '2026-05-20T14:30:00'})
      await wrapper.get('[data-test="clear"]').trigger('click')
      expect(wrapper.emitted('update:modelValue')?.at(-1)).toEqual([null])
    })
  })

  describe('accessibilité', () => {
    it('positionne aria-invalid et describedby sur erreur', () => {
      const wrapper = mountDateTime({error: 'Date requise'})
      const input = wrapper.get('[data-test="date-input"]')
      expect(input.attributes('aria-invalid')).toBe('true')
      expect(input.attributes('aria-describedby')).toBeTruthy()
      expect(wrapper.text()).toContain('Date requise')
    })
  })
})
  • Step 2 : Lancer les tests pour vérifier qu'ils échouent

Run: npx vitest run app/components/malio/date/DateTime.test.ts Expected: FAIL (DateTime.vue introuvable).

  • Step 3 : Écrire l'implémentation

Créer app/components/malio/date/DateTime.vue :

<template>
  <CalendarField
    :id="id"
    :display-value="displayValue"
    :sync-to="datePart"
    :name="name"
    :label="label"
    :placeholder="placeholder"
    :required="required"
    :disabled="disabled"
    :readonly="readonly"
    :hint="hint"
    :error="error"
    :success="success"
    :clearable="clearable"
    :input-class="inputClass"
    :label-class="labelClass"
    :group-class="groupClass"
    v-bind="$attrs"
    @clear="onClear"
  >
    <template #default="{ currentMonth, currentYear }">
      <MonthGrid
        :month="currentMonth"
        :year="currentYear"
        :selected-date="datePart"
        :min="min?.slice(0, 10)"
        :max="max?.slice(0, 10)"
        @select="onSelectDay"
      />
      <!-- Bloc heure intérimaire : input natif, isolé pour remplacement futur par le sélecteur dédié. -->
      <div class="mt-[10px] flex items-center gap-2 border-t border-m-muted/30 pt-[10px]">
        <label
          :for="timeInputId"
          class="text-sm font-medium text-m-muted"
        >
          Heure
        </label>
        <input
          :id="timeInputId"
          data-test="time-input"
          type="time"
          :value="timeValue"
          class="rounded-md border border-m-muted bg-white px-2 py-1 text-base outline-none focus:border-m-primary"
          @input="onTimeInput"
        >
      </div>
    </template>
  </CalendarField>
</template>

<script setup lang="ts">
import {computed, ref, useId, watch} from 'vue'
import CalendarField from './internal/CalendarField.vue'
import MonthGrid from './internal/MonthGrid.vue'
import {composeDateTime, formatIsoDateTimeToDisplay, isValidIsoDateTime, splitDateTime} from './composables/datetimeFormat'

defineOptions({name: 'MalioDateTime', inheritAttrs: false})

const props = withDefaults(
  defineProps<{
    id?: string
    name?: string
    label?: string
    modelValue?: string | null
    placeholder?: string
    required?: boolean
    disabled?: boolean
    readonly?: boolean
    hint?: string
    error?: string
    success?: string
    min?: string
    max?: string
    clearable?: boolean
    inputClass?: string
    labelClass?: string
    groupClass?: string
  }>(),
  {
    id: '',
    name: '',
    label: '',
    modelValue: undefined,
    placeholder: 'JJ/MM/AAAA HH:MM',
    required: false,
    disabled: false,
    readonly: false,
    hint: '',
    error: '',
    success: '',
    min: undefined,
    max: undefined,
    clearable: true,
    inputClass: '',
    labelClass: '',
    groupClass: '',
  },
)

const emit = defineEmits<{(e: 'update:modelValue', value: string | null): void}>()

const generatedId = useId()
const timeInputId = computed(() => `${props.id || `malio-datetime-${generatedId}`}-time`)

// pendingTime : heure réglée avant qu'un jour ne soit choisi (sinon on ne peut pas émettre).
const pendingTime = ref('')

const parts = computed(() => splitDateTime(props.modelValue ?? null))
const datePart = computed(() => parts.value.date)
const displayValue = computed(() => formatIsoDateTimeToDisplay(props.modelValue ?? null))
const timeValue = computed(() => parts.value.time || pendingTime.value)

function onSelectDay(iso: string) {
  const time = parts.value.time || pendingTime.value || '00:00'
  emit('update:modelValue', composeDateTime(iso, time))
}

function onTimeInput(e: Event) {
  const value = (e.target as HTMLInputElement).value
  if (!value) return
  if (datePart.value) {
    emit('update:modelValue', composeDateTime(datePart.value, value))
  }
  else {
    pendingTime.value = value
  }
}

function onClear() {
  pendingTime.value = ''
  emit('update:modelValue', null)
}

watch(() => props.modelValue, (val) => {
  if (val && !isValidIsoDateTime(val) && import.meta.dev) {
    console.warn(`[MalioDateTime] modelValue invalide ignoré : "${val}"`)
  }
})
</script>
  • Step 4 : Lancer les tests pour vérifier qu'ils passent

Run: npx vitest run app/components/malio/date/DateTime.test.ts Expected: PASS (tous verts).

Note : si @input ne déclenche pas value correctement en jsdom, utiliser await time.setValue('09:15') à la place du couple .value = + .trigger('input') dans les tests.

  • Step 5 : Commit
git add app/components/malio/date/DateTime.vue app/components/malio/date/DateTime.test.ts
git commit --no-verify -m "feat : composant MalioDateTime (date + heure) (#MUI-33)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"

Task 3 : Page playground + entrée nav

Files:

  • Create: .playground/pages/composant/date/datetime.vue

  • Modify: .playground/playground.nav.ts (section DATES & HEURES)

  • Step 1 : Créer la page playground

Créer .playground/pages/composant/date/datetime.vue :

<template>
  <div class="space-y-6 p-4">
    <h1 class="text-2xl font-bold">MalioDateTime</h1>

    <div class="flex flex-wrap items-start gap-10">
      <div class="w-[396px] space-y-3">
        <h2 class="font-semibold">Simple</h2>
        <MalioDateTime
          v-model="value"
          label="Date et heure du rendez-vous"
          hint="Choisis un jour puis une heure"
        />
        <div class="rounded border p-3 text-sm">
          <p>Valeur (ISO naïf) : <code>{{ value ?? 'null' }}</code></p>
        </div>
        <button
          type="button"
          class="rounded border px-3 py-1.5"
          @click="value = null"
        >
          Réinitialiser
        </button>
      </div>

      <div class="w-[396px] space-y-3">
        <h2 class="font-semibold">Valeur initiale + bornes</h2>
        <MalioDateTime
          v-model="bounded"
          label="Créneau"
          :min="todayIso"
          :max="maxIso"
          hint="Entre aujourd'hui et +30 jours"
        />
        <div class="rounded border p-3 text-sm">
          <p>Valeur (ISO naïf) : <code>{{ bounded ?? 'null' }}</code></p>
        </div>
      </div>
    </div>
  </div>
</template>

<script setup lang="ts">
import {ref} from 'vue'

const pad = (n: number) => String(n).padStart(2, '0')
const toIso = (d: Date) => `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T00:00:00`
const now = new Date()
const todayIso = toIso(now)
const maxIso = toIso(new Date(now.getTime() + 30 * 86400000))

const value = ref<string | null>(null)
const bounded = ref<string | null>('2026-05-20T14:30:00')
</script>
  • Step 2 : Ajouter l'entrée nav

Dans .playground/playground.nav.ts, section DATES & HEURES, ajouter l'item après Semaine :

      {label: 'Date & heure', to: '/composant/date/datetime'},

Le bloc devient :

    items: [
      {label: 'Date', to: '/composant/date/date'},
      {label: 'Plage de dates', to: '/composant/date/dateRange'},
      {label: 'Semaine', to: '/composant/date/dateWeek'},
      {label: 'Date & heure', to: '/composant/date/datetime'},
      {label: 'Heure', to: '/composant/time/time'},
    ],
  • Step 3 : Vérifier le lint

Run: npm run lint Expected: PASS (pas d'erreur sur les fichiers playground).

  • Step 4 : Commit
git add .playground/pages/composant/date/datetime.vue .playground/playground.nav.ts
git commit --no-verify -m "feat : page playground MalioDateTime (#MUI-33)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"

Task 4 : Story Histoire

Files:

  • Create: app/story/date/dateTime.story.vue

  • Step 1 : Créer la story

Créer app/story/date/dateTime.story.vue (nom de fichier camelCase pour éviter vue/multi-word-component-names) :

<template>
  <Story title="Date/DateTime">
    <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>
        <MalioDateTime
          v-model="simpleValue"
          label="Date et heure"
        />
      </div>

      <div class="rounded-lg border p-4">
        <h2 class="mb-4 text-xl font-bold">Valeur initiale</h2>
        <MalioDateTime
          v-model="initialValue"
          label="Rendez-vous"
        />
      </div>

      <div class="rounded-lg border p-4">
        <h2 class="mb-4 text-xl font-bold">Avec min/max</h2>
        <MalioDateTime
          v-model="boundedValue"
          label="Créneau"
          :min="todayIso"
          :max="maxIso"
          hint="Entre aujourd'hui et +30 jours"
        />
      </div>

      <div class="rounded-lg border p-4">
        <h2 class="mb-4 text-xl font-bold">Erreur</h2>
        <MalioDateTime
          v-model="errorValue"
          label="Date limite"
          error="Date et heure requises"
        />
      </div>

      <div class="rounded-lg border p-4">
        <h2 class="mb-4 text-xl font-bold">Désactivé</h2>
        <MalioDateTime
          v-model="initialValue"
          label="Désactivé"
          disabled
        />
      </div>

      <div class="rounded-lg border p-4">
        <h2 class="mb-4 text-xl font-bold">Lecture seule</h2>
        <MalioDateTime
          v-model="initialValue"
          label="Lecture seule"
          readonly
        />
      </div>
    </div>
  </Story>
</template>

<script setup lang="ts">
import {ref} from 'vue'
import MalioDateTime from '../../components/malio/date/DateTime.vue'

const pad = (n: number) => String(n).padStart(2, '0')
const toIso = (d: Date) => `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T00:00:00`

const now = new Date()
const todayIso = toIso(now)
const maxIso = toIso(new Date(now.getTime() + 30 * 86400000))

const simpleValue = ref<string | null>(null)
const initialValue = ref<string | null>('2026-05-20T14:30:00')
const boundedValue = ref<string | null>(null)
const errorValue = ref<string | null>(null)
</script>
  • Step 2 : Vérifier le lint

Run: npm run lint Expected: PASS.

  • Step 3 : Commit
git add app/story/date/dateTime.story.vue
git commit --no-verify -m "docs : story Histoire pour MalioDateTime (#MUI-33)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"

Task 5 : Documentation (COMPONENTS.md + CHANGELOG.md)

Files:

  • Modify: COMPONENTS.md

  • Modify: CHANGELOG.md

  • Step 1 : Ajouter la section dans COMPONENTS.md

Ouvrir COMPONENTS.md, repérer la section MalioDate (ou la famille date) et ajouter une section MalioDateTime calquée dessus, documentant :

  • Description : champ unique date + heure, popover (grille + sélecteur d'heure), version intérimaire avec <input type="time"> natif.
  • modelValue : string | null, format "YYYY-MM-DDTHH:MM:00" (ISO naïf sans fuseau ; Symfony applique son fuseau).
  • Table des props identique à MalioDate (ajouter la note sur min/max bornant la date).
  • Émission update:modelValue.
  • Exemple d'usage :
<MalioDateTime v-model="rdv" label="Date et heure du rendez-vous" />
<!-- rdv === "2026-05-20T14:30:00" -->
  • Note : le sélecteur d'heure est intérimaire et sera remplacé par un composant dédié (maquette à venir).

Respecter le style et la structure exacts des sections existantes (titres, tableaux markdown).

  • Step 2 : Ajouter l'entrée CHANGELOG.md

Dans CHANGELOG.md, sous ## [0.0.0]### Added, ajouter après la ligne [#MUI-33] Développer le composant Datepicker :

* [#MUI-33] Création du composant DateTime (date + heure, sélecteur d'heure natif intérimaire)
  • Step 3 : Commit
git add COMPONENTS.md CHANGELOG.md
git commit --no-verify -m "docs : documente MalioDateTime (COMPONENTS + CHANGELOG) (#MUI-33)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"

Vérification finale

  • npx vitest run app/components/malio/date/ → toute la famille date verte.
  • npm run lint → pas d'erreur.
  • Revue finale du composant (cohérence avec la famille date, isolation du bloc heure pour remplacement futur).