Live form
    Bounds (today and today + 30 days) are injected by the JS at runtime.
      09:00–18:00, on the half-hour. step takes seconds for type=time.
        booking.html
        <form id="booking-form" novalidate>
          <div>
            <label for="booking-date">Date</label>
            <input id="booking-date" name="date" type="date"
                   data-validation="required;numeric" />
            <ul class="error-list"></ul>
            <small>Bounds (today and today + 30 days) are injected by the JS at runtime.</small>
          </div>
          <div>
            <label for="booking-time">Time</label>
            <input id="booking-time" name="time" type="time"
                   data-validation="required;numeric;min(09:00);max(18:00);step(1800)" />
            <ul class="error-list"></ul>
            <small>09:00–18:00, on the half-hour. <code>step</code> takes seconds for type=time.</small>
          </div>
          <div>
            <label for="booking-party">Party size</label>
            <input id="booking-party" name="party" type="number"
                   data-validation="required;numeric;min(1);max(8);step(1)" />
            <ul class="error-list"></ul>
          </div>
          <button type="submit">Reserve</button>
        </form>
        booking.demo.ts
        import { FormValidator } from '@form-validator-js/core';
        import { required, numeric, min, max, step } from '@form-validator-js/validators';
        
        const form = document.querySelector<HTMLFormElement>('#booking-form');
        if (form) {
          // Compute today + 30 days for the date bounds.
          const today = new Date();
          const todayIso = today.toISOString().slice(0, 10);
          const in30 = new Date(today.getTime() + 30 * 86_400_000).toISOString().slice(0, 10);
        
          // Inject computed bounds into the data-validation attributes.
          const dateInput = form.querySelector<HTMLInputElement>('#booking-date');
          if (dateInput) {
            dateInput.dataset.validation = `required;numeric;min(${todayIso});max(${in30})`;
          }
        
          new FormValidator({
            form,
            validatorDeclarations: {
              required: { ...required, errorMessage: 'Required' },
              numeric:  { ...numeric,  errorMessage: 'Not a valid value' },
              min:      { ...min,      errorMessage: 'Too early / too small' },
              max:      { ...max,      errorMessage: 'Too late / too large' },
              step:     { ...step,     errorMessage: 'Not on the allowed step' },
            },
            onErrorMessageListChanged(target, errors) {
              if (target === form) return;
              const list = (target as HTMLElement).parentElement?.querySelector('.error-list');
              if (!list) return;
              list.innerHTML = errors.map((m) => `<li>${m}</li>`).join('');
            },
          });
        
          form.addEventListener('submit', (e) => {
            e.preventDefault();
            alert('Booked.');
          });
        }