ruby_ui 1.1.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 (86) hide show
  1. checksums.yaml +4 -4
  2. data/lib/generators/ruby_ui/component_generator.rb +5 -1
  3. data/lib/generators/ruby_ui/dependencies.yml +32 -0
  4. data/lib/generators/ruby_ui/install/install_generator.rb +1 -1
  5. data/lib/generators/ruby_ui/install/templates/tailwind.css.erb +1 -1
  6. data/lib/generators/ruby_ui/javascript_utils.rb +27 -6
  7. data/lib/ruby_ui/avatar/avatar.rb +3 -0
  8. data/lib/ruby_ui/avatar/avatar_controller.js +33 -0
  9. data/lib/ruby_ui/avatar/avatar_fallback.rb +3 -0
  10. data/lib/ruby_ui/avatar/avatar_image.rb +4 -0
  11. data/lib/ruby_ui/base.rb +6 -0
  12. data/lib/ruby_ui/calendar/calendar.rb +3 -1
  13. data/lib/ruby_ui/calendar/calendar_controller.js +66 -7
  14. data/lib/ruby_ui/calendar/calendar_days.rb +20 -0
  15. data/lib/ruby_ui/calendar/calendar_docs.rb +9 -0
  16. data/lib/ruby_ui/combobox/combobox_badge.rb +17 -0
  17. data/lib/ruby_ui/combobox/combobox_badge_trigger.rb +47 -0
  18. data/lib/ruby_ui/combobox/combobox_clear_button.rb +40 -0
  19. data/lib/ruby_ui/combobox/combobox_controller.js +4 -2
  20. data/lib/ruby_ui/combobox/combobox_docs.rb +199 -64
  21. data/lib/ruby_ui/combobox/combobox_input_trigger.rb +64 -0
  22. data/lib/ruby_ui/combobox/combobox_item_indicator.rb +30 -0
  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/data_table/data_table.rb +29 -0
  29. data/lib/ruby_ui/data_table/data_table_bulk_actions.rb +18 -0
  30. data/lib/ruby_ui/data_table/data_table_column_toggle.rb +62 -0
  31. data/lib/ruby_ui/data_table/data_table_column_visibility_controller.js +14 -0
  32. data/lib/ruby_ui/data_table/data_table_controller.js +57 -0
  33. data/lib/ruby_ui/data_table/data_table_docs.rb +180 -0
  34. data/lib/ruby_ui/data_table/data_table_expand_toggle.rb +53 -0
  35. data/lib/ruby_ui/data_table/data_table_form.rb +39 -0
  36. data/lib/ruby_ui/data_table/data_table_kaminari_adapter.rb +17 -0
  37. data/lib/ruby_ui/data_table/data_table_manual_adapter.rb +17 -0
  38. data/lib/ruby_ui/data_table/data_table_pagination.rb +100 -0
  39. data/lib/ruby_ui/data_table/data_table_pagination_bar.rb +15 -0
  40. data/lib/ruby_ui/data_table/data_table_pagy_adapter.rb +17 -0
  41. data/lib/ruby_ui/data_table/data_table_per_page_select.rb +35 -0
  42. data/lib/ruby_ui/data_table/data_table_row_checkbox.rb +30 -0
  43. data/lib/ruby_ui/data_table/data_table_search.rb +57 -0
  44. data/lib/ruby_ui/data_table/data_table_search_controller.js +62 -0
  45. data/lib/ruby_ui/data_table/data_table_select_all_checkbox.rb +21 -0
  46. data/lib/ruby_ui/data_table/data_table_selection_summary.rb +25 -0
  47. data/lib/ruby_ui/data_table/data_table_sort_head.rb +112 -0
  48. data/lib/ruby_ui/data_table/data_table_toolbar.rb +15 -0
  49. data/lib/ruby_ui/date_picker/date_picker.rb +85 -0
  50. data/lib/ruby_ui/date_picker/date_picker_docs.rb +23 -0
  51. data/lib/ruby_ui/native_select/native_select.rb +39 -0
  52. data/lib/ruby_ui/native_select/native_select_docs.rb +83 -0
  53. data/lib/ruby_ui/native_select/native_select_group.rb +15 -0
  54. data/lib/ruby_ui/native_select/native_select_icon.rb +39 -0
  55. data/lib/ruby_ui/native_select/native_select_option.rb +15 -0
  56. data/lib/ruby_ui/select/select_value.rb +2 -1
  57. data/lib/ruby_ui/sheet/sheet.rb +9 -1
  58. data/lib/ruby_ui/sheet/sheet_controller.js +6 -0
  59. data/lib/ruby_ui/theme_toggle/theme_toggle.rb +14 -2
  60. data/lib/ruby_ui/theme_toggle/theme_toggle_controller.js +27 -19
  61. data/lib/ruby_ui/theme_toggle/theme_toggle_docs.rb +12 -42
  62. data/lib/ruby_ui/toast/toast.rb +18 -0
  63. data/lib/ruby_ui/toast/toast_action.rb +27 -0
  64. data/lib/ruby_ui/toast/toast_cancel.rb +27 -0
  65. data/lib/ruby_ui/toast/toast_close.rb +40 -0
  66. data/lib/ruby_ui/toast/toast_controller.js +151 -0
  67. data/lib/ruby_ui/toast/toast_description.rb +18 -0
  68. data/lib/ruby_ui/toast/toast_docs.rb +12 -0
  69. data/lib/ruby_ui/toast/toast_icon.rb +65 -0
  70. data/lib/ruby_ui/toast/toast_item.rb +72 -0
  71. data/lib/ruby_ui/toast/toast_region.rb +124 -0
  72. data/lib/ruby_ui/toast/toast_title.rb +18 -0
  73. data/lib/ruby_ui/toast/toaster_controller.js +306 -0
  74. data/lib/ruby_ui/toggle/toggle.rb +101 -0
  75. data/lib/ruby_ui/toggle/toggle_controller.js +33 -0
  76. data/lib/ruby_ui/toggle_group/toggle_group.rb +119 -0
  77. data/lib/ruby_ui/toggle_group/toggle_group_controller.js +126 -0
  78. data/lib/ruby_ui/toggle_group/toggle_group_item.rb +67 -0
  79. data/lib/ruby_ui/tooltip/tooltip_content.rb +12 -5
  80. data/lib/ruby_ui/tooltip/tooltip_controller.js +58 -22
  81. data/lib/ruby_ui/tooltip/tooltip_docs.rb +13 -0
  82. data/lib/ruby_ui/tooltip/tooltip_trigger.rb +10 -3
  83. data/lib/ruby_ui.rb +3 -1
  84. metadata +66 -10
  85. data/lib/ruby_ui/theme_toggle/set_dark_mode.rb +0 -16
  86. 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: ae8ccd52ccc249a9a1a409facc1883da7f87170bc5f70a0df32755e2fc5e5577
4
- data.tar.gz: df99edbcb78e881e98a114c58b1e0e771180d40acc0677ab182c50e29487d33d
3
+ metadata.gz: 41271e83392afeb5499d950f64ea1e5513c3694fac4521c7599f3ac1c281c979
4
+ data.tar.gz: 5370ed6872556cf2848763b09d6edddecc2b5b822357c3af35e6bbfc1eb07ae0
5
5
  SHA512:
6
- metadata.gz: 9509f7c13c94b9f1b4767cc194e7eaeecda6ffd8eeb872c97693e7cf9ac875dbefa1457edc882df7e2ac093facf77ff856f1af3191549a7f37be8d901cb00ec6
7
- data.tar.gz: 44f479fb9b95d79318e358dc4b2864a5b13cc51d1ddd958a199cd6c184ed190de8d93a07e07d416ca2d5c89e23094f1b158c187048b3030d17eadca9b23eb065
6
+ metadata.gz: e3694f2acd18f037160228f955f019c20696543b5047f87cb1a3108a272982b69c188bbbec174063add1881c29d3625f0fde34d7fe8d9dfebe9bb29621986831
7
+ data.tar.gz: f0853192ca6871211535ac8e60e1db1dfae5adcba4fe60e4284f147bfe6296ce0ded60436764ff3829db5d20c874f3efcd7f68a56f47f411900f8c95f8d6dac3
@@ -9,6 +9,7 @@ module RubyUI
9
9
  source_root File.expand_path("../../ruby_ui", __dir__)
10
10
  argument :component_name, type: :string, required: true
11
11
  class_option :force, type: :boolean, default: false
12
+ class_option :with_docs, type: :boolean, default: false
12
13
 
13
14
  def generate_component
14
15
  if component_not_found?
@@ -63,7 +64,10 @@ module RubyUI
63
64
 
64
65
  def component_folder_path = File.join(self.class.source_root, component_folder_name)
65
66
 
66
- def components_file_paths = Dir.glob(File.join(component_folder_path, "*.rb"))
67
+ def components_file_paths
68
+ files = Dir.glob(File.join(component_folder_path, "*.rb"))
69
+ options["with_docs"] ? files : files.reject { |f| f.end_with?("_docs.rb") }
70
+ end
67
71
 
68
72
  def js_controller_file_paths = Dir.glob(File.join(component_folder_path, "*.js"))
69
73
 
@@ -42,6 +42,22 @@ context_menu:
42
42
  js_packages:
43
43
  - "tippy.js"
44
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
+
45
61
  dropdown_menu:
46
62
  js_packages:
47
63
  - "@floating-ui/dom"
@@ -69,6 +85,22 @@ select:
69
85
  js_packages:
70
86
  - "@floating-ui/dom"
71
87
 
88
+ toast:
89
+ js_packages: []
90
+
72
91
  tooltip:
92
+ components:
93
+ - "Typography"
94
+
73
95
  js_packages:
74
96
  - "@floating-ui/dom"
97
+
98
+ toggle: {}
99
+
100
+ toggle_group:
101
+ components:
102
+ - "Toggle"
103
+
104
+ theme_toggle:
105
+ components:
106
+ - "Toggle"
@@ -30,7 +30,7 @@ module RubyUI
30
30
  if gem_installed?("tailwind_merge")
31
31
  say "tailwind_merge is already installed", :green
32
32
  else
33
- say "Adding phlex-rails to Gemfile"
33
+ say "Adding tailwind_merge to Gemfile"
34
34
  run %(bundle add tailwind_merge)
35
35
  end
36
36
  end
@@ -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,9 +1,13 @@
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)
9
+ elsif using_bun?
10
+ run "bun add #{package}"
7
11
  elsif using_yarn?
8
12
  run "yarn add #{package}"
9
13
  elsif using_npm?
@@ -19,6 +23,8 @@ module RubyUI
19
23
  case package
20
24
  when "motion"
21
25
  pin_motion
26
+ when "tw-animate-css"
27
+ pin_tw_animate_css
22
28
  when "tippy.js"
23
29
  pin_tippy_js
24
30
  else
@@ -27,21 +33,34 @@ module RubyUI
27
33
  end
28
34
 
29
35
  def using_importmap?
30
- 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"))
31
37
  end
32
38
 
33
- def using_npm? = File.exist?(Rails.root.join("package-lock.json"))
39
+ def using_bun? = File.exist?(rails_root.join("bun.lock"))
40
+
41
+ def using_npm? = File.exist?(rails_root.join("package-lock.json"))
42
+
43
+ def using_pnpm? = File.exist?(rails_root.join("pnpm-lock.yaml"))
44
+
45
+ def using_yarn? = File.exist?(rails_root.join("yarn.lock"))
34
46
 
35
- def using_pnpm? = File.exist?(Rails.root.join("pnpm-lock.yaml"))
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
36
51
 
37
- def using_yarn? = File.exist?(Rails.root.join("yarn.lock"))
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
38
57
 
39
58
  def pin_motion
40
59
  say <<~TEXT
41
60
  WARNING: Installing motion from CDN because `bin/importmap pin motion` doesn't download the correct file.
42
61
  TEXT
43
62
 
44
- inject_into_file Rails.root.join("config/importmap.rb"), <<~RUBY
63
+ inject_into_file rails_root.join("config/importmap.rb"), <<~RUBY
45
64
  pin "motion", to: "https://cdn.jsdelivr.net/npm/motion@11.11.17/+esm"\n
46
65
  RUBY
47
66
  end
@@ -51,11 +70,13 @@ module RubyUI
51
70
  WARNING: Installing tippy.js from CDN because `bin/importmap pin tippy.js` doesn't download the correct file.
52
71
  TEXT
53
72
 
54
- inject_into_file Rails.root.join("config/importmap.rb"), <<~RUBY
73
+ inject_into_file rails_root.join("config/importmap.rb"), <<~RUBY
55
74
  pin "tippy.js", to: "https://cdn.jsdelivr.net/npm/tippy.js@6.3.7/+esm"
56
75
  pin "@popperjs/core", to: "https://cdn.jsdelivr.net/npm/@popperjs/core@2.11.8/+esm"\n
57
76
  RUBY
58
77
  end
78
+
79
+ def rails_root = Rails.root
59
80
  end
60
81
  end
61
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))
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyUI
4
+ class ComboboxBadge < Base
5
+ def view_template(&)
6
+ span(**attrs, &)
7
+ end
8
+
9
+ private
10
+
11
+ def default_attrs
12
+ {
13
+ class: "inline-flex items-center gap-1 rounded-md border bg-secondary px-1.5 py-0.5 text-xs font-medium text-secondary-foreground"
14
+ }
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyUI
4
+ class ComboboxBadgeTrigger < Base
5
+ def initialize(placeholder: "", clear_button: false, **)
6
+ @placeholder = placeholder
7
+ @clear_button = clear_button
8
+ super(**)
9
+ end
10
+
11
+ def view_template(&)
12
+ div(**attrs) do
13
+ div(data: {ruby_ui__combobox_target: "badgeContainer"}, class: "hidden")
14
+ input(
15
+ type: "text",
16
+ class: "flex-1 min-w-8 bg-transparent border-0 px-0 outline-none focus:ring-0 placeholder:text-muted-foreground text-sm",
17
+ autocomplete: "off",
18
+ autocorrect: "off",
19
+ spellcheck: "false",
20
+ placeholder: @placeholder,
21
+ data: {
22
+ ruby_ui__combobox_target: "badgeInput",
23
+ action: "keyup->ruby-ui--combobox#filterItems input->ruby-ui--combobox#filterItems keydown.backspace->ruby-ui--combobox#handleBadgeInputBackspace"
24
+ }
25
+ )
26
+ render ComboboxClearButton.new if @clear_button
27
+ end
28
+ end
29
+
30
+ private
31
+
32
+ # JS-toggled classes (referenced here so Tailwind compiles them): h-auto min-h-9 pt-1.5
33
+ def default_attrs
34
+ {
35
+ class: "flex h-9 w-full flex-wrap items-center gap-1 rounded-md border border-input bg-background px-3 text-sm ring-offset-background focus-within:ring-2 focus-within:ring-ring focus-within:ring-offset-2 cursor-text",
36
+ data: {
37
+ ruby_ui__combobox_target: "trigger",
38
+ action: "click->ruby-ui--combobox#openPopover focusin->ruby-ui--combobox#openPopover"
39
+ },
40
+ aria: {
41
+ haspopup: "listbox",
42
+ expanded: "false"
43
+ }
44
+ }
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyUI
4
+ class ComboboxClearButton < Base
5
+ def view_template
6
+ button(**attrs) do
7
+ svg(
8
+ xmlns: "http://www.w3.org/2000/svg",
9
+ width: "24",
10
+ height: "24",
11
+ viewbox: "0 0 24 24",
12
+ fill: "none",
13
+ stroke: "currentColor",
14
+ stroke_width: "2",
15
+ stroke_linecap: "round",
16
+ stroke_linejoin: "round",
17
+ class: "size-3.5"
18
+ ) do |s|
19
+ s.path(d: "M18 6 6 18")
20
+ s.path(d: "m6 6 12 12")
21
+ end
22
+ end
23
+ end
24
+
25
+ private
26
+
27
+ def default_attrs
28
+ {
29
+ type: "button",
30
+ class: "ml-auto shrink-0 rounded-sm text-muted-foreground hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring hidden",
31
+ aria: {label: "Clear selection"},
32
+ data: {
33
+ ruby_ui__combobox_target: "clearButton",
34
+ # JS implementation in combobox_controller.js
35
+ action: "ruby-ui--combobox#clearAll"
36
+ }
37
+ }
38
+ end
39
+ end
40
+ end
@@ -4,7 +4,8 @@ import { computePosition, autoUpdate, offset, flip } from "@floating-ui/dom";
4
4
  // Connects to data-controller="ruby-ui--combobox"
5
5
  export default class extends Controller {
6
6
  static values = {
7
- term: String
7
+ term: String,
8
+ minPopoverWidth: { type: Number, default: 240 }
8
9
  }
9
10
 
10
11
  static targets = [
@@ -186,6 +187,7 @@ export default class extends Controller {
186
187
  }
187
188
 
188
189
  updatePopoverWidth() {
189
- this.popoverTarget.style.width = `${this.triggerTarget.offsetWidth}px`
190
+ const width = Math.max(this.triggerTarget.offsetWidth, this.minPopoverWidthValue)
191
+ this.popoverTarget.style.width = `${width}px`
190
192
  }
191
193
  }