rails-datepicker 0.1.0

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.
Files changed (51) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +29 -0
  3. data/LICENSE.txt +21 -0
  4. data/README.md +166 -0
  5. data/app/assets/stylesheets/rails_datepicker.css +98 -0
  6. data/app/javascript/controllers/rails_datepicker_controller.js +412 -0
  7. data/app/javascript/rails-datepicker/calendar.js +236 -0
  8. data/app/javascript/rails-datepicker/index.js +1 -0
  9. data/config/importmap.rb +2 -0
  10. data/config/locales/rails_datepicker.am.yml +12 -0
  11. data/config/locales/rails_datepicker.en.yml +12 -0
  12. data/lib/generators/rails_datepicker/install_generator.rb +87 -0
  13. data/lib/generators/rails_datepicker/templates/initializer.rb.tt +9 -0
  14. data/lib/rails-datepicker.rb +4 -0
  15. data/lib/rails_datepicker/calendar.rb +150 -0
  16. data/lib/rails_datepicker/configuration.rb +55 -0
  17. data/lib/rails_datepicker/engine.rb +31 -0
  18. data/lib/rails_datepicker/form_helper.rb +267 -0
  19. data/lib/rails_datepicker/simple_form_input.rb +17 -0
  20. data/lib/rails_datepicker/version.rb +5 -0
  21. data/lib/rails_datepicker.rb +25 -0
  22. data/package.json +22 -0
  23. data/test/application_system_test_case.rb +11 -0
  24. data/test/dummy/app/controllers/application_controller.rb +4 -0
  25. data/test/dummy/app/controllers/students_controller.rb +27 -0
  26. data/test/dummy/app/javascript/application.js +6 -0
  27. data/test/dummy/app/models/application_record.rb +5 -0
  28. data/test/dummy/app/models/student.rb +5 -0
  29. data/test/dummy/app/views/layouts/application.html.erb +10 -0
  30. data/test/dummy/app/views/students/frame.html.erb +8 -0
  31. data/test/dummy/app/views/students/new.html.erb +21 -0
  32. data/test/dummy/config/application.rb +16 -0
  33. data/test/dummy/config/boot.rb +2 -0
  34. data/test/dummy/config/cable.yml +2 -0
  35. data/test/dummy/config/database.yml +3 -0
  36. data/test/dummy/config/environment.rb +4 -0
  37. data/test/dummy/config/environments/test.rb +11 -0
  38. data/test/dummy/config/importmap.rb +5 -0
  39. data/test/dummy/config/routes.rb +7 -0
  40. data/test/dummy/db/schema.rb +7 -0
  41. data/test/fixtures/tailwind.css +2 -0
  42. data/test/generators/install_generator_test.rb +53 -0
  43. data/test/helpers/form_helper_test.rb +77 -0
  44. data/test/integration/form_submission_test.rb +22 -0
  45. data/test/javascript/calendar_test.js +56 -0
  46. data/test/rails_datepicker/calendar_test.rb +57 -0
  47. data/test/rails_datepicker/configuration_test.rb +39 -0
  48. data/test/simple_form_input_test.rb +20 -0
  49. data/test/system/datepicker_test.rb +93 -0
  50. data/test/test_helper.rb +9 -0
  51. metadata +253 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: c51da7d226f8f410243d30109623c9c60e8f5443d63091974f318a775afaaa5c
4
+ data.tar.gz: 71b41a3dc594e10a95c6c579ba236f640116d5fd1188f4deaed1082c226ac9dc
5
+ SHA512:
6
+ metadata.gz: 8277f456e3205227e6016d5bcba395c3ee266ddda0221536d5da5a6c070b5d372905ce00e52366d612b1bc41016e1c585a55a991f1478e61fd6941d49eb20594
7
+ data.tar.gz: a3c4819b1bdc4a08c0ec1580ddd5fd184837921d06cd9136e832ed2926272d3000778fc973c05708e3efa4a0d4c6d7cc9b213ab0655e436da9d53b60fb6284d5
data/CHANGELOG.md ADDED
@@ -0,0 +1,29 @@
1
+ # Changelog
2
+
3
+ All notable changes follow [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
4
+
5
+ ## Unreleased
6
+
7
+ ### Changed
8
+
9
+ - Made popup calendars compact and independent of the form field width.
10
+ - Added an English/Amharic switch for all calendar controls and labels.
11
+ - Added `display_calendar` so Ethiopian calendar selection can display Gregorian results.
12
+
13
+ - Renamed the gem from `stimulus-datepicker` to `rails-datepicker`, including its Ruby namespace, generator, Stimulus identifier, and asset names.
14
+ - Made Ethiopian/Gregorian conversion accurate across Gregorian century boundaries.
15
+ - Applied each field's locale to server-rendered labels and preserved caller-supplied ARIA descriptions.
16
+ - Made disabled inline calendars inert and ensured unconstrained calendars keep selectable dates.
17
+ - Added Puma as a declared development dependency so the browser test suite can boot.
18
+
19
+ ## 0.1.0 - 2026-10-15
20
+
21
+ ### Added
22
+
23
+ - Ethiopian and Gregorian calendars with bidirectional conversion, leap years, Pagume, and New Year boundary coverage.
24
+ - English and Amharic labels, configurable week start, date format, and Ethiopic numerals.
25
+ - Accessible Stimulus 3 popover and inline calendars with keyboard navigation and responsive positioning.
26
+ - Gregorian ISO form submission for Rails form builders, nested forms, and optional Simple Form input.
27
+ - Tailwind CSS 4 light, dark, and automatic themes.
28
+ - Rails installer for Importmap and Bun/esbuild applications.
29
+ - Rails 8 dummy application, integration, generator, system, and JavaScript tests.
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Tamiru
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,166 @@
1
+ # rails-datepicker
2
+
3
+ `rails-datepicker` is a Rails 8 datepicker for Ethiopian and Gregorian dates. It uses Stimulus 3, Turbo, vanilla JavaScript, and Tailwind CSS 4. Users can change the displayed calendar without changing the selected day; forms always submit a Gregorian ISO value (`YYYY-MM-DD`) that maps directly to an Active Record `date` column.
4
+
5
+ ## Requirements
6
+
7
+ - Ruby 3.2 or newer
8
+ - Rails 8.0
9
+ - Stimulus 3 and Turbo
10
+ - Tailwind CSS 4
11
+ - Importmap, Bun, or another Rails JavaScript bundler
12
+
13
+ No Bootstrap, DaisyUI, jQuery, React, or Vue dependency is used.
14
+
15
+ ## Installation
16
+
17
+ Add the gem to the application:
18
+
19
+ ```ruby
20
+ gem "rails-datepicker"
21
+ ```
22
+
23
+ Then install it:
24
+
25
+ ```bash
26
+ bundle install
27
+ bin/rails generate rails_datepicker:install
28
+ ```
29
+
30
+ The generator:
31
+
32
+ - creates `config/initializers/rails_datepicker.rb`;
33
+ - copies the Stimulus controller and calendar module into `app/javascript`;
34
+ - adds the Importmap pin when `config/importmap.rb` exists;
35
+ - copies and imports the Tailwind CSS 4 component source.
36
+
37
+ Existing files go through the standard Rails generator collision prompt. Restart the asset watcher after installation. With Bun/esbuild, the installer rewrites the copied controller to a relative ES module import, so no package alias is needed. With Importmap, it retains the pinned module name and `stimulus-rails` discovers the copied controller automatically.
38
+
39
+ If the Tailwind entrypoint uses a custom path, add this line after `@import "tailwindcss"`:
40
+
41
+ ```css
42
+ @import "./rails_datepicker.css";
43
+ ```
44
+
45
+ ## Configuration
46
+
47
+ ```ruby
48
+ RailsDatepicker.configure do |config|
49
+ config.default_calendar = :ethiopian
50
+ config.default_locale = :am
51
+ config.date_format = "YYYY-MM-DD"
52
+ config.theme = :auto
53
+ config.week_start = :sunday
54
+ end
55
+ ```
56
+
57
+ Valid calendars are `:ethiopian` and `:gregorian`; locales are `:am` and `:en`; themes are `:auto`, `:light`, and `:dark`; week starts are `:sunday` and `:monday`.
58
+
59
+ ## Rails form builders
60
+
61
+ ```erb
62
+ <%= form_with model: @student do |f| %>
63
+ <%= f.label :birth_date %>
64
+ <%= f.datepicker_field :birth_date,
65
+ calendar: :ethiopian,
66
+ locale: :am %>
67
+ <% end %>
68
+ ```
69
+
70
+ The visible input has no `name`. A hidden input named `student[birth_date]` contains the canonical Gregorian date, so standard strong parameters and Active Record date casting work unchanged.
71
+
72
+ Standalone helper:
73
+
74
+ ```erb
75
+ <%= rails_datepicker_field(
76
+ :student,
77
+ :birth_date,
78
+ object: @student,
79
+ calendar: :ethiopian,
80
+ locale: :am
81
+ ) %>
82
+ ```
83
+
84
+ Nested `fields_for` names and IDs are generated by the Rails builder. Standard HTML options such as `required`, `disabled`, `class`, `data`, and `aria` are preserved. Model validation errors are connected to the visible input with `aria-describedby`.
85
+
86
+ ## Field options
87
+
88
+ ```erb
89
+ <%= f.datepicker_field :appointment_date,
90
+ calendar: :ethiopian,
91
+ locale: :am,
92
+ display_calendar: :gregorian,
93
+ date_format: "DD/MM/YYYY",
94
+ week_start: :monday,
95
+ theme: :auto,
96
+ ethiopic_numerals: true,
97
+ min: Date.current,
98
+ max: 2.years.from_now.to_date,
99
+ disabled_dates: holidays,
100
+ readonly: false,
101
+ inline: false,
102
+ years_before: 100,
103
+ years_after: 20 %>
104
+ ```
105
+
106
+ `display_calendar: :gregorian` keeps the visible result and manual input Gregorian while the popup can use either calendar. Omit it to display the result in the active calendar. The popup has independent calendar and English/Amharic switches.
107
+
108
+ `min`, `max`, and every `disabled_dates` entry are interpreted as canonical Gregorian dates. Manual input must exactly match `date_format`; invalid and unavailable dates use native constraint validation. `readonly: true` disables manual editing while retaining calendar selection. `inline: true` keeps the calendar visible.
109
+
110
+ ## Simple Form
111
+
112
+ When Simple Form is installed, the engine loads `RailsDatepickerInput`:
113
+
114
+ ```erb
115
+ <%= simple_form_for @student do |f| %>
116
+ <%= f.input :birth_date,
117
+ as: :rails_datepicker,
118
+ calendar: :ethiopian,
119
+ locale: :am %>
120
+ <% end %>
121
+ ```
122
+
123
+ Simple Form is optional and is not a runtime dependency.
124
+
125
+ ## Accessibility and Turbo
126
+
127
+ - The input exposes combobox/dialog relationships and validation state.
128
+ - Arrow keys move by day or week; Home/End move within a week; Page Up/Down move by month; Shift+Page Up/Down move by year; Escape closes and restores focus.
129
+ - Focus starts on the selected date, today, or the first enabled date.
130
+ - The popover flips above the field and clamps horizontally to remain inside the viewport.
131
+ - Every controller instance owns and removes its document/window listeners in `disconnect()`, including Turbo Frame replacement and Turbo cache navigation.
132
+ - Inline and popup instances are independent and work inside modal dialogs.
133
+
134
+ ## Calendar accuracy
135
+
136
+ Conversion is anchored at 2015-01-01 Ethiopian / 2022-09-11 Gregorian and advances by the Ethiopian calendar's own year lengths, so it remains accurate across Gregorian century boundaries. Ethiopian leap years satisfy `year % 4 == 3`; Pagume has six days in those years and five otherwise. In the modern 1900–2098 range, Ethiopian New Year falls on September 12 when the following Gregorian year is a leap year, and September 11 otherwise.
137
+
138
+ The Ruby and JavaScript suites include reference boundaries such as:
139
+
140
+ | Ethiopian | Gregorian |
141
+ |---|---|
142
+ | 2015-01-01 | 2022-09-11 |
143
+ | 2015-13-06 | 2023-09-11 |
144
+ | 2016-01-01 | 2023-09-12 |
145
+
146
+ Round-trip tests cover representative dates for every year from 1900 through 2100, with explicit reference assertions around the 2099/2100 century boundary.
147
+
148
+ ## Testing
149
+
150
+ ```bash
151
+ bundle exec rake test
152
+ npm test
153
+ RUN_SYSTEM_TESTS=1 bundle exec ruby -Itest test/system/datepicker_test.rb
154
+ ```
155
+
156
+ The test suite includes a Rails 8 dummy application and covers conversion, leap years, Pagume, New Year boundaries, strict input, restrictions, canonical form submission, Simple Form, multiple instances, Turbo Frame reconnection, keyboard navigation, and light/dark/inline rendering.
157
+
158
+ ## Publishing
159
+
160
+ Build locally with `gem build rails-datepicker.gemspec`. Review the gem contents with `gem contents --show-install-dir rails-datepicker` after installing the artifact. Enable MFA on the RubyGems account before publishing. The gemspec requires MFA.
161
+
162
+ The repository does not contain any automated publish step. Publishing to RubyGems and pushing to GitHub must be performed explicitly by a maintainer.
163
+
164
+ ## License
165
+
166
+ MIT.
@@ -0,0 +1,98 @@
1
+ @layer components {
2
+ .rdp {
3
+ --rdp-bg: #ffffff;
4
+ --rdp-fg: #172033;
5
+ --rdp-muted: #64748b;
6
+ --rdp-border: #cbd5e1;
7
+ --rdp-accent: #2563eb;
8
+ --rdp-accent-fg: #ffffff;
9
+ --rdp-hover: #eff6ff;
10
+ --rdp-danger: #dc2626;
11
+ position: relative;
12
+ width: 100%;
13
+ color: var(--rdp-fg);
14
+ font-family: ui-sans-serif, system-ui, sans-serif;
15
+ }
16
+
17
+ /*
18
+ * Auto theme adopts common host design tokens (including DaisyUI) while
19
+ * retaining standalone fallbacks for applications without a token system.
20
+ */
21
+ .rdp--auto {
22
+ --rdp-bg: var(--color-base-100, #ffffff);
23
+ --rdp-fg: var(--color-base-content, #172033);
24
+ --rdp-muted: color-mix(in oklab, var(--color-base-content, #172033) 60%, transparent);
25
+ --rdp-border: var(--color-base-300, #cbd5e1);
26
+ --rdp-accent: var(--color-primary, #2563eb);
27
+ --rdp-accent-fg: var(--color-primary-content, #ffffff);
28
+ --rdp-hover: color-mix(in oklab, var(--color-primary, #2563eb) 12%, var(--color-base-100, #ffffff));
29
+ --rdp-danger: var(--color-error, #dc2626);
30
+ }
31
+
32
+ .rdp__input-group { position: relative; display: flex; align-items: stretch; width: 100%; }
33
+ .rdp__input {
34
+ width: 100%; min-height: 2.5rem; border: 1px solid var(--rdp-border); border-radius: .5rem;
35
+ background: var(--rdp-bg); color: var(--rdp-fg); padding: .5rem 2.5rem .5rem .75rem;
36
+ font-size: .875rem; line-height: 1.25rem; box-shadow: 0 1px 2px rgb(15 23 42 / .05);
37
+ }
38
+ .rdp__input:focus { border-color: var(--rdp-accent); outline: 2px solid color-mix(in srgb, var(--rdp-accent) 25%, transparent); outline-offset: 1px; }
39
+ .rdp__input[aria-invalid="true"] { border-color: var(--rdp-danger); }
40
+ .rdp__input:disabled { cursor: not-allowed; opacity: .6; }
41
+ .rdp__trigger { position: absolute; inset-block: 0; inset-inline-end: 0; display: grid; width: 2.5rem; place-items: center; color: var(--rdp-muted); border-radius: 0 .5rem .5rem 0; }
42
+ .rdp__trigger:hover { color: var(--rdp-accent); background: var(--rdp-hover); }
43
+ .rdp__trigger:focus-visible, .rdp__nav:focus-visible, .rdp__day:focus-visible, .rdp__footer-button:focus-visible, .rdp__locale-toggle:focus-visible, .rdp__calendar-toggle:focus-visible { outline: 2px solid var(--rdp-accent); outline-offset: 2px; }
44
+ .rdp__icon { width: 1.125rem; height: 1.125rem; }
45
+
46
+ .rdp__popover {
47
+ position: fixed; z-index: 1000; width: 18rem; border: 1px solid var(--rdp-border); border-radius: .75rem;
48
+ background: var(--rdp-bg); color: var(--rdp-fg); padding: .625rem; box-shadow: 0 8px 24px rgb(15 23 42 / .12);
49
+ }
50
+ .rdp__popover[hidden] { display: none; }
51
+ .rdp--inline .rdp__popover { position: relative; inset: auto; z-index: auto; width: 100%; margin-top: .5rem; box-shadow: none; }
52
+ .rdp__header { display: grid; grid-template-columns: 2rem minmax(0, 1fr) 2rem; gap: .25rem; align-items: center; }
53
+ .rdp__nav { display: grid; width: 2rem; height: 2rem; place-items: center; border-radius: .375rem; color: var(--rdp-muted); font-size: 1.25rem; }
54
+ .rdp__nav:hover { background: var(--rdp-hover); color: var(--rdp-accent); }
55
+ .rdp__selectors { display: grid; grid-template-columns: minmax(0, 1fr) 4.75rem; gap: .25rem; }
56
+ .rdp__select { min-width: 0; height: 2rem; border: 1px solid var(--rdp-border); border-radius: .375rem; background: var(--rdp-bg); color: var(--rdp-fg); padding: 0 .375rem; font-size: .75rem; }
57
+ .rdp__weekdays, .rdp__grid { display: grid; grid-template-columns: repeat(7, minmax(0, 1fr)); gap: .125rem; }
58
+ .rdp__weekdays { margin-top: .75rem; }
59
+ .rdp__weekday { padding-block: .25rem; overflow: hidden; color: var(--rdp-muted); text-align: center; font-size: .6875rem; font-weight: 600; text-overflow: ellipsis; white-space: nowrap; }
60
+ .rdp__grid { margin-top: .25rem; }
61
+ .rdp__day { display: grid; height: 2rem; min-width: 0; padding: 0; place-items: center; border-radius: .375rem; font-size: .8125rem; }
62
+ .rdp__day:hover:not(:disabled) { background: var(--rdp-hover); color: var(--rdp-accent); }
63
+ .rdp__day--today { box-shadow: inset 0 0 0 1px var(--rdp-accent); color: var(--rdp-accent); font-weight: 700; }
64
+ .rdp__day--selected, .rdp__day--selected:hover { background: var(--rdp-accent); color: var(--rdp-accent-fg); font-weight: 700; }
65
+ .rdp__day:disabled { cursor: not-allowed; color: var(--rdp-muted); opacity: .35; text-decoration: line-through; }
66
+ .rdp__day--empty { visibility: hidden; }
67
+ .rdp__footer { display: flex; align-items: center; gap: .25rem; margin-top: .75rem; padding-top: .625rem; border-top: 1px solid var(--rdp-border); }
68
+ .rdp__footer-button, .rdp__locale-toggle, .rdp__calendar-toggle { border-radius: .375rem; padding: .375rem .25rem; color: var(--rdp-accent); font-size: .75rem; font-weight: 600; }
69
+ .rdp__footer-button:hover, .rdp__locale-toggle:hover, .rdp__calendar-toggle:hover { background: var(--rdp-hover); }
70
+ .rdp__locale-toggle { margin-inline-start: auto; }
71
+ .rdp__error { margin-top: .375rem; color: var(--rdp-danger); font-size: .75rem; }
72
+
73
+ .rdp--dark, .dark .rdp--auto {
74
+ --rdp-bg: #111827; --rdp-fg: #f8fafc; --rdp-muted: #94a3b8; --rdp-border: #475569;
75
+ --rdp-accent: #60a5fa; --rdp-accent-fg: #0f172a; --rdp-hover: #1e293b; --rdp-danger: #f87171;
76
+ }
77
+ }
78
+
79
+ @media (prefers-color-scheme: dark) {
80
+ .rdp--auto {
81
+ --rdp-bg: var(--color-base-100, #111827);
82
+ --rdp-fg: var(--color-base-content, #f8fafc);
83
+ --rdp-muted: color-mix(in oklab, var(--color-base-content, #f8fafc) 60%, transparent);
84
+ --rdp-border: var(--color-base-300, #475569);
85
+ --rdp-accent: var(--color-primary, #60a5fa);
86
+ --rdp-accent-fg: var(--color-primary-content, #0f172a);
87
+ --rdp-hover: color-mix(in oklab, var(--color-primary, #60a5fa) 14%, var(--color-base-100, #111827));
88
+ --rdp-danger: var(--color-error, #f87171);
89
+ }
90
+ }
91
+
92
+ @media (max-width: 360px) {
93
+ .rdp__popover { padding: .5rem; }
94
+ }
95
+
96
+ @media (forced-colors: active) {
97
+ .rdp__day--selected { outline: 2px solid Highlight; }
98
+ }