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 +4 -4
- data/README.md +26 -0
- data/app/assets/stylesheets/calendar.css +222 -0
- data/app/assets/stylesheets/date_picker.css +172 -0
- data/app/assets/tailwind/maquina_components_engine/engine.css +16 -16
- data/app/helpers/maquina_components/calendar_helper.rb +196 -0
- data/app/helpers/maquina_components/icons_helper.rb +220 -0
- data/app/helpers/maquina_components/table_helper.rb +9 -10
- data/app/javascript/controllers/calendar_controller.js +394 -0
- data/app/javascript/controllers/date_picker_controller.js +261 -0
- data/app/views/components/_calendar.html.erb +121 -0
- data/app/views/components/_date_picker.html.erb +102 -0
- data/app/views/components/calendar/_header.html.erb +22 -0
- data/app/views/components/calendar/_week.html.erb +53 -0
- data/lib/maquina_components/version.rb +1 -1
- metadata +10 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1449d3d6edf1341e73d321b0821c1e3e355659022865465949ebf2202426d7b5
|
|
4
|
+
data.tar.gz: ffbe3396ad981086e0979756e401942410a66e2257db71a0e21a3d7a674658ed
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
@
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|