Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Added

- **`precision: 'month'` — a forward-looking month picker.** Opens directly on the months grid (with the year prev/next arrows), positioned by the usual `value` > `initialMonth` > today precedence and clamped into `[minDate, maxDate]`. Clicking a month commits the first day of that month as the single selection, emits `calendar:change` (`detail.dates[0]` = first-of-month), formats the input via `format` (e.g. `'MMMM YYYY'` → `"August 2026"`), and closes the popup — no day grid, no year step. The bound input becomes read-only (selection-only), `format` defaults to `'MM/YYYY'`, and a stored first-of-month `value` restores both the displayed string and the highlighted month. Designed to compose with `mode: 'single'`; combining with `range`/`multiple` or `wizard` warns (precision takes precedence over the wizard).

## [1.0.1]

### Fixed
Expand Down
47 changes: 47 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,52 @@ To use a custom ref name:

Wizard modes: `true` (or `'full'`) for Year → Month → Day, `'year-month'` for Year → Month, `'month-day'` for Month → Day.

### Month Picker (departure-month)

Set `precision: 'month'` to turn the calendar into a forward-looking **month picker**. It opens directly on the months grid (with the year prev/next arrows), and clicking a month commits the **first day of that month** as the selection — there is no day grid and no separate year step.

```html
<div x-data="calendar({
mode: 'single',
precision: 'month',
format: 'MMMM YYYY',
display: 'popup',
minDate: '2026-06-01',
maxDate: '2027-12-31',
})">
<input x-ref="rc-input" type="text" class="rc-input">
</div>
```

Behavior:

- **Opens on the months grid**, positioned with the usual precedence: `value` > `initialMonth` > today. The opening month is clamped into `[minDate, maxDate]`, so the picker never opens on an out-of-range year.
- **`minDate`/`maxDate` are hard limits.** The year arrows are disabled at the first/last in-range year, and months outside the range are disabled. With `minDate: '2026-06-01'` and `maxDate: '2027-12-31'`, only **2026** and **2027** are reachable and months before June 2026 are disabled.
- **Clicking a month commits the 1st of that month**, emits `calendar:change` (with `detail.dates[0]` = first-of-month, e.g. `'2026-08-01'`), formats the input via `format` (e.g. `'MMMM YYYY'` → `"August 2026"`), and closes the popup.
- The bound input is made **read-only** (selection-only) — a `'MMMM YYYY'` value can't be typed back in.
- Set `name` to submit the value in a plain form — a hidden `<input>` carries the first-of-month ISO string (e.g. `2026-08-01`), no event wiring needed.
- `format` defaults to `'MM/YYYY'` when omitted. Use a month-only format such as `'MM/YYYY'` or `'MMMM YYYY'`; a day-based format logs a warning.
- Designed for `mode: 'single'`. Combining it with `range`/`multiple`, or with `wizard`, logs a warning (`precision: 'month'` takes precedence over the wizard).

**Restoring a selection across pages.** Pass the stored first-of-month back as `value` (or the month as `initialMonth`):

```html
<!-- Reopens on August 2026, with August marked selected and "August 2026" in the input -->
<div x-data="calendar({
mode: 'single',
precision: 'month',
format: 'MMMM YYYY',
display: 'popup',
minDate: '2026-06-01',
maxDate: '2027-12-31',
value: '2026-08-01',
})">
<input x-ref="rc-input" type="text" class="rc-input">
</div>
```

`value` both selects and displays the month; `initialMonth` (`'2026-08'`) only positions the view without selecting.

### Form Submission

```html
Expand Down Expand Up @@ -157,6 +203,7 @@ All options are passed via `x-data="calendar({ ... })"`.
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `mode` | `'single' \| 'multiple' \| 'range'` | `'single'` | Selection mode |
| `precision` | `'day' \| 'month'` | `'day'` | Selection granularity. `'month'` is a month picker (see [Month Picker](#month-picker-departure-month)) |
| `display` | `'inline' \| 'popup'` | `'inline'` | Inline calendar or popup with input |
| `format` | `string` | `'DD/MM/YYYY'` | Date format (tokens: `DD`, `MM`, `YYYY`, `D`, `M`, `YY`, `MMM`, `MMMM`) |
| `months` | `number` | `1` | Months to display (1=single, 2=dual side-by-side, 3+=scrollable) |
Expand Down
101 changes: 101 additions & 0 deletions demo/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -396,6 +396,7 @@ <h1>Alpine Calendar</h1>
<a href="#constraints">Constraints</a>
<a href="#rules">Rules</a>
<a href="#wizard">Wizard</a>
<a href="#month-picker">Month Picker</a>
<a href="#metadata">Metadata</a>
<a href="#scrollable">Scrollable</a>
<a href="#theming">Theming</a>
Expand Down Expand Up @@ -895,6 +896,106 @@ <h2 class="section-title">Wizard Mode</h2>
</div>
</div>

<!-- ============================================================
MONTH PICKER
============================================================ -->
<div class="container section" id="month-picker">
<h2 class="section-title">Month Picker</h2>
<p class="section-desc">Set <code>precision: 'month'</code> for a forward-looking month picker. It opens on the months grid, and clicking a month commits the first of that month — perfect for a "departure month" booking selector.</p>

<div class="examples examples-grid-2">

<div class="card">
<div class="card-header">
<div class="card-title">Month Picker</div>
<div class="card-desc">Opens on the months grid; year arrows navigate. No day step.</div>
</div>
<div class="card-body">
<div x-data="calendar({ mode: 'single', precision: 'month', format: 'MMMM YYYY' })"></div>
</div>
<details class="card-code">
<summary>View Code</summary>
<pre><code>&lt;div x-data="calendar({
mode: 'single',
precision: 'month',
format: 'MMMM YYYY'})"&gt;&lt;/div&gt;</code></pre>
</details>
</div>

<div class="card">
<div class="card-header">
<div class="card-title">Booking Window (min / max)</div>
<div class="card-desc">minDate/maxDate are hard limits: only 2026–2027 are reachable, months before June 2026 are disabled.</div>
</div>
<div class="card-body">
<div x-data="calendar({ mode: 'single', precision: 'month', format: 'MMMM YYYY', minDate: '2026-06-01', maxDate: '2027-12-31' })"></div>
</div>
<details class="card-code">
<summary>View Code</summary>
<pre><code>&lt;div x-data="calendar({
mode: 'single',
precision: 'month',
format: 'MMMM YYYY',
minDate: '2026-06-01',
maxDate: '2027-12-31'})"&gt;&lt;/div&gt;</code></pre>
</details>
</div>

<div class="card card--wide">
<div class="card-header">
<div class="card-title">Popup + Restore from Value</div>
<div class="card-desc">Bound to a read-only input. A stored first-of-month <code>value</code> restores both the "August 2026" label and the highlighted month.</div>
</div>
<div class="card-body">
<div x-data="calendar({ mode: 'single', precision: 'month', format: 'MMMM YYYY', display: 'popup', minDate: '2026-06-01', maxDate: '2027-12-31', value: '2026-08-01' })">
<input x-ref="rc-input" type="text" class="rc-input" placeholder="Departure month...">
</div>
</div>
<details class="card-code">
<summary>View Code</summary>
<pre><code>&lt;div x-data="calendar({
mode: 'single',
precision: 'month',
format: 'MMMM YYYY',
display: 'popup',
minDate: '2026-06-01',
maxDate: '2027-12-31',
value: '2026-08-01'})"&gt;
&lt;input x-ref="rc-input" type="text" class="rc-input"
placeholder="Departure month..."&gt;
&lt;/div&gt;</code></pre>
</details>
</div>

<div class="card card--wide">
<div class="card-header">
<div class="card-title">Form Submission (no JS)</div>
<div class="card-desc">With <code>name</code> set, a hidden input submits the first-of-month ISO value (e.g. <code>2026-08-01</code>) — no event wiring needed.</div>
</div>
<div class="card-body">
<form @submit.prevent="$dispatch('notify', { message: 'Submitted departure_month: ' + (new FormData($el).get('departure_month') || '(none)') })">
<div x-data="calendar({ mode: 'single', precision: 'month', format: 'MMMM YYYY', name: 'departure_month', minDate: '2026-06-01', maxDate: '2027-12-31' })"></div>
<button type="submit" class="demo-btn" style="margin-top: 0.75rem;">Submit Form</button>
</form>
</div>
<details class="card-code">
<summary>View Code</summary>
<pre><code>&lt;form method="post"&gt;
&lt;div x-data="calendar({
mode: 'single',
precision: 'month',
format: 'MMMM YYYY',
name: 'departure_month'
})"&gt;&lt;/div&gt;
&lt;button type="submit"&gt;Submit&lt;/button&gt;
&lt;/form&gt;
&lt;!-- POSTs departure_month=2026-08-01 (first-of-month ISO) --&gt;</code></pre>
</details>
</div>

</div>
</div>

<!-- ============================================================
METADATA
============================================================ -->
Expand Down
8 changes: 8 additions & 0 deletions eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,13 @@ export default tseslint.config(
'@typescript-eslint/consistent-type-imports': 'error',
},
},
{
// Test files routinely assert known-present values; non-null assertions are
// idiomatic here and used throughout the suite.
files: ['tests/**/*.ts'],
rules: {
'@typescript-eslint/no-non-null-assertion': 'off',
},
},
{ ignores: ['dist/', 'demo/', 'coverage/'] },
)
Loading
Loading