ruby_ui 1.2.0 → 1.3.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 (62) hide show
  1. checksums.yaml +4 -4
  2. data/lib/generators/ruby_ui/dependencies.yml +32 -10
  3. data/lib/generators/ruby_ui/install/templates/tailwind.css.erb +1 -1
  4. data/lib/generators/ruby_ui/javascript_utils.rb +24 -7
  5. data/lib/ruby_ui/avatar/avatar.rb +3 -0
  6. data/lib/ruby_ui/avatar/avatar_controller.js +33 -0
  7. data/lib/ruby_ui/avatar/avatar_fallback.rb +3 -0
  8. data/lib/ruby_ui/avatar/avatar_image.rb +4 -0
  9. data/lib/ruby_ui/base.rb +6 -0
  10. data/lib/ruby_ui/calendar/calendar.rb +3 -1
  11. data/lib/ruby_ui/calendar/calendar_controller.js +66 -7
  12. data/lib/ruby_ui/calendar/calendar_days.rb +20 -0
  13. data/lib/ruby_ui/calendar/calendar_docs.rb +9 -0
  14. data/lib/ruby_ui/combobox/combobox.rb +1 -7
  15. data/lib/ruby_ui/combobox/combobox_checkbox.rb +7 -1
  16. data/lib/ruby_ui/combobox/combobox_controller.js +56 -244
  17. data/lib/ruby_ui/combobox/combobox_item.rb +7 -5
  18. data/lib/ruby_ui/combobox/combobox_list_group.rb +1 -1
  19. data/lib/ruby_ui/combobox/combobox_popover.rb +5 -0
  20. data/lib/ruby_ui/combobox/combobox_radio.rb +8 -1
  21. data/lib/ruby_ui/combobox/combobox_toggle_all_checkbox.rb +7 -1
  22. data/lib/ruby_ui/combobox/combobox_trigger.rb +19 -19
  23. data/lib/ruby_ui/command/command_controller.js +10 -19
  24. data/lib/ruby_ui/command/command_dialog.rb +4 -1
  25. data/lib/ruby_ui/command/command_dialog_content.rb +2 -2
  26. data/lib/ruby_ui/command/command_dialog_controller.js +34 -0
  27. data/lib/ruby_ui/command/command_dialog_trigger.rb +2 -2
  28. data/lib/ruby_ui/date_picker/date_picker.rb +85 -0
  29. data/lib/ruby_ui/date_picker/date_picker_docs.rb +23 -0
  30. data/lib/ruby_ui/masked_input/masked_input.rb +1 -11
  31. data/lib/ruby_ui/masked_input/masked_input_controller.js +0 -13
  32. data/lib/ruby_ui/select/select_value.rb +2 -1
  33. data/lib/ruby_ui/sheet/sheet.rb +9 -1
  34. data/lib/ruby_ui/sheet/sheet_controller.js +6 -0
  35. data/lib/ruby_ui/theme_toggle/theme_toggle.rb +14 -2
  36. data/lib/ruby_ui/theme_toggle/theme_toggle_controller.js +27 -19
  37. data/lib/ruby_ui/theme_toggle/theme_toggle_docs.rb +12 -42
  38. data/lib/ruby_ui/toast/toast.rb +18 -0
  39. data/lib/ruby_ui/toast/toast_action.rb +27 -0
  40. data/lib/ruby_ui/toast/toast_cancel.rb +27 -0
  41. data/lib/ruby_ui/toast/toast_close.rb +40 -0
  42. data/lib/ruby_ui/toast/toast_controller.js +151 -0
  43. data/lib/ruby_ui/toast/toast_description.rb +18 -0
  44. data/lib/ruby_ui/toast/toast_docs.rb +12 -0
  45. data/lib/ruby_ui/toast/toast_icon.rb +65 -0
  46. data/lib/ruby_ui/toast/toast_item.rb +72 -0
  47. data/lib/ruby_ui/toast/toast_region.rb +124 -0
  48. data/lib/ruby_ui/toast/toast_title.rb +18 -0
  49. data/lib/ruby_ui/toast/toaster_controller.js +306 -0
  50. data/lib/ruby_ui/toggle/toggle.rb +101 -0
  51. data/lib/ruby_ui/toggle/toggle_controller.js +33 -0
  52. data/lib/ruby_ui/toggle_group/toggle_group.rb +119 -0
  53. data/lib/ruby_ui/toggle_group/toggle_group_controller.js +126 -0
  54. data/lib/ruby_ui/toggle_group/toggle_group_item.rb +67 -0
  55. data/lib/ruby_ui/tooltip/tooltip_content.rb +12 -5
  56. data/lib/ruby_ui/tooltip/tooltip_controller.js +58 -22
  57. data/lib/ruby_ui/tooltip/tooltip_docs.rb +13 -0
  58. data/lib/ruby_ui/tooltip/tooltip_trigger.rb +10 -3
  59. data/lib/ruby_ui.rb +3 -1
  60. metadata +30 -14
  61. data/lib/ruby_ui/theme_toggle/set_dark_mode.rb +0 -16
  62. data/lib/ruby_ui/theme_toggle/set_light_mode.rb +0 -16
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 488fdc5b97a189ce4916f776af376a06760255437f681b3e3ef7e9aa0998e2b3
4
- data.tar.gz: ff44a35b46e45d1cf1b510716fdc174fc50e229ef7a82d81c8318d51911beb3d
3
+ metadata.gz: 41271e83392afeb5499d950f64ea1e5513c3694fac4521c7599f3ac1c281c979
4
+ data.tar.gz: 5370ed6872556cf2848763b09d6edddecc2b5b822357c3af35e6bbfc1eb07ae0
5
5
  SHA512:
6
- metadata.gz: 7f7fe5941b4a9e4375d280efd31ab1e35dca88b699cf4580f6722294c4b8fe161240bbe7bce09bf5f75d5a41a9c2d62e6c24ab9a3b4d4f32ef8062fa4e81d5b3
7
- data.tar.gz: 0b3e785f7380c7ba5abc0e721d5d17c3a5708a554b13bb2cd946e8019241642ebd7dfb1b3cfc46b759d14e18e37d82cbf0f874824a0489ddf8d09003bda1d9d1
6
+ metadata.gz: e3694f2acd18f037160228f955f019c20696543b5047f87cb1a3108a272982b69c188bbbec174063add1881c29d3625f0fde34d7fe8d9dfebe9bb29621986831
7
+ data.tar.gz: f0853192ca6871211535ac8e60e1db1dfae5adcba4fe60e4284f147bfe6296ce0ded60436764ff3829db5d20c874f3efcd7f68a56f47f411900f8c95f8d6dac3
@@ -2,16 +2,6 @@ accordion:
2
2
  js_packages:
3
3
  - "motion"
4
4
 
5
- data_table:
6
- components:
7
- - "Table"
8
- - "Checkbox"
9
- - "NativeSelect"
10
- - "Pagination"
11
- - "DropdownMenu"
12
- - "Input"
13
- - "Button"
14
-
15
5
  alert_dialog:
16
6
  components:
17
7
  - "Button"
@@ -52,6 +42,22 @@ context_menu:
52
42
  js_packages:
53
43
  - "tippy.js"
54
44
 
45
+ date_picker:
46
+ components:
47
+ - "Input"
48
+ - "Popover"
49
+ - "Calendar"
50
+
51
+ data_table:
52
+ components:
53
+ - "Button"
54
+ - "Checkbox"
55
+ - "DropdownMenu"
56
+ - "Input"
57
+ - "NativeSelect"
58
+ - "Pagination"
59
+ - "Table"
60
+
55
61
  dropdown_menu:
56
62
  js_packages:
57
63
  - "@floating-ui/dom"
@@ -79,6 +85,22 @@ select:
79
85
  js_packages:
80
86
  - "@floating-ui/dom"
81
87
 
88
+ toast:
89
+ js_packages: []
90
+
82
91
  tooltip:
92
+ components:
93
+ - "Typography"
94
+
83
95
  js_packages:
84
96
  - "@floating-ui/dom"
97
+
98
+ toggle: {}
99
+
100
+ toggle_group:
101
+ components:
102
+ - "Toggle"
103
+
104
+ theme_toggle:
105
+ components:
106
+ - "Toggle"
@@ -1,7 +1,7 @@
1
1
  @import "tailwindcss";
2
2
 
3
3
  <% if using_importmap? %>
4
- @import "../../../vendor/javascript/tw-animate-css.js";
4
+ @import "../../../vendor/javascript/tw-animate-css.css";
5
5
  <% else %>
6
6
  @import "tw-animate-css";
7
7
  <% end %>
@@ -1,6 +1,8 @@
1
1
  module RubyUI
2
2
  module Generators
3
3
  module JavascriptUtils
4
+ TW_ANIMATE_CSS_VERSION = "1.4.0"
5
+
4
6
  def install_js_package(package)
5
7
  if using_importmap?
6
8
  pin_with_importmap(package)
@@ -21,6 +23,8 @@ module RubyUI
21
23
  case package
22
24
  when "motion"
23
25
  pin_motion
26
+ when "tw-animate-css"
27
+ pin_tw_animate_css
24
28
  when "tippy.js"
25
29
  pin_tippy_js
26
30
  else
@@ -29,23 +33,34 @@ module RubyUI
29
33
  end
30
34
 
31
35
  def using_importmap?
32
- File.exist?(Rails.root.join("config/importmap.rb")) && File.exist?(Rails.root.join("bin/importmap"))
36
+ File.exist?(rails_root.join("config/importmap.rb")) && File.exist?(rails_root.join("bin/importmap"))
33
37
  end
34
38
 
35
- def using_bun? = File.exist?(Rails.root.join("bun.lock"))
39
+ def using_bun? = File.exist?(rails_root.join("bun.lock"))
40
+
41
+ def using_npm? = File.exist?(rails_root.join("package-lock.json"))
36
42
 
37
- def using_npm? = File.exist?(Rails.root.join("package-lock.json"))
43
+ def using_pnpm? = File.exist?(rails_root.join("pnpm-lock.yaml"))
38
44
 
39
- def using_pnpm? = File.exist?(Rails.root.join("pnpm-lock.yaml"))
45
+ def using_yarn? = File.exist?(rails_root.join("yarn.lock"))
40
46
 
41
- def using_yarn? = File.exist?(Rails.root.join("yarn.lock"))
47
+ def pin_tw_animate_css
48
+ say <<~TEXT
49
+ WARNING: Installing tw-animate-css as a CSS asset because Importmap cannot pin CSS-only package exports.
50
+ TEXT
51
+
52
+ empty_directory rails_root.join("vendor/javascript")
53
+ # CDN serves "tw-animate.css"; we save as "tw-animate-css.css" to match package name. Do not "correct" the URL.
54
+ get "https://cdn.jsdelivr.net/npm/tw-animate-css@#{TW_ANIMATE_CSS_VERSION}/dist/tw-animate.css",
55
+ rails_root.join("vendor/javascript/tw-animate-css.css")
56
+ end
42
57
 
43
58
  def pin_motion
44
59
  say <<~TEXT
45
60
  WARNING: Installing motion from CDN because `bin/importmap pin motion` doesn't download the correct file.
46
61
  TEXT
47
62
 
48
- inject_into_file Rails.root.join("config/importmap.rb"), <<~RUBY
63
+ inject_into_file rails_root.join("config/importmap.rb"), <<~RUBY
49
64
  pin "motion", to: "https://cdn.jsdelivr.net/npm/motion@11.11.17/+esm"\n
50
65
  RUBY
51
66
  end
@@ -55,11 +70,13 @@ module RubyUI
55
70
  WARNING: Installing tippy.js from CDN because `bin/importmap pin tippy.js` doesn't download the correct file.
56
71
  TEXT
57
72
 
58
- inject_into_file Rails.root.join("config/importmap.rb"), <<~RUBY
73
+ inject_into_file rails_root.join("config/importmap.rb"), <<~RUBY
59
74
  pin "tippy.js", to: "https://cdn.jsdelivr.net/npm/tippy.js@6.3.7/+esm"
60
75
  pin "@popperjs/core", to: "https://cdn.jsdelivr.net/npm/@popperjs/core@2.11.8/+esm"\n
61
76
  RUBY
62
77
  end
78
+
79
+ def rails_root = Rails.root
63
80
  end
64
81
  end
65
82
  end
@@ -24,6 +24,9 @@ module RubyUI
24
24
 
25
25
  def default_attrs
26
26
  {
27
+ data: {
28
+ controller: "ruby-ui--avatar"
29
+ },
27
30
  class: ["relative flex shrink-0 overflow-hidden rounded-full", @size_classes]
28
31
  }
29
32
  end
@@ -0,0 +1,33 @@
1
+ import { Controller } from "@hotwired/stimulus";
2
+
3
+ export default class extends Controller {
4
+ static targets = ["image", "fallback"];
5
+
6
+ connect() {
7
+ if (!this.hasImageTarget) {
8
+ return;
9
+ }
10
+
11
+ if (this.imageTarget.complete && this.imageTarget.naturalWidth > 0) {
12
+ this.showImage();
13
+ } else {
14
+ // Image not yet loaded (or failed): hide it so the fallback shows.
15
+ // Image visibility is restored by the load/error handlers.
16
+ this.showFallback();
17
+ }
18
+ }
19
+
20
+ showImage() {
21
+ this.imageTargets.forEach((image) => image.classList.remove("hidden"));
22
+ this.fallbackTargets.forEach((fallback) =>
23
+ fallback.classList.add("hidden"),
24
+ );
25
+ }
26
+
27
+ showFallback() {
28
+ this.imageTargets.forEach((image) => image.classList.add("hidden"));
29
+ this.fallbackTargets.forEach((fallback) =>
30
+ fallback.classList.remove("hidden"),
31
+ );
32
+ }
33
+ }
@@ -10,6 +10,9 @@ module RubyUI
10
10
 
11
11
  def default_attrs
12
12
  {
13
+ data: {
14
+ ruby_ui__avatar_target: "fallback"
15
+ },
13
16
  class: "flex h-full w-full items-center justify-center rounded-full bg-muted"
14
17
  }
15
18
  end
@@ -17,6 +17,10 @@ module RubyUI
17
17
  def default_attrs
18
18
  {
19
19
  loading: "lazy",
20
+ data: {
21
+ ruby_ui__avatar_target: "image",
22
+ action: "load->ruby-ui--avatar#showImage error->ruby-ui--avatar#showFallback"
23
+ },
20
24
  class: "aspect-square h-full w-full",
21
25
  alt: @alt,
22
26
  src: @src
data/lib/ruby_ui/base.rb CHANGED
@@ -18,5 +18,11 @@ module RubyUI
18
18
  def default_attrs
19
19
  {}
20
20
  end
21
+
22
+ if defined?(Rails) && Rails.env.development?
23
+ def before_template
24
+ comment { "Before #{self.class.name}" }
25
+ end
26
+ end
21
27
  end
22
28
  end
@@ -2,8 +2,9 @@
2
2
 
3
3
  module RubyUI
4
4
  class Calendar < Base
5
- def initialize(selected_date: nil, input_id: nil, date_format: "yyyy-MM-dd", **attrs)
5
+ def initialize(selected_date: nil, min_date: nil, input_id: nil, date_format: "yyyy-MM-dd", **attrs)
6
6
  @selected_date = selected_date
7
+ @min_date = min_date
7
8
  @input_id = input_id
8
9
  @date_format = date_format
9
10
  super(**attrs)
@@ -30,6 +31,7 @@ module RubyUI
30
31
  data: {
31
32
  controller: "ruby-ui--calendar",
32
33
  ruby_ui__calendar_selected_date_value: @selected_date&.to_s,
34
+ ruby_ui__calendar_min_date_value: @min_date&.to_s,
33
35
  ruby_ui__calendar_format_value: @date_format,
34
36
  ruby_ui__calendar_ruby_ui__calendar_input_outlet: @input_id
35
37
  }
@@ -6,6 +6,7 @@ export default class extends Controller {
6
6
  "calendar",
7
7
  "title",
8
8
  "weekdaysTemplate",
9
+ "disabledDateTemplate",
9
10
  "selectedDateTemplate",
10
11
  "todayDateTemplate",
11
12
  "currentMonthDateTemplate",
@@ -16,6 +17,10 @@ export default class extends Controller {
16
17
  type: String,
17
18
  default: null,
18
19
  },
20
+ minDate: {
21
+ type: String,
22
+ default: null,
23
+ },
19
24
  viewDate: {
20
25
  type: String,
21
26
  default: new Date().toISOString().slice(0, 10),
@@ -43,13 +48,21 @@ export default class extends Controller {
43
48
 
44
49
  selectDay(e) {
45
50
  e.preventDefault();
51
+ if (this.isDateDisabled(e.currentTarget.dataset.day)) return;
52
+
46
53
  // Set the selected date value
47
54
  this.selectedDateValue = e.currentTarget.dataset.day;
48
55
  }
49
56
 
50
57
  selectedDateValueChanged(value, prevValue) {
58
+ const selectedDate = this.selectedDate();
59
+ if (!selectedDate) {
60
+ this.updateCalendar();
61
+ return;
62
+ }
63
+
51
64
  // update the viewDateValue to the first day of month of the selected date (This will trigger updateCalendar() function)
52
- const newViewDate = new Date(this.selectedDateValue);
65
+ const newViewDate = new Date(selectedDate);
53
66
  newViewDate.setDate(2); // set the day to the 2nd (to avoid issues with months with different number of days and timezones)
54
67
  this.viewDateValue = newViewDate.toISOString().slice(0, 10);
55
68
 
@@ -58,7 +71,7 @@ export default class extends Controller {
58
71
 
59
72
  // update the input value
60
73
  this.rubyUiCalendarInputOutlets.forEach((outlet) => {
61
- const formattedDate = this.formatDate(this.selectedDate());
74
+ const formattedDate = this.formatDate(selectedDate);
62
75
  outlet.setValue(formattedDate);
63
76
  });
64
77
  }
@@ -101,10 +114,20 @@ export default class extends Controller {
101
114
 
102
115
  renderDay(day) {
103
116
  const today = new Date();
117
+ const selectedDate = this.selectedDate();
104
118
  let dateHTML = "";
105
119
  const data = { day: day, dayDate: day.getDate() };
106
120
 
107
- if (day.toDateString() === this.selectedDate().toDateString()) {
121
+ if (this.isDateDisabled(day)) {
122
+ // disabledDate
123
+ dateHTML = Mustache.render(
124
+ this.disabledDateTemplateTarget.innerHTML,
125
+ data,
126
+ );
127
+ } else if (
128
+ selectedDate &&
129
+ day.toDateString() === selectedDate.toDateString()
130
+ ) {
108
131
  // selectedDate
109
132
  // Render the selected date template target innerHTML with Mustache
110
133
  dateHTML = Mustache.render(
@@ -137,13 +160,13 @@ export default class extends Controller {
137
160
  }
138
161
 
139
162
  selectedDate() {
140
- return new Date(this.selectedDateValue);
163
+ return this.parseDate(this.selectedDateValue);
141
164
  }
142
165
 
143
166
  viewDate() {
144
- return this.viewDateValue
145
- ? new Date(this.viewDateValue)
146
- : this.selectedDate();
167
+ return (
168
+ this.parseDate(this.viewDateValue) || this.selectedDate() || new Date()
169
+ );
147
170
  }
148
171
 
149
172
  getFullWeeksStartAndEndInMonth() {
@@ -246,4 +269,40 @@ export default class extends Controller {
246
269
  return "th";
247
270
  }
248
271
  }
272
+
273
+ minDate() {
274
+ return this.parseDate(this.minDateValue);
275
+ }
276
+
277
+ isDateDisabled(date) {
278
+ const minDate = this.minDate();
279
+ const candidate = this.parseDate(date);
280
+
281
+ if (!minDate || !candidate) return false;
282
+
283
+ return this.startOfDay(candidate) < this.startOfDay(minDate);
284
+ }
285
+
286
+ parseDate(value) {
287
+ if (!value) return null;
288
+ if (value instanceof Date) return new Date(value);
289
+
290
+ const isoDate = value.toString().match(/^(\d{4})-(\d{2})-(\d{2})/);
291
+ if (isoDate) {
292
+ return new Date(
293
+ Number(isoDate[1]),
294
+ Number(isoDate[2]) - 1,
295
+ Number(isoDate[3]),
296
+ );
297
+ }
298
+
299
+ const date = new Date(value);
300
+ return Number.isNaN(date.getTime()) ? null : date;
301
+ }
302
+
303
+ startOfDay(date) {
304
+ const normalizedDate = new Date(date);
305
+ normalizedDate.setHours(0, 0, 0, 0);
306
+ return normalizedDate;
307
+ }
249
308
  }
@@ -5,6 +5,7 @@ module RubyUI
5
5
  BASE_CLASS = "inline-flex items-center justify-center rounded-md text-sm ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 h-8 w-8 p-0 font-normal aria-selected:opacity-100"
6
6
 
7
7
  def view_template
8
+ render_disabled_date_template
8
9
  render_selected_date_template
9
10
  render_today_date_template
10
11
  render_current_month_date_template
@@ -13,6 +14,25 @@ module RubyUI
13
14
 
14
15
  private
15
16
 
17
+ def render_disabled_date_template
18
+ date_template("disabledDateTemplate") do
19
+ button(
20
+ data_day: "{{day}}",
21
+ name: "day",
22
+ class:
23
+ [
24
+ BASE_CLASS,
25
+ "cursor-not-allowed bg-background text-muted-foreground hover:bg-background hover:text-muted-foreground"
26
+ ],
27
+ disabled: true,
28
+ role: "gridcell",
29
+ tabindex: "-1",
30
+ type: "button",
31
+ aria_disabled: "true"
32
+ ) { "{{dayDate}}" }
33
+ end
34
+ end
35
+
16
36
  def render_selected_date_template
17
37
  date_template("selectedDateTemplate") do
18
38
  button(
@@ -26,6 +26,15 @@ class Views::Docs::Calendar < Views::Base
26
26
  RUBY
27
27
  end
28
28
 
29
+ render Docs::VisualCodeExample.new(title: "Minimum date", description: "Disable dates before a given date", context: self) do
30
+ <<~RUBY
31
+ div(class: 'space-y-4') do
32
+ Input(type: 'string', placeholder: "Select a date", class: 'rounded-md border shadow', id: 'minimum-date', data_controller: 'ruby-ui--calendar-input')
33
+ Calendar(input_id: '#minimum-date', min_date: Date.current, class: 'rounded-md border shadow')
34
+ end
35
+ RUBY
36
+ end
37
+
29
38
  render Components::ComponentSetup::Tabs.new(component_name: component)
30
39
 
31
40
  render Docs::ComponentsTable.new(component_files(component))
@@ -19,13 +19,7 @@ module RubyUI
19
19
  data: {
20
20
  controller: "ruby-ui--combobox",
21
21
  ruby_ui__combobox_term_value: @term,
22
- action: %w[
23
- turbo:morph@window->ruby-ui--combobox#updateTriggerContent
24
- keydown.down->ruby-ui--combobox#keyDownPressed
25
- keydown.up->ruby-ui--combobox#keyUpPressed
26
- keydown.enter->ruby-ui--combobox#keyEnterPressed
27
- keydown.esc->ruby-ui--combobox#closePopover:prevent
28
- ]
22
+ action: "turbo:morph@window->ruby-ui--combobox#updateTriggerContent"
29
23
  }
30
24
  }
31
25
  end
@@ -10,7 +10,13 @@ module RubyUI
10
10
 
11
11
  def default_attrs
12
12
  {
13
- class: "peer sr-only",
13
+ class: [
14
+ "peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background accent-primary",
15
+ "disabled:cursor-not-allowed disabled:opacity-50",
16
+ "checked:bg-primary checked:text-primary-foreground",
17
+ "aria-disabled:cursor-not-allowed aria-disabled:opacity-50 aria-disabled:pointer-events-none",
18
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
19
+ ],
14
20
  data: {
15
21
  ruby_ui__combobox_target: "input",
16
22
  action: "ruby-ui--combobox#inputChanged"