maquina-components 0.7.4 → 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f12b51321ecf3ce0e3c1372bab8dec0770be3fc69f66ac68384f8980994d1018
4
- data.tar.gz: 6b24d430d5014890dc45a78cf2b13e5ef1d4b8991732b59b0002c40d8f7f598d
3
+ metadata.gz: 3cc64fa3b021d92f3dd1e54f71c762404e3c7778a137f2eee3ca4a33d28c247b
4
+ data.tar.gz: aa3a45337cc8d43e1c47e884750418b198c2afde379cc74858a9c866b7aa0bec
5
5
  SHA512:
6
- metadata.gz: c64b09b67ffc2f10d85e7870edfa538c11171e210abf0381b1ac8f7e481771e83f24327317bb2208795fdd675dd8133ae58557c7eba17913f5d6f7fcd5cd176c
7
- data.tar.gz: 2d66b6f096c3cfc5854e4fff1daea50f4f7f2dfe9b640db6f849cda507ae449f6ccf7a37b9b0f2a2724d3a72e5f8859575072a4ce35284a3f93a82c5f13b7fea
6
+ metadata.gz: 9568653a565de12c6a0485663fc48b7f31b75855da80a5cbbb118df5025ecae1a8e3bcf88eeaf43d86e42bd2c1f458b2dc4aebe9deff0e3e8276689a98a75185
7
+ data.tar.gz: cce7f348df9e0594b464366b492efbc76270698e44d76316bcd6c11e2aacf9d2c99c6c2dfda2b3cc40da07705c4480bc4cb0c43d2701bbb661a2645b0dfde283
@@ -7,15 +7,24 @@
7
7
  * Structure:
8
8
  * - calendar (root container)
9
9
  * - header (navigation)
10
+ * - caption, or with caption_layout: :dropdown
11
+ * dropdowns > month-select, year-select (native [data-component="select"])
10
12
  * - weekdays (day name headers)
11
13
  * - grid (day buttons grid)
12
14
  * - week (row)
13
15
  * - day (button)
16
+ * - footer (optional, the partial's block)
17
+ *
18
+ * Inside a popover the controller sets data-side="top|bottom" on the root.
19
+ * Flipped above its trigger, a popover is pinned at its bottom edge while
20
+ * months change height (4-6 weeks), so [data-side="top"] moves the header
21
+ * last — next to the trigger, where the arrows stay put.
14
22
  *
15
23
  * Cascade order in this file (full contract documented in form.css):
16
24
  * 1. base [data-component="calendar"], [data-calendar-part="…"]
17
25
  * 2. sizes — none (cell metrics come from --cell-size)
18
- * 3. variants [data-calendar-part="day"]:where([data-today], [data-state="…"], [data-outside])
26
+ * 3. variants [data-calendar-part="day"]:where([data-today], [data-state="…"], [data-outside]),
27
+ * [data-component="calendar"]:where([data-side="top"])
19
28
  * 4. states […]:where(:hover, :focus-visible, :disabled)
20
29
  * 5. dark — none; dark-mode differences are tokens (see tokens.css)
21
30
  * Every rule is flattened to 0,1,0 with :where() and lives in
@@ -94,6 +103,26 @@
94
103
  color: var(--foreground);
95
104
  }
96
105
 
106
+ /* ===== Caption dropdowns =====
107
+ Month and year are native selects styled by form.css (colors, radius,
108
+ focus, chevron, dark mode). Here they only shrink to fit beside the
109
+ arrows in a 7-cell header: calendar.css loads after form.css, so these
110
+ 0,1,0 rules win on source order. */
111
+ [data-calendar-part="dropdowns"] {
112
+ display: flex;
113
+ align-items: center;
114
+ @apply gap-1;
115
+ }
116
+
117
+ :where([data-calendar-part="dropdowns"]) [data-component="select"] {
118
+ width: auto;
119
+ height: var(--cell-size);
120
+ @apply pl-2 pr-6 py-0 text-sm;
121
+ font-weight: var(--label-weight, 500);
122
+ background-position: right 0.25rem center;
123
+ background-size: 1rem 1rem;
124
+ }
125
+
97
126
  /* ===== Weekday Headers ===== */
98
127
  [data-calendar-part="weekdays"] {
99
128
  display: grid;
@@ -123,6 +152,11 @@
123
152
  grid-template-columns: repeat(7, var(--cell-size));
124
153
  }
125
154
 
155
+ /* ===== Footer ===== */
156
+ [data-calendar-part="footer"] {
157
+ @apply mt-4;
158
+ }
159
+
126
160
  /* ===== Day Button Base ===== */
127
161
  [data-calendar-part="day"] {
128
162
  display: inline-flex;
@@ -205,6 +239,19 @@
205
239
  @apply opacity-30;
206
240
  }
207
241
 
242
+ /* ===== Popover side ===== */
243
+
244
+ /* Opened above the trigger: weekdays, grid, footer, then the header */
245
+ [data-component="calendar"]:where([data-side="top"]) {
246
+ display: flex;
247
+ flex-direction: column;
248
+ }
249
+
250
+ :where([data-component="calendar"][data-side="top"]) [data-calendar-part="header"] {
251
+ order: 1;
252
+ @apply mt-4 mb-0;
253
+ }
254
+
208
255
  /* ===== Day States ===== */
209
256
 
210
257
  /* Hover — only for cells with no selection state of their own */
@@ -14,7 +14,8 @@
14
14
  * 2. sizes :where([data-size="sm"|"lg"]) [data-date-picker-part="trigger"]
15
15
  * 3. variants :where([data-full-width]) …
16
16
  * 4. states […]:where(:hover, :focus-visible, :disabled, [aria-expanded],
17
- * :has(input:invalid))
17
+ * :has(input:invalid)); popover open/closed, and
18
+ * :has(calendar[data-side="top"]) for the animation
18
19
  * 5. dark — none; dark-mode differences are tokens (see tokens.css)
19
20
  * Every rule is flattened to 0,1,0 with :where() and lives in
20
21
  * @layer components, so a caller's utility class wins and source order is
@@ -160,6 +161,17 @@
160
161
  }
161
162
  }
162
163
 
164
+ /* Opened above the trigger (the calendar's data-side): grow upward out of it */
165
+ [data-date-picker-part="popover"]:where(:not(:popover-open):has([data-component="calendar"][data-side="top"])) {
166
+ transform: translateY(0.5rem) scale(0.95);
167
+ }
168
+
169
+ @starting-style {
170
+ [data-date-picker-part="popover"]:where(:popover-open:has([data-component="calendar"][data-side="top"])) {
171
+ transform: translateY(0.5rem) scale(0.95);
172
+ }
173
+ }
174
+
163
175
  /* Backdrop */
164
176
  [data-date-picker-part="popover"]::backdrop {
165
177
  background-color: transparent;
@@ -72,6 +72,43 @@ module MaquinaComponents
72
72
  end
73
73
  end
74
74
 
75
+ # Years offered by the caption's year select (caption_layout: :dropdown)
76
+ #
77
+ # min_date/max_date set the bounds when present; otherwise years applies,
78
+ # 100 years back to 10 ahead by default. Given both, the narrower bound
79
+ # wins. The displayed year is always included.
80
+ #
81
+ # @param display_year [Integer] Year the calendar shows
82
+ # @param min_date [Date, nil] Minimum selectable date
83
+ # @param max_date [Date, nil] Maximum selectable date
84
+ # @param years [Range, nil] Explicit span of years
85
+ # @return [Range]
86
+ def calendar_year_range(display_year, min_date: nil, max_date: nil, years: nil)
87
+ first, last = (years || ((Date.current.year - 100)..(Date.current.year + 10))).minmax
88
+ first = min_date.year if min_date && (years.nil? || min_date.year > first)
89
+ last = max_date.year if max_date && (years.nil? || max_date.year < last)
90
+
91
+ [first, display_year].min..[last, display_year].max
92
+ end
93
+
94
+ # Options for the caption's month select: abbreviated localized names
95
+ # (the full ones do not fit beside the year in a 7-cell header), with the
96
+ # months wholly outside min_date/max_date disabled for that year
97
+ #
98
+ # @param year [Integer] Year the months belong to
99
+ # @param min_date [Date, nil] Minimum selectable date
100
+ # @param max_date [Date, nil] Maximum selectable date
101
+ # @return [Array<Array(String, Integer, Boolean)>] [name, month, disabled]
102
+ def calendar_month_options(year, min_date: nil, max_date: nil)
103
+ I18n.t("date.abbr_month_names").drop(1).each_with_index.map do |name, index|
104
+ first_of_month = Date.new(year, index + 1, 1)
105
+ disabled = (min_date && first_of_month.end_of_month < min_date) ||
106
+ (max_date && first_of_month > max_date)
107
+
108
+ [name, index + 1, !!disabled]
109
+ end
110
+ end
111
+
75
112
  # Check if a date falls within a range
76
113
  #
77
114
  # @param date [Date] The date to check
@@ -1,7 +1,7 @@
1
1
  import { Controller } from "@hotwired/stimulus"
2
2
 
3
3
  export default class extends Controller {
4
- static targets = ["day", "input", "inputEnd", "prevButton", "nextButton", "grid", "caption"]
4
+ static targets = ["day", "input", "inputEnd", "prevButton", "nextButton", "grid", "caption", "monthSelect", "yearSelect"]
5
5
 
6
6
  static values = {
7
7
  month: Number,
@@ -13,12 +13,47 @@ export default class extends Controller {
13
13
  mode: { type: String, default: "single" },
14
14
  weekStartsOn: { type: String, default: "sunday" },
15
15
  disabledDates: Array,
16
- showOutsideDays: { type: Boolean, default: true }
16
+ showOutsideDays: { type: Boolean, default: true },
17
+ // false: a click on the selected day keeps it (single mode), and the start
18
+ // clicked again makes a one-day range (range mode), so a required date
19
+ // can't be emptied by a click
20
+ deselectable: { type: Boolean, default: true },
21
+ // Bounds of the caption's year select (caption_layout: :dropdown only)
22
+ startYear: Number,
23
+ endYear: Number
17
24
  }
18
25
 
19
26
  connect() {
20
27
  this.updateNavigationState()
21
28
  this.updateDayStates()
29
+
30
+ // Inside a popover (date picker or a caller's own), track which side of
31
+ // the trigger it opened on, so the header can sit next to the trigger
32
+ this.popover = this.element.closest("[popover]")
33
+ if (this.popover) {
34
+ this.boundUpdateSide = this.updateSide.bind(this)
35
+ this.popover.addEventListener("toggle", this.boundUpdateSide)
36
+ }
37
+ }
38
+
39
+ disconnect() {
40
+ this.popover?.removeEventListener("toggle", this.boundUpdateSide)
41
+ }
42
+
43
+ /**
44
+ * Sets data-side="top|bottom" when the popover is open. A popover flipped
45
+ * above its trigger is pinned at its bottom edge, so CSS moves the header
46
+ * there: month heights differ, and the arrows must not move under the cursor.
47
+ */
48
+ updateSide() {
49
+ if (!this.popover?.matches(":popover-open")) return
50
+
51
+ const trigger = this.popover.id &&
52
+ document.querySelector(`[popovertarget="${CSS.escape(this.popover.id)}"]`)
53
+ if (!trigger) return
54
+
55
+ const above = this.popover.getBoundingClientRect().top < trigger.getBoundingClientRect().top
56
+ this.element.dataset.side = above ? "top" : "bottom"
22
57
  }
23
58
 
24
59
  previousMonth() {
@@ -45,12 +80,38 @@ export default class extends Controller {
45
80
  this.navigateToMonth(newMonth, newYear)
46
81
  }
47
82
 
83
+ // Caption dropdowns (caption_layout: :dropdown)
84
+ selectMonth() {
85
+ this.navigateToMonth(Number(this.monthSelectTarget.value), this.yearValue)
86
+ }
87
+
88
+ selectYear() {
89
+ const year = Number(this.yearSelectTarget.value)
90
+ this.navigateToMonth(this.clampMonth(this.monthValue, year), year)
91
+ }
92
+
93
+ // A year change keeps the month, unless min/max date rule it out that year
94
+ clampMonth(month, year) {
95
+ const [minYear, minMonth] = (this.minDateValue || "").split("-").map(Number)
96
+ const [maxYear, maxMonth] = (this.maxDateValue || "").split("-").map(Number)
97
+
98
+ if (year === minYear && month < minMonth) return minMonth
99
+ if (year === maxYear && month > maxMonth) return maxMonth
100
+ return month
101
+ }
102
+
48
103
  navigateToMonth(month, year) {
104
+ // The year select lists only startYear..endYear; never show a month it can't
105
+ if ((this.hasStartYearValue && year < this.startYearValue) ||
106
+ (this.hasEndYearValue && year > this.endYearValue)) return
107
+
49
108
  this.monthValue = month
50
109
  this.yearValue = year
51
110
 
52
111
  this.rebuildCalendar()
112
+ this.syncDropdowns()
53
113
  this.updateNavigationState()
114
+ this.updateSide()
54
115
 
55
116
  this.dispatch("navigate", {
56
117
  detail: {
@@ -131,7 +192,10 @@ export default class extends Controller {
131
192
  disabledDates.has(dateStr)
132
193
 
133
194
  let dayState = null
134
- if (isSelected && this.modeValue === "range" && selectedEndStr) {
195
+ if (isSelected && isRangeEnd) {
196
+ // A one-day range
197
+ dayState = "selected"
198
+ } else if (isSelected && this.modeValue === "range" && selectedEndStr) {
135
199
  dayState = "range-start"
136
200
  } else if (isRangeEnd) {
137
201
  dayState = "range-end"
@@ -185,6 +249,8 @@ export default class extends Controller {
185
249
 
186
250
  handleSingleSelection(dateStr) {
187
251
  if (this.selectedValue === dateStr) {
252
+ // Nothing changes, so nothing is dispatched
253
+ if (!this.deselectableValue) return
188
254
  this.selectedValue = ""
189
255
  } else {
190
256
  this.selectedValue = dateStr
@@ -211,12 +277,13 @@ export default class extends Controller {
211
277
  // Clicked before start - swap
212
278
  this.selectedEndValue = this.selectedValue
213
279
  this.selectedValue = dateStr
214
- } else if (dateStr === selectedStr) {
280
+ } else if (dateStr === selectedStr && this.deselectableValue) {
215
281
  // Clicked same date - clear
216
282
  this.selectedValue = ""
217
283
  this.selectedEndValue = ""
218
284
  } else {
219
- // Clicked after start - set end
285
+ // Clicked after start, or the start again when not deselectable
286
+ // (a one-day range) - set end
220
287
  this.selectedEndValue = dateStr
221
288
  }
222
289
  }
@@ -301,7 +368,10 @@ export default class extends Controller {
301
368
  let state = null
302
369
 
303
370
  if (this.modeValue === "range" && selectedStr && selectedEndStr) {
304
- if (dateStr === selectedStr) {
371
+ if (dateStr === selectedStr && dateStr === selectedEndStr) {
372
+ // A one-day range
373
+ state = "selected"
374
+ } else if (dateStr === selectedStr) {
305
375
  state = "range-start"
306
376
  } else if (dateStr === selectedEndStr) {
307
377
  state = "range-end"
@@ -337,14 +407,40 @@ export default class extends Controller {
337
407
  }
338
408
 
339
409
  updateNavigationState() {
340
- if (this.minDateValue && this.hasPrevButtonTarget) {
341
- const firstOfMonth = this.formatDate(new Date(this.yearValue, this.monthValue - 1, 1))
342
- this.prevButtonTarget.disabled = firstOfMonth <= this.minDateValue
410
+ const firstOfMonth = this.formatDate(new Date(this.yearValue, this.monthValue - 1, 1))
411
+ const lastOfMonth = this.formatDate(new Date(this.yearValue, this.monthValue, 0))
412
+
413
+ if (this.hasPrevButtonTarget) {
414
+ this.prevButtonTarget.disabled = Boolean(
415
+ (this.minDateValue && firstOfMonth <= this.minDateValue) ||
416
+ (this.hasStartYearValue && this.monthValue === 1 && this.yearValue <= this.startYearValue)
417
+ )
418
+ }
419
+
420
+ if (this.hasNextButtonTarget) {
421
+ this.nextButtonTarget.disabled = Boolean(
422
+ (this.maxDateValue && lastOfMonth >= this.maxDateValue) ||
423
+ (this.hasEndYearValue && this.monthValue === 12 && this.yearValue >= this.endYearValue)
424
+ )
425
+ }
426
+ }
427
+
428
+ // Mirrors calendar_month_options: a month wholly outside min/max is disabled
429
+ syncDropdowns() {
430
+ if (this.hasMonthSelectTarget) {
431
+ for (const option of this.monthSelectTarget.options) {
432
+ const month = Number(option.value)
433
+ const first = this.formatDate(new Date(this.yearValue, month - 1, 1))
434
+ const last = this.formatDate(new Date(this.yearValue, month, 0))
435
+
436
+ option.disabled = Boolean((this.minDateValue && last < this.minDateValue) ||
437
+ (this.maxDateValue && first > this.maxDateValue))
438
+ }
439
+ this.monthSelectTarget.value = String(this.monthValue)
343
440
  }
344
441
 
345
- if (this.maxDateValue && this.hasNextButtonTarget) {
346
- const lastOfMonth = this.formatDate(new Date(this.yearValue, this.monthValue, 0))
347
- this.nextButtonTarget.disabled = lastOfMonth >= this.maxDateValue
442
+ if (this.hasYearSelectTarget) {
443
+ this.yearSelectTarget.value = String(this.yearValue)
348
444
  }
349
445
  }
350
446
 
@@ -128,6 +128,19 @@ export default class extends Controller {
128
128
  })
129
129
  }
130
130
 
131
+ /**
132
+ * Close the popover when the selected day is clicked again and stays
133
+ * selected (a non-deselectable calendar): the click confirms the date.
134
+ * Bound on the calendar, so it runs after the day's own selectDay; a click
135
+ * that picks a new day has already closed the popover in handleChange.
136
+ */
137
+ confirmSelection(event) {
138
+ const day = event.target.closest("[data-calendar-part='day']")
139
+ if (day?.dataset.state === "selected" && this.popoverTarget.matches(":popover-open")) {
140
+ this.closePopover()
141
+ }
142
+ }
143
+
131
144
  /**
132
145
  * Handle calendar navigate event
133
146
  * Can be used to fetch events for the new month
@@ -1,4 +1,4 @@
1
- <%# locals: (selected: nil, selected_end: nil, month: nil, year: nil, mode: :single, min_date: nil, max_date: nil, disabled_dates: [], show_outside_days: true, week_starts_on: :sunday, cell_size: nil, input_name: nil, input_name_end: nil, css_classes: "", **html_options) %>
1
+ <%# locals: (selected: nil, selected_end: nil, month: nil, year: nil, mode: :single, min_date: nil, max_date: nil, disabled_dates: [], show_outside_days: true, week_starts_on: :sunday, caption_layout: :label, years: nil, deselectable: true, cell_size: nil, input_name: nil, input_name_end: nil, css_classes: "", **html_options) %>
2
2
  <%
3
3
  # Dates may arrive as Date, Time or ISO strings
4
4
  selected_date = calendar_to_date(selected)
@@ -36,6 +36,18 @@
36
36
  abbr_day_names.rotate(1) :
37
37
  abbr_day_names
38
38
 
39
+ # caption_layout: :dropdown swaps the caption for month and year selects.
40
+ # Their locals reach the header only then, so an app's own calendar/header
41
+ # override written for the caption keeps working.
42
+ dropdown = caption_layout.to_sym == :dropdown
43
+ year_range = calendar_year_range(display_year, min_date: min_date_parsed, max_date: max_date_parsed, years: years) if dropdown
44
+ header_locals = if dropdown
45
+ {caption_layout: :dropdown, years: year_range,
46
+ month_options: calendar_month_options(display_year, min_date: min_date_parsed, max_date: max_date_parsed)}
47
+ else
48
+ {}
49
+ end
50
+
39
51
  merged_data = merge_component_data(html_options,
40
52
  controller: "calendar",
41
53
  component: "calendar",
@@ -48,7 +60,10 @@
48
60
  "calendar-max-date-value": max_date_parsed&.iso8601,
49
61
  "calendar-week-starts-on-value": week_starts_on,
50
62
  "calendar-disabled-dates-value": disabled_dates_parsed.map(&:iso8601).presence,
51
- "calendar-show-outside-days-value": show_outside_days
63
+ "calendar-show-outside-days-value": show_outside_days,
64
+ "calendar-deselectable-value": (false unless deselectable),
65
+ "calendar-start-year-value": year_range&.first,
66
+ "calendar-end-year-value": year_range&.last
52
67
  )
53
68
 
54
69
  # cell_size and a caller's style: both write the style attribute, so join them
@@ -80,7 +95,8 @@
80
95
  <%= render "components/calendar/header",
81
96
  month: display_month,
82
97
  year: display_year,
83
- month_name: I18n.l(first_of_month, format: "%B %Y") %>
98
+ month_name: I18n.l(first_of_month, format: "%B %Y"),
99
+ **header_locals %>
84
100
 
85
101
  <%# Weekday headers %>
86
102
  <div data-calendar-part="weekdays">
@@ -104,4 +120,13 @@
104
120
  show_outside_days: show_outside_days %>
105
121
  <% end %>
106
122
  </div>
123
+
124
+ <%# Optional footer (actions such as Clear/Done); stays between the grid and
125
+ a header moved to the bottom when the calendar's popover opens above.
126
+ Without a caller block a partial's yield returns content_for(:layout),
127
+ which is the whole page when rendered from a layout, so that is no footer. %>
128
+ <% footer = capture { yield }.presence %>
129
+ <% if footer && footer != content_for(:layout) %>
130
+ <div data-calendar-part="footer"><%= footer %></div>
131
+ <% end %>
107
132
  <% end %>
@@ -1,4 +1,4 @@
1
- <%# locals: (selected: nil, selected_end: nil, mode: :single, min_date: nil, max_date: nil, disabled_dates: [], show_outside_days: true, week_starts_on: :sunday, placeholder: nil, input_name: nil, input_name_end: nil, id: nil, disabled: false, required: false, size: :default, full_width: false, css_classes: "", **html_options) %>
1
+ <%# locals: (selected: nil, selected_end: nil, mode: :single, min_date: nil, max_date: nil, disabled_dates: [], show_outside_days: true, week_starts_on: :sunday, caption_layout: :label, years: nil, placeholder: nil, input_name: nil, input_name_end: nil, id: nil, disabled: false, required: false, deselectable: nil, size: :default, full_width: false, css_classes: "", **html_options) %>
2
2
  <%
3
3
  # Deterministic ID for popover targeting: random ids break idiomorph
4
4
  # matching across Turbo morphs
@@ -81,9 +81,13 @@
81
81
  disabled_dates: disabled_dates,
82
82
  show_outside_days: show_outside_days,
83
83
  week_starts_on: week_starts_on,
84
+ caption_layout: caption_layout,
85
+ years: years,
86
+ # A required date can't be emptied by clicking it again
87
+ deselectable: deselectable.nil? ? !required : deselectable,
84
88
  data: {
85
89
  "date-picker-target": "calendar",
86
- action: "calendar:change->date-picker#handleChange calendar:navigate->date-picker#handleNavigate"
90
+ action: "calendar:change->date-picker#handleChange calendar:navigate->date-picker#handleNavigate click->date-picker#confirmSelection"
87
91
  } %>
88
92
  </div>
89
93
  <% end %>
@@ -1,4 +1,4 @@
1
- <%# locals: (month:, year:, month_name:, css_classes: "", **html_options) %>
1
+ <%# locals: (month:, year:, month_name:, caption_layout: :label, years: nil, month_options: nil, css_classes: "", **html_options) %>
2
2
  <% merged_data = merge_component_data(html_options, "calendar-part": "header") %>
3
3
 
4
4
  <%= content_tag :div, class: css_classes.presence, data: merged_data, **html_options do %>
@@ -9,9 +9,33 @@
9
9
  <%= builtin_icon_for :chevron_left, class: "size-4" %>
10
10
  </button>
11
11
 
12
- <div data-calendar-part="caption">
13
- <%= month_name %>
14
- </div>
12
+ <% if caption_layout.to_sym == :dropdown %>
13
+ <div data-calendar-part="dropdowns">
14
+ <select data-component="select"
15
+ data-calendar-part="month-select"
16
+ data-calendar-target="monthSelect"
17
+ data-action="change->calendar#selectMonth"
18
+ aria-label="<%= t("maquina_components.calendar.month", default: "Month") %>">
19
+ <% month_options.each do |name, value, disabled| %>
20
+ <%= tag.option name, value: value, selected: value == month, disabled: disabled %>
21
+ <% end %>
22
+ </select>
23
+
24
+ <select data-component="select"
25
+ data-calendar-part="year-select"
26
+ data-calendar-target="yearSelect"
27
+ data-action="change->calendar#selectYear"
28
+ aria-label="<%= t("maquina_components.calendar.year", default: "Year") %>">
29
+ <% years.each do |value| %>
30
+ <%= tag.option value, value: value, selected: value == year %>
31
+ <% end %>
32
+ </select>
33
+ </div>
34
+ <% else %>
35
+ <div data-calendar-part="caption">
36
+ <%= month_name %>
37
+ </div>
38
+ <% end %>
15
39
 
16
40
  <button type="button"
17
41
  data-action="click->calendar#nextMonth"
@@ -20,7 +20,9 @@
20
20
  (max_date && day > max_date) ||
21
21
  disabled_dates.include?(day)
22
22
 
23
- day_state = if is_selected && mode == :range && selected_end_date
23
+ day_state = if is_selected && is_range_end
24
+ "selected" # a one-day range
25
+ elsif is_selected && mode == :range && selected_end_date
24
26
  "range-start"
25
27
  elsif is_range_end
26
28
  "range-end"
@@ -1,3 +1,3 @@
1
1
  module MaquinaComponents
2
- VERSION = "0.7.4"
2
+ VERSION = "0.8.1"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: maquina-components
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.7.4
4
+ version: 0.8.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mario Alberto Chávez