maquina-components 0.3.0 → 0.3.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: e04564783a082eb56e58395e1de24dc21deb1312c49ebc47e48428a4230e6925
4
- data.tar.gz: 78630e11ca92ae6a964c2bc100a7357722d7618df3a5a0a7d0619bbc48eeb5d3
3
+ metadata.gz: 1449d3d6edf1341e73d321b0821c1e3e355659022865465949ebf2202426d7b5
4
+ data.tar.gz: ffbe3396ad981086e0979756e401942410a66e2257db71a0e21a3d7a674658ed
5
5
  SHA512:
6
- metadata.gz: 225bbf4a0100ac2f80a03e3bcf38f6080365db13228fade762fa5c8378631b2f0aa5d45d71654e2ecfa6577fe88f9c317163a8c29b44f003657146870bd3c44f
7
- data.tar.gz: f3b563c5a789cb8039fc5e30e5461114556ca37289bd2b9859b0516973f1c4d7f54204745ad48628271ae24574522784cae4e8865f83e596326b5707d903a4ea
6
+ metadata.gz: ec918743d27c761eb32bdce4c3a7f79e02668ea649c6ee3cdf676408c8666866e5d1dddeb72ec725d8a1a0f738273091553d75214cadf0a3c18a1c6932795131
7
+ data.tar.gz: 2ba3990b4ed9dfb77e03bf5f9734a355f48ee9a3903afb41cd1b8e00c5f022a17b3db790e774a9d4868b8d8bd663969d278f24fc5678441c37579662d693930f
data/README.md CHANGED
@@ -152,7 +152,9 @@ bin/rails generate maquina_components:install --skip-helper
152
152
 
153
153
  | Component | Description | Documentation |
154
154
  |-----------|-------------|---------------|
155
+ | **Calendar** | Inline date picker with single/range selection | [Calendar](https://maquina.app/documentation/components/calendar/) |
155
156
  | **Combobox** | Searchable dropdown with keyboard navigation | [Combobox](https://maquina.app/documentation/components/combobox/) |
157
+ | **Date Picker** | Popover-based date selection | [Date Picker](https://maquina.app/documentation/components/date-picker/) |
156
158
  | **Toggle Group** | Single/multiple selection button group | [Toggle Group](https://maquina.app/documentation/components/toggle-group/) |
157
159
 
158
160
  ### Feedback Components
@@ -425,7 +427,9 @@ The install generator adds default theme variables. Customize them in `app/asset
425
427
 
426
428
  ### Interactive
427
429
 
430
+ - **[Calendar](https://maquina.app/documentation/components/calendar/)** — Inline date picker
428
431
  - **[Combobox](https://maquina.app/documentation/components/combobox/)** — Searchable dropdown selection
432
+ - **[Date Picker](https://maquina.app/documentation/components/date-picker/)** — Popover date selection
429
433
  - **[Toggle Group](https://maquina.app/documentation/components/toggle-group/)** — Toggle button groups
430
434
 
431
435
  ### Feedback
@@ -455,6 +459,28 @@ bin/rails test
455
459
 
456
460
  ---
457
461
 
462
+ ## Claude Code Skill
463
+
464
+ This repository includes a Claude Code skill that teaches Claude how to build consistent, accessible UIs using maquina_components. The skill provides:
465
+
466
+ - **Component catalog** — Complete reference for all components with ERB examples
467
+ - **Form patterns** — Validation, error handling, and complex form structures
468
+ - **Layout patterns** — Sidebar navigation, page structure, responsive design
469
+ - **Turbo integration** — Turbo Frames, Streams, and component updates
470
+ - **Spec checklist** — Review criteria for UI implementation quality
471
+
472
+ ### Installation
473
+
474
+ Copy the `skill/` directory to your Rails project:
475
+
476
+ ```bash
477
+ cp -r /path/to/maquina_components/skill .claude/skills/maquina-ui-standards
478
+ ```
479
+
480
+ See the [Skill README](skill/README.md) for detailed installation and usage instructions.
481
+
482
+ ---
483
+
458
484
  ## Contributing
459
485
 
460
486
  Bug reports and pull requests are welcome on GitHub at [github.com/maquina-app/maquina_components](https://github.com/maquina-app/maquina_components).
@@ -0,0 +1,222 @@
1
+ /* ===== Calendar Component Styles ===== */
2
+ /*
3
+ * A date picker calendar with single and range selection.
4
+ * Uses data attributes for styling to avoid inline utility classes.
5
+ * Fully compatible with dark mode via CSS variables.
6
+ *
7
+ * Structure:
8
+ * - calendar (root container)
9
+ * - header (navigation)
10
+ * - weekdays (day name headers)
11
+ * - grid (day buttons grid)
12
+ * - week (row)
13
+ * - day (button)
14
+ */
15
+
16
+ /* ===== Root Container ===== */
17
+ [data-component="calendar"] {
18
+ --cell-size: 2rem;
19
+
20
+ @apply p-3 w-fit rounded-lg border;
21
+ background-color: var(--background);
22
+ border-color: var(--border);
23
+ }
24
+
25
+ /* ===== Header (Navigation) ===== */
26
+ [data-calendar-part="header"] {
27
+ display: flex;
28
+ align-items: center;
29
+ justify-content: space-between;
30
+ @apply mb-4;
31
+ }
32
+
33
+ [data-calendar-part="header"] button {
34
+ display: inline-flex;
35
+ align-items: center;
36
+ justify-content: center;
37
+ width: var(--cell-size);
38
+ height: var(--cell-size);
39
+ @apply rounded-md;
40
+ border: none;
41
+ cursor: pointer;
42
+ background-color: transparent;
43
+ color: var(--foreground);
44
+ @apply transition-colors duration-150;
45
+ }
46
+
47
+ [data-calendar-part="header"] button:hover:not(:disabled) {
48
+ background-color: var(--accent);
49
+ color: var(--accent-foreground);
50
+ }
51
+
52
+ [data-calendar-part="header"] button:focus-visible {
53
+ @apply outline-none;
54
+ box-shadow: 0 0 0 2px var(--background),
55
+ 0 0 0 4px var(--ring);
56
+ }
57
+
58
+ [data-calendar-part="header"] button:disabled {
59
+ @apply opacity-50 cursor-not-allowed;
60
+ }
61
+
62
+ [data-calendar-part="header"] button svg {
63
+ @apply size-4 shrink-0;
64
+ }
65
+
66
+ [data-calendar-part="caption"] {
67
+ @apply text-sm font-medium select-none;
68
+ color: var(--foreground);
69
+ }
70
+
71
+ /* ===== Weekday Headers ===== */
72
+ [data-calendar-part="weekdays"] {
73
+ display: grid;
74
+ grid-template-columns: repeat(7, var(--cell-size));
75
+ @apply mb-1;
76
+ }
77
+
78
+ [data-calendar-part="weekday"] {
79
+ display: flex;
80
+ align-items: center;
81
+ justify-content: center;
82
+ width: var(--cell-size);
83
+ height: var(--cell-size);
84
+ @apply text-xs font-normal select-none;
85
+ color: var(--muted-foreground);
86
+ }
87
+
88
+ /* ===== Calendar Grid ===== */
89
+ [data-calendar-part="grid"] {
90
+ display: flex;
91
+ flex-direction: column;
92
+ @apply gap-1;
93
+ }
94
+
95
+ [data-calendar-part="week"] {
96
+ display: grid;
97
+ grid-template-columns: repeat(7, var(--cell-size));
98
+ }
99
+
100
+ /* ===== Day Button Base ===== */
101
+ [data-calendar-part="day"] {
102
+ display: inline-flex;
103
+ align-items: center;
104
+ justify-content: center;
105
+ width: var(--cell-size);
106
+ height: var(--cell-size);
107
+ @apply text-sm font-normal rounded-md;
108
+ border: none;
109
+ cursor: pointer;
110
+ background-color: transparent;
111
+ color: var(--foreground);
112
+ @apply transition-colors duration-150;
113
+ }
114
+
115
+ /* ===== Day States ===== */
116
+
117
+ /* Hover */
118
+ [data-calendar-part="day"]:hover:not(:disabled):not([data-state]) {
119
+ background-color: var(--accent);
120
+ color: var(--accent-foreground);
121
+ }
122
+
123
+ /* Today */
124
+ [data-calendar-part="day"][data-today] {
125
+ background-color: var(--accent);
126
+ color: var(--accent-foreground);
127
+ }
128
+
129
+ /* Selected (single mode) */
130
+ [data-calendar-part="day"][data-state="selected"] {
131
+ background-color: var(--primary);
132
+ color: var(--primary-foreground);
133
+ @apply rounded-md;
134
+ }
135
+
136
+ [data-calendar-part="day"][data-state="selected"]:hover {
137
+ background-color: var(--primary);
138
+ color: var(--primary-foreground);
139
+ }
140
+
141
+ /* Range Start */
142
+ [data-calendar-part="day"][data-state="range-start"] {
143
+ background-color: var(--primary);
144
+ color: var(--primary-foreground);
145
+ @apply rounded-l-md rounded-r-none;
146
+ }
147
+
148
+ /* Range End */
149
+ [data-calendar-part="day"][data-state="range-end"] {
150
+ background-color: var(--primary);
151
+ color: var(--primary-foreground);
152
+ @apply rounded-r-md rounded-l-none;
153
+ }
154
+
155
+ /* Range Middle */
156
+ [data-calendar-part="day"][data-state="range-middle"] {
157
+ background-color: var(--accent);
158
+ color: var(--accent-foreground);
159
+ @apply rounded-none;
160
+ }
161
+
162
+ /* Today within selection - override background */
163
+ [data-calendar-part="day"][data-today][data-state="selected"],
164
+ [data-calendar-part="day"][data-today][data-state="range-start"],
165
+ [data-calendar-part="day"][data-today][data-state="range-end"] {
166
+ background-color: var(--primary);
167
+ color: var(--primary-foreground);
168
+ }
169
+
170
+ [data-calendar-part="day"][data-today][data-state="range-middle"] {
171
+ background-color: var(--accent);
172
+ color: var(--accent-foreground);
173
+ }
174
+
175
+ /* Outside days (previous/next month) */
176
+ [data-calendar-part="day"][data-outside] {
177
+ color: var(--muted-foreground);
178
+ @apply opacity-50;
179
+ }
180
+
181
+ [data-calendar-part="day"][data-outside][data-state="selected"],
182
+ [data-calendar-part="day"][data-outside][data-state="range-start"],
183
+ [data-calendar-part="day"][data-outside][data-state="range-end"] {
184
+ color: var(--primary-foreground);
185
+ @apply opacity-30;
186
+ }
187
+
188
+ [data-calendar-part="day"][data-outside][data-state="range-middle"] {
189
+ color: var(--accent-foreground);
190
+ @apply opacity-30;
191
+ }
192
+
193
+ /* Disabled */
194
+ [data-calendar-part="day"]:disabled {
195
+ color: var(--muted-foreground);
196
+ @apply opacity-50 cursor-not-allowed;
197
+ }
198
+
199
+ /* Focus */
200
+ [data-calendar-part="day"]:focus-visible {
201
+ @apply outline-none;
202
+ position: relative;
203
+ z-index: 10;
204
+ box-shadow: 0 0 0 2px var(--background),
205
+ 0 0 0 4px var(--ring);
206
+ }
207
+
208
+ /* ===== Responsive Cell Sizes ===== */
209
+ /*
210
+ * Custom cell sizes can be set via --cell-size CSS variable:
211
+ * style="--cell-size: 2.5rem;"
212
+ *
213
+ * Or with Tailwind classes:
214
+ * css_classes: "[--cell-size:2.5rem] md:[--cell-size:3rem]"
215
+ */
216
+
217
+ /* ===== Dark Mode ===== */
218
+ /*
219
+ * Dark mode is handled automatically through CSS variables.
220
+ * The theme variables change based on the .dark class on html/body.
221
+ * No additional dark mode styles needed here.
222
+ */
@@ -0,0 +1,172 @@
1
+ /* ===== DatePicker Component Styles ===== */
2
+ /*
3
+ * A date picker with trigger button and popover calendar.
4
+ * Uses native Popover API for open/close without JavaScript.
5
+ * Supports single and range selection modes.
6
+ *
7
+ * Structure:
8
+ * - date-picker (root container)
9
+ * - trigger (button with popovertarget)
10
+ * - popover (native popover with calendar)
11
+ */
12
+
13
+ /* ===== Root Container ===== */
14
+ [data-component="date-picker"] {
15
+ position: relative;
16
+ display: inline-block;
17
+ }
18
+
19
+ /* ===== Trigger Button ===== */
20
+ [data-date-picker-part="trigger"] {
21
+ display: inline-flex;
22
+ align-items: center;
23
+ justify-content: flex-start;
24
+ gap: 0.5rem;
25
+ width: 100%;
26
+ min-width: 200px;
27
+ @apply h-9 px-3 py-2 text-sm rounded-md;
28
+ border: 1px solid var(--input);
29
+ background-color: var(--background);
30
+ color: var(--foreground);
31
+ cursor: pointer;
32
+ text-align: left;
33
+ @apply transition-colors duration-150;
34
+ }
35
+
36
+ [data-date-picker-part="trigger"]:hover:not(:disabled) {
37
+ background-color: var(--accent);
38
+ }
39
+
40
+ [data-date-picker-part="trigger"]:focus-visible {
41
+ @apply outline-none;
42
+ border-color: var(--ring);
43
+ box-shadow: 0 0 0 2px var(--background),
44
+ 0 0 0 4px var(--ring);
45
+ }
46
+
47
+ [data-date-picker-part="trigger"]:disabled {
48
+ @apply opacity-50 cursor-not-allowed;
49
+ }
50
+
51
+ [data-date-picker-part="trigger"] svg {
52
+ @apply size-4 shrink-0;
53
+ color: var(--muted-foreground);
54
+ }
55
+
56
+ /* Placeholder state */
57
+ [data-date-picker-part="trigger"]:has([data-date-picker-part="placeholder-indicator"]) {
58
+ color: var(--muted-foreground);
59
+ }
60
+
61
+ /* ===== Popover ===== */
62
+ [data-date-picker-part="popover"] {
63
+ @apply p-0 rounded-lg border;
64
+ background-color: var(--background);
65
+ border-color: var(--border);
66
+ box-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1),
67
+ 0 4px 6px -4px rgb(0 0 0 / 0.1);
68
+
69
+ /* Reset default popover styles */
70
+ margin: 0;
71
+ overflow: visible;
72
+
73
+ /* Position below trigger */
74
+ position-area: bottom span-right;
75
+ position-try-fallbacks: flip-block, flip-inline, flip-block flip-inline;
76
+ margin-top: 0.25rem;
77
+
78
+ /* Entry animation */
79
+ opacity: 1;
80
+ transform: translateY(0) scale(1);
81
+ transition: opacity 150ms ease-out,
82
+ transform 150ms ease-out;
83
+ }
84
+
85
+ /* Closed state - for exit animation */
86
+ [data-date-picker-part="popover"]:not(:popover-open) {
87
+ opacity: 0;
88
+ transform: translateY(-0.5rem) scale(0.95);
89
+ }
90
+
91
+ /* Entry animation starting state */
92
+ @starting-style {
93
+ [data-date-picker-part="popover"]:popover-open {
94
+ opacity: 0;
95
+ transform: translateY(-0.5rem) scale(0.95);
96
+ }
97
+ }
98
+
99
+ /* Backdrop */
100
+ [data-date-picker-part="popover"]::backdrop {
101
+ background-color: transparent;
102
+ }
103
+
104
+ /* ===== Calendar inside popover adjustments ===== */
105
+ [data-date-picker-part="popover"] [data-component="calendar"] {
106
+ border: none;
107
+ box-shadow: none;
108
+ }
109
+
110
+ /* ===== Trigger aria-expanded state ===== */
111
+ [data-date-picker-part="trigger"][aria-expanded="true"] {
112
+ border-color: var(--ring);
113
+ }
114
+
115
+ /* ===== Size Variants ===== */
116
+ [data-component="date-picker"][data-size="sm"] [data-date-picker-part="trigger"] {
117
+ @apply h-8 px-2 py-1 text-xs;
118
+ min-width: 160px;
119
+ }
120
+
121
+ [data-component="date-picker"][data-size="sm"] [data-date-picker-part="trigger"] svg {
122
+ @apply size-3.5;
123
+ }
124
+
125
+ [data-component="date-picker"][data-size="lg"] [data-date-picker-part="trigger"] {
126
+ @apply h-11 px-4 py-3 text-base;
127
+ min-width: 240px;
128
+ }
129
+
130
+ [data-component="date-picker"][data-size="lg"] [data-date-picker-part="trigger"] svg {
131
+ @apply size-5;
132
+ }
133
+
134
+ /* ===== Full width variant ===== */
135
+ [data-component="date-picker"][data-full-width] {
136
+ display: block;
137
+ width: 100%;
138
+ }
139
+
140
+ [data-component="date-picker"][data-full-width] [data-date-picker-part="trigger"] {
141
+ width: 100%;
142
+ }
143
+
144
+ /* ===== Error state ===== */
145
+ [data-component="date-picker"]:has(input:invalid),
146
+ [data-component="date-picker"]:has(input[aria-invalid="true"]) {
147
+ [data-date-picker-part="trigger"] {
148
+ border-color: var(--destructive);
149
+ }
150
+
151
+ [data-date-picker-part="trigger"]:focus-visible {
152
+ box-shadow: 0 0 0 2px var(--background),
153
+ 0 0 0 4px var(--destructive);
154
+ }
155
+ }
156
+
157
+ /* ===== Fallback for browsers without anchor positioning ===== */
158
+ @supports not (position-area: bottom) {
159
+ [data-date-picker-part="popover"] {
160
+ position: absolute;
161
+ top: 100%;
162
+ left: 0;
163
+ margin-top: 0.25rem;
164
+ }
165
+ }
166
+
167
+ /* ===== Dark Mode ===== */
168
+ /*
169
+ * Dark mode is handled automatically through CSS variables.
170
+ * The theme variables change based on the .dark class on html/body.
171
+ * No additional dark mode styles needed here.
172
+ */
@@ -1,18 +1,18 @@
1
1
  @source "../../../views/";
2
2
 
3
- @layer components {
4
- @import "../../stylesheets/alert.css";
5
- @import "../../stylesheets/badge.css";
6
- @import "../../stylesheets/breadcrumbs.css";
7
- @import "../../stylesheets/card.css";
8
- @import "../../stylesheets/combobox.css";
9
- @import "../../stylesheets/dropdown_menu.css";
10
- @import "../../stylesheets/empty.css";
11
- @import "../../stylesheets/form.css";
12
- @import "../../stylesheets/header.css";
13
- @import "../../stylesheets/pagination.css";
14
- @import "../../stylesheets/sidebar.css";
15
- @import "../../stylesheets/table.css";
16
- @import "../../stylesheets/toast.css";
17
- @import "../../stylesheets/toggle_group.css";
18
- }
3
+ @import "../../stylesheets/alert.css";
4
+ @import "../../stylesheets/badge.css";
5
+ @import "../../stylesheets/breadcrumbs.css";
6
+ @import "../../stylesheets/calendar.css";
7
+ @import "../../stylesheets/card.css";
8
+ @import "../../stylesheets/combobox.css";
9
+ @import "../../stylesheets/date_picker.css";
10
+ @import "../../stylesheets/dropdown_menu.css";
11
+ @import "../../stylesheets/empty.css";
12
+ @import "../../stylesheets/form.css";
13
+ @import "../../stylesheets/header.css";
14
+ @import "../../stylesheets/pagination.css";
15
+ @import "../../stylesheets/sidebar.css";
16
+ @import "../../stylesheets/table.css";
17
+ @import "../../stylesheets/toast.css";
18
+ @import "../../stylesheets/toggle_group.css";
@@ -0,0 +1,196 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MaquinaComponents
4
+ # Calendar Helper
5
+ #
6
+ # Provides utility methods for working with calendar and date picker data.
7
+ #
8
+ # @example Generate month data
9
+ # calendar_month_data(Date.current, :sunday)
10
+ #
11
+ # @example Check if date is in range
12
+ # calendar_date_in_range?(date, start_date, end_date)
13
+ #
14
+ module CalendarHelper
15
+ # Generate calendar month data
16
+ #
17
+ # @param date [Date] Any date within the target month
18
+ # @param week_starts_on [Symbol] :sunday or :monday
19
+ # @return [Hash] Month metadata and weeks array
20
+ def calendar_month_data(date, week_starts_on = :sunday)
21
+ first_of_month = date.beginning_of_month
22
+ last_of_month = date.end_of_month
23
+
24
+ # Calculate start of calendar grid
25
+ week_start = (week_starts_on == :monday) ? 1 : 0
26
+ days_before = (first_of_month.wday - week_start) % 7
27
+ calendar_start = first_of_month - days_before.days
28
+
29
+ # Calculate end of calendar grid (6 weeks max)
30
+ total_days = days_before + last_of_month.day
31
+ weeks_needed = (total_days / 7.0).ceil
32
+ weeks_needed = [weeks_needed, 6].min
33
+ calendar_end = calendar_start + (weeks_needed * 7 - 1).days
34
+
35
+ # Build weeks array
36
+ weeks = (calendar_start..calendar_end).each_slice(7).to_a
37
+
38
+ {
39
+ month: date.month,
40
+ year: date.year,
41
+ first_of_month: first_of_month,
42
+ last_of_month: last_of_month,
43
+ weeks: weeks,
44
+ week_starts_on: week_starts_on
45
+ }
46
+ end
47
+
48
+ # Format month name with year
49
+ #
50
+ # @param date [Date] Any date within the target month
51
+ # @param format [Symbol] :long (%B %Y) or :short (%b %Y)
52
+ # @return [String] Formatted month name
53
+ def calendar_month_name(date, format = :long)
54
+ case format
55
+ when :short
56
+ I18n.l(date, format: "%b %Y")
57
+ else
58
+ I18n.l(date, format: "%B %Y")
59
+ end
60
+ end
61
+
62
+ # Check if a date falls within a range
63
+ #
64
+ # @param date [Date] The date to check
65
+ # @param start_date [Date, nil] Range start (inclusive)
66
+ # @param end_date [Date, nil] Range end (inclusive)
67
+ # @return [Boolean]
68
+ def calendar_date_in_range?(date, start_date, end_date)
69
+ return false unless start_date && end_date
70
+ date.between?(start_date, end_date)
71
+ end
72
+
73
+ # Generate data attributes hash for calendar
74
+ #
75
+ # @param mode [Symbol] :single or :range
76
+ # @param selected [Date, String, nil] Selected date
77
+ # @param selected_end [Date, String, nil] End date for range
78
+ # @param month [Integer, nil] Display month
79
+ # @param year [Integer, nil] Display year
80
+ # @return [Hash] Data attributes for use with content_tag
81
+ def calendar_data_attrs(mode: :single, selected: nil, selected_end: nil, month: nil, year: nil)
82
+ selected_str = case selected
83
+ when Date, Time, DateTime then selected.to_date.iso8601
84
+ when String then selected
85
+ end
86
+
87
+ selected_end_str = case selected_end
88
+ when Date, Time, DateTime then selected_end.to_date.iso8601
89
+ when String then selected_end
90
+ end
91
+
92
+ display_date = selected_str ? Date.parse(selected_str) : Date.current
93
+ display_month = month || display_date.month
94
+ display_year = year || display_date.year
95
+
96
+ {
97
+ data: {
98
+ controller: "calendar",
99
+ component: "calendar",
100
+ "calendar-mode-value": mode,
101
+ "calendar-month-value": display_month,
102
+ "calendar-year-value": display_year,
103
+ "calendar-selected-value": selected_str,
104
+ "calendar-selected-end-value": selected_end_str
105
+ }.compact
106
+ }
107
+ end
108
+
109
+ # Get weekday names based on week start
110
+ #
111
+ # @param week_starts_on [Symbol] :sunday or :monday
112
+ # @param format [Symbol] :short (Mo, Tu) or :narrow (M, T) or :long (Monday)
113
+ # @return [Array<String>]
114
+ def calendar_weekday_names(week_starts_on = :sunday, format = :short)
115
+ names = case format
116
+ when :narrow
117
+ %w[S M T W T F S]
118
+ when :long
119
+ I18n.t("date.day_names")
120
+ else
121
+ %w[Su Mo Tu We Th Fr Sa]
122
+ end
123
+
124
+ (week_starts_on == :monday) ? names.rotate(1) : names
125
+ end
126
+
127
+ # Generate data attributes hash for date picker
128
+ #
129
+ # @param mode [Symbol] :single or :range
130
+ # @param selected [Date, String, nil] Selected date
131
+ # @param selected_end [Date, String, nil] End date for range
132
+ # @return [Hash] Data attributes for use with content_tag
133
+ def date_picker_data_attrs(mode: :single, selected: nil, selected_end: nil)
134
+ selected_str = case selected
135
+ when Date, Time, DateTime then selected.to_date.iso8601
136
+ when String then selected
137
+ end
138
+
139
+ selected_end_str = case selected_end
140
+ when Date, Time, DateTime then selected_end.to_date.iso8601
141
+ when String then selected_end
142
+ end
143
+
144
+ {
145
+ data: {
146
+ controller: "date-picker",
147
+ component: "date-picker",
148
+ "date-picker-mode-value": mode,
149
+ "date-picker-selected-value": selected_str,
150
+ "date-picker-selected-end-value": selected_end_str
151
+ }.compact
152
+ }
153
+ end
154
+
155
+ # Format date for display in date picker
156
+ #
157
+ # @param date [Date, String, nil] Date to format
158
+ # @param format [Symbol] :short, :long, or :full
159
+ # @return [String, nil]
160
+ def date_picker_format(date, format = :long)
161
+ return nil unless date
162
+
163
+ date = Date.parse(date) if date.is_a?(String)
164
+
165
+ case format
166
+ when :short
167
+ I18n.l(date, format: :short)
168
+ when :full
169
+ I18n.l(date, format: :long)
170
+ else
171
+ I18n.l(date, format: :long)
172
+ end
173
+ rescue ArgumentError
174
+ nil
175
+ end
176
+
177
+ # Format date range for display
178
+ #
179
+ # @param start_date [Date, String, nil] Start date
180
+ # @param end_date [Date, String, nil] End date
181
+ # @param format [Symbol] :short or :long
182
+ # @return [String, nil]
183
+ def date_picker_format_range(start_date, end_date, format = :short)
184
+ start_str = date_picker_format(start_date, format)
185
+ end_str = date_picker_format(end_date, format)
186
+
187
+ return nil unless start_str
188
+
189
+ if end_str
190
+ "#{start_str} - #{end_str}"
191
+ else
192
+ "#{start_str} - ..."
193
+ end
194
+ end
195
+ end
196
+ end