Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
7 changes: 7 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).

## [1.1.0]

### 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 All @@ -16,4 +22,5 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- `scrollableDayView()` template helper now takes an `isPopup` flag and routes through a new internal `scrollMaxHeight()` helper.
- Demo (`demo/index.html`) updated to better showcase the responsive popup behavior.

[1.1.0]: https://github.com/reachweb/alpine-calendar/releases/tag/v1.1.0
[1.0.1]: https://github.com/reachweb/alpine-calendar/releases/tag/v1.0.1
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