ruby_ui 1.0.0 → 1.0.2

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 (68) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +5 -0
  3. data/lib/generators/ruby_ui/component/all_generator.rb +22 -0
  4. data/lib/generators/ruby_ui/component_generator.rb +4 -3
  5. data/lib/generators/ruby_ui/install/install_generator.rb +1 -7
  6. data/lib/generators/ruby_ui/install/templates/tailwind.css.erb +0 -3
  7. data/lib/generators/ruby_ui/javascript_utils.rb +4 -0
  8. data/lib/ruby_ui/alert_dialog/alert_dialog_content.rb +2 -3
  9. data/lib/ruby_ui/button/button.rb +32 -16
  10. data/lib/ruby_ui/checkbox/checkbox.rb +7 -1
  11. data/lib/ruby_ui/combobox/combobox.rb +3 -2
  12. data/lib/ruby_ui/combobox/combobox_checkbox.rb +4 -2
  13. data/lib/ruby_ui/combobox/combobox_controller.js +4 -4
  14. data/lib/ruby_ui/combobox/combobox_item.rb +2 -1
  15. data/lib/ruby_ui/combobox/combobox_radio.rb +8 -1
  16. data/lib/ruby_ui/combobox/combobox_search_input.rb +11 -5
  17. data/lib/ruby_ui/combobox/combobox_toggle_all_checkbox.rb +2 -1
  18. data/lib/ruby_ui/combobox/combobox_trigger.rb +7 -1
  19. data/lib/ruby_ui/command/command_controller.js +0 -1
  20. data/lib/ruby_ui/dialog/dialog_content.rb +2 -3
  21. data/lib/ruby_ui/dialog/dialog_controller.js +1 -1
  22. data/lib/ruby_ui/dropdown_menu/dropdown_menu.rb +9 -0
  23. data/lib/ruby_ui/dropdown_menu/dropdown_menu_content.rb +17 -2
  24. data/lib/ruby_ui/dropdown_menu/dropdown_menu_controller.js +43 -14
  25. data/lib/ruby_ui/form/form_field_label.rb +7 -1
  26. data/lib/ruby_ui/input/input.rb +8 -1
  27. data/lib/ruby_ui/link/link.rb +32 -16
  28. data/lib/ruby_ui/radio_button/radio_button.rb +3 -1
  29. data/lib/ruby_ui/select/select_item.rb +14 -5
  30. data/lib/ruby_ui/select/select_trigger.rb +9 -4
  31. data/lib/ruby_ui/sidebar/collapsible_sidebar.rb +99 -0
  32. data/lib/ruby_ui/sidebar/mobile_sidebar.rb +45 -0
  33. data/lib/ruby_ui/sidebar/non_collapsible_sidebar.rb +17 -0
  34. data/lib/ruby_ui/sidebar/sidebar.rb +29 -0
  35. data/lib/ruby_ui/sidebar/sidebar_content.rb +20 -0
  36. data/lib/ruby_ui/sidebar/sidebar_controller.js +67 -0
  37. data/lib/ruby_ui/sidebar/sidebar_footer.rb +20 -0
  38. data/lib/ruby_ui/sidebar/sidebar_group.rb +20 -0
  39. data/lib/ruby_ui/sidebar/sidebar_group_action.rb +33 -0
  40. data/lib/ruby_ui/sidebar/sidebar_group_content.rb +20 -0
  41. data/lib/ruby_ui/sidebar/sidebar_group_label.rb +26 -0
  42. data/lib/ruby_ui/sidebar/sidebar_header.rb +20 -0
  43. data/lib/ruby_ui/sidebar/sidebar_input.rb +20 -0
  44. data/lib/ruby_ui/sidebar/sidebar_inset.rb +23 -0
  45. data/lib/ruby_ui/sidebar/sidebar_menu.rb +20 -0
  46. data/lib/ruby_ui/sidebar/sidebar_menu_action.rb +48 -0
  47. data/lib/ruby_ui/sidebar/sidebar_menu_badge.rb +30 -0
  48. data/lib/ruby_ui/sidebar/sidebar_menu_button.rb +63 -0
  49. data/lib/ruby_ui/sidebar/sidebar_menu_item.rb +20 -0
  50. data/lib/ruby_ui/sidebar/sidebar_menu_skeleton.rb +36 -0
  51. data/lib/ruby_ui/sidebar/sidebar_menu_sub.rb +24 -0
  52. data/lib/ruby_ui/sidebar/sidebar_menu_sub_button.rb +50 -0
  53. data/lib/ruby_ui/sidebar/sidebar_menu_sub_item.rb +9 -0
  54. data/lib/ruby_ui/sidebar/sidebar_rail.rb +36 -0
  55. data/lib/ruby_ui/sidebar/sidebar_separator.rb +20 -0
  56. data/lib/ruby_ui/sidebar/sidebar_trigger.rb +42 -0
  57. data/lib/ruby_ui/sidebar/sidebar_wrapper.rb +24 -0
  58. data/lib/ruby_ui/switch/switch.rb +12 -2
  59. data/lib/ruby_ui/table/table_footer.rb +1 -1
  60. data/lib/ruby_ui/table/table_row.rb +1 -1
  61. data/lib/ruby_ui/tabs/tabs_trigger.rb +7 -1
  62. data/lib/ruby_ui/textarea/textarea.rb +8 -1
  63. data/lib/ruby_ui/theme_toggle/set_dark_mode.rb +16 -0
  64. data/lib/ruby_ui/theme_toggle/set_light_mode.rb +16 -0
  65. data/lib/ruby_ui/theme_toggle/theme_toggle.rb +0 -32
  66. data/lib/ruby_ui/tooltip/tooltip_controller.js +5 -4
  67. data/lib/ruby_ui.rb +1 -1
  68. metadata +34 -4
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 81eaeaadec3023c062c306ca82163e3ea43f5263a05cfa8241dbf9f3ff28e3f2
4
- data.tar.gz: 1c9036dff1203041c8c34dac6b7a5a9c530abeeba9e69b658aa5064b1ea23dfe
3
+ metadata.gz: 0caf3d89b3375e334dc257510c34bf7f168b295534dc8bf2bfc4dde95d3a6a04
4
+ data.tar.gz: 55dcc86d6feb87270a5c82915fa635e752b1a7ae1a91d5b30e12af61f91fc13f
5
5
  SHA512:
6
- metadata.gz: 250e76eff54d1edcfb57f375ac6c0944182f95c3135002a9810d1a33814bbdc9c77b50dfaf4071316f1407282a81383dac841aa486a2337d1b3d1e4c2cf58eb8
7
- data.tar.gz: 28f38979ad9df377fa7a941bd6593683ed19d1e18a116bda8f6ba4ea99d5e11e4ceb153eb4a056cdab936ed0f8239918df6023833c60b28af274c3707d4c543d
6
+ metadata.gz: abcfe605c2dc17b11f2c3cc8dbe5e2db054c95f5e0dac319c883762add2985fb0b7ddd8547c8148504e75ef83971ee7c55c874bffa3505deefe50eb7e8e09f6a
7
+ data.tar.gz: fbe88cc5c6c8e327209588628340d1225773dcd0a6ce064a657ccaa9eaa163c98d4d2280e966932bdf2c9e7da910ab0a6913cb65093cb4091c026690f88039ac
data/README.md CHANGED
@@ -26,6 +26,9 @@ Use this as a reference to build your own component libraries.
26
26
 
27
27
  ## Installation 🚀
28
28
 
29
+ > [!NOTE]
30
+ > RubyUI 1.0 requires Ruby 3.2 or later
31
+
29
32
  ### 1. Install the gem
30
33
 
31
34
  ```bash
@@ -52,6 +55,8 @@ You can generate your components using `ruby_ui:component` generator.
52
55
  bin/rails g ruby_ui:component Accordion
53
56
  ```
54
57
 
58
+ You also can generate all components using `ruby_ui:component:all` generator
59
+
55
60
  ## Documentation 📖
56
61
 
57
62
  Visit https://rubyui.com/docs/introduction to view the full documentation, including:
@@ -0,0 +1,22 @@
1
+ module RubyUI
2
+ module Generators
3
+ module Component
4
+ class AllGenerator < Rails::Generators::Base
5
+ namespace "ruby_ui:component:all"
6
+
7
+ source_root File.expand_path("../../../ruby_ui", __dir__)
8
+ class_option :force, type: :boolean, default: false
9
+
10
+ def generate_components
11
+ say "Generating all components..."
12
+
13
+ Dir.children(self.class.source_root).each do |folder_name|
14
+ next if folder_name.ends_with?(".rb")
15
+
16
+ run "bin/rails generate ruby_ui:component #{folder_name} --force #{options["force"]}"
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -8,6 +8,7 @@ module RubyUI
8
8
 
9
9
  source_root File.expand_path("../../ruby_ui", __dir__)
10
10
  argument :component_name, type: :string, required: true
11
+ class_option :force, type: :boolean, default: false
11
12
 
12
13
  def generate_component
13
14
  if component_not_found?
@@ -23,7 +24,7 @@ module RubyUI
23
24
 
24
25
  components_file_paths.each do |file_path|
25
26
  component_file_name = file_path.split("/").last
26
- copy_file file_path, Rails.root.join("app/components/ruby_ui", component_folder_name, component_file_name)
27
+ copy_file file_path, Rails.root.join("app/components/ruby_ui", component_folder_name, component_file_name), force: options["force"]
27
28
  end
28
29
  end
29
30
 
@@ -34,7 +35,7 @@ module RubyUI
34
35
 
35
36
  js_controller_file_paths.each do |file_path|
36
37
  controller_file_name = file_path.split("/").last
37
- copy_file file_path, Rails.root.join("app/javascript/controllers/ruby_ui", controller_file_name)
38
+ copy_file file_path, Rails.root.join("app/javascript/controllers/ruby_ui", controller_file_name), force: options["force"]
38
39
  end
39
40
 
40
41
  # Importmap doesn't have controller manifest, instead it uses `eagerLoadControllersFrom("controllers", application)`
@@ -68,7 +69,7 @@ module RubyUI
68
69
 
69
70
  def install_components_dependencies(components)
70
71
  components&.each do |component|
71
- run "bin/rails generate ruby_ui:component #{component}"
72
+ run "bin/rails generate ruby_ui:component #{component} --force #{options["force"]}"
72
73
  end
73
74
  end
74
75
 
@@ -42,7 +42,7 @@ module RubyUI
42
42
 
43
43
  def add_ruby_ui_module_to_components_base
44
44
  say "Adding RubyUI Kit to Components::Base"
45
- insert_into_file Rails.root.join("app/components/base.rb"), after: "include Components" do
45
+ insert_into_file Rails.root.join("app/components/base.rb"), after: "class Components::Base < Phlex::HTML" do
46
46
  "\n include RubyUI"
47
47
  end
48
48
  end
@@ -62,12 +62,6 @@ module RubyUI
62
62
  def install_tailwind_plugins
63
63
  say "Installing tw-animate-css plugin"
64
64
  install_js_package("tw-animate-css")
65
-
66
- say "Installing @tailwindcss/forms plugin"
67
- install_js_package("@tailwindcss/forms")
68
-
69
- say "Installing @tailwindcss/typography plugin"
70
- install_js_package("@tailwindcss/typography")
71
65
  end
72
66
 
73
67
  def add_ruby_ui_base
@@ -1,8 +1,5 @@
1
1
  @import "tailwindcss";
2
2
 
3
- @plugin "@tailwindcss/forms";
4
- @plugin "@tailwindcss/typography";
5
-
6
3
  <% if using_importmap? %>
7
4
  @import "../../../vendor/javascript/tw-animate-css.js";
8
5
  <% else %>
@@ -8,6 +8,8 @@ module RubyUI
8
8
  run "yarn add #{package}"
9
9
  elsif using_npm?
10
10
  run "npm install #{package}"
11
+ elsif using_pnpm?
12
+ run "pnpm install #{package}"
11
13
  else
12
14
  say "Could not detect the package manager, you need to install '#{package}' manually", :red
13
15
  end
@@ -30,6 +32,8 @@ module RubyUI
30
32
 
31
33
  def using_npm? = File.exist?(Rails.root.join("package-lock.json"))
32
34
 
35
+ def using_pnpm? = File.exist?(Rails.root.join("pnpm-lock.yaml"))
36
+
33
37
  def using_yarn? = File.exist?(Rails.root.join("yarn.lock"))
34
38
 
35
39
  def pin_motion
@@ -14,8 +14,7 @@ module RubyUI
14
14
  def background
15
15
  div(
16
16
  data_state: "open",
17
- class:
18
- "fixed inset-0 z-50 bg-black/80 backdrop-blur-sm data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
17
+ class: "fixed inset-0 z-50 bg-black/80 backdrop-blur-sm data-[state=open]:animate-in",
19
18
  style: "pointer-events:auto",
20
19
  data_aria_hidden: "true",
21
20
  aria_hidden: "true"
@@ -26,7 +25,7 @@ module RubyUI
26
25
  div(
27
26
  role: "alertdialog",
28
27
  data_state: "open",
29
- class: "flex flex-col fixed left-[50%] top-[50%] z-50 w-full max-w-lg max-h-screen overflow-y-auto translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg md:w-full",
28
+ class: "flex flex-col fixed left-[50%] top-[50%] z-50 w-full max-w-lg max-h-screen overflow-y-auto translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 sm:rounded-lg md:w-full",
30
29
  style: "pointer-events:auto",
31
30
  &
32
31
  )
@@ -2,6 +2,13 @@
2
2
 
3
3
  module RubyUI
4
4
  class Button < Base
5
+ BASE_CLASSES = [
6
+ "whitespace-nowrap inline-flex items-center justify-center rounded-md font-medium transition-colors",
7
+ "disabled:pointer-events-none disabled:opacity-50",
8
+ "focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring",
9
+ "aria-disabled:pointer-events-none aria-disabled:opacity-50 aria-disabled:cursor-not-allowed"
10
+ ].freeze
11
+
5
12
  def initialize(type: :button, variant: :primary, size: :md, icon: false, **attrs)
6
13
  @type = type
7
14
  @variant = variant.to_sym
@@ -36,43 +43,55 @@ module RubyUI
36
43
 
37
44
  def primary_classes
38
45
  [
39
- "whitespace-nowrap inline-flex items-center justify-center rounded-md font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 bg-primary text-primary-foreground shadow hover:bg-primary/90",
40
- size_classes
46
+ BASE_CLASSES,
47
+ size_classes,
48
+ "bg-primary text-primary-foreground shadow",
49
+ "hover:bg-primary/90"
41
50
  ]
42
51
  end
43
52
 
44
53
  def link_classes
45
54
  [
46
- "whitespace-nowrap inline-flex items-center justify-center rounded-md font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 text-primary underline-offset-4 hover:underline",
47
- size_classes
55
+ BASE_CLASSES,
56
+ size_classes,
57
+ "text-primary underline-offset-4",
58
+ "hover:underline"
48
59
  ]
49
60
  end
50
61
 
51
62
  def secondary_classes
52
63
  [
53
- "whitespace-nowrap inline-flex items-center justify-center rounded-md font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 bg-secondary text-secondary-foreground hover:bg-opacity-80",
54
- size_classes
64
+ BASE_CLASSES,
65
+ size_classes,
66
+ "bg-secondary text-secondary-foreground",
67
+ "hover:bg-opacity-80"
55
68
  ]
56
69
  end
57
70
 
58
71
  def destructive_classes
59
72
  [
60
- "whitespace-nowrap inline-flex items-center justify-center rounded-md font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
61
- size_classes
73
+ BASE_CLASSES,
74
+ size_classes,
75
+ "bg-destructive text-white shadow-sm",
76
+ "[a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20",
77
+ "dark:focus-visible:ring-destructive/40 dark:bg-destructive/60"
62
78
  ]
63
79
  end
64
80
 
65
81
  def outline_classes
66
82
  [
67
- "whitespace-nowrap inline-flex items-center justify-center rounded-md font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
68
- size_classes
83
+ BASE_CLASSES,
84
+ size_classes,
85
+ "border border-input bg-background shadow-sm",
86
+ "hover:bg-accent hover:text-accent-foreground"
69
87
  ]
70
88
  end
71
89
 
72
90
  def ghost_classes
73
91
  [
74
- "whitespace-nowrap inline-flex items-center justify-center rounded-md font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 hover:bg-accent hover:text-accent-foreground",
75
- size_classes
92
+ BASE_CLASSES,
93
+ size_classes,
94
+ "hover:bg-accent hover:text-accent-foreground"
76
95
  ]
77
96
  end
78
97
 
@@ -88,10 +107,7 @@ module RubyUI
88
107
  end
89
108
 
90
109
  def default_attrs
91
- {
92
- type: @type,
93
- class: default_classes
94
- }
110
+ {type: @type, class: default_classes}
95
111
  end
96
112
  end
97
113
  end
@@ -16,7 +16,13 @@ module RubyUI
16
16
  ruby_ui__checkbox_group_target: "checkbox",
17
17
  action: "change->ruby-ui--checkbox-group#onChange change->ruby-ui--form-field#onInput invalid->ruby-ui--form-field#onInvalid"
18
18
  },
19
- class: "peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 accent-primary"
19
+ class: [
20
+ "peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background accent-primary",
21
+ "disabled:cursor-not-allowed disabled:opacity-50",
22
+ "checked:bg-primary checked:text-primary-foreground",
23
+ "aria-disabled:cursor-not-allowed aria-disabled:opacity-50 aria-disabled:pointer-events-none",
24
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
25
+ ]
20
26
  }
21
27
  end
22
28
  end
@@ -2,7 +2,7 @@
2
2
 
3
3
  module RubyUI
4
4
  class Combobox < Base
5
- def initialize(term: "items", **)
5
+ def initialize(term: nil, **)
6
6
  @term = term
7
7
  super(**)
8
8
  end
@@ -18,7 +18,8 @@ module RubyUI
18
18
  role: "combobox",
19
19
  data: {
20
20
  controller: "ruby-ui--combobox",
21
- ruby_ui__combobox_term_value: @term.to_s
21
+ ruby_ui__combobox_term_value: @term,
22
+ action: "turbo:morph@window->ruby-ui--combobox#updateTriggerContent"
22
23
  }
23
24
  }
24
25
  end
@@ -12,8 +12,10 @@ module RubyUI
12
12
  {
13
13
  class: [
14
14
  "peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background accent-primary",
15
- "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
16
- "disabled:cursor-not-allowed disabled:opacity-50"
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"
17
19
  ],
18
20
  data: {
19
21
  ruby_ui__combobox_target: "input",
@@ -53,12 +53,12 @@ export default class extends Controller {
53
53
  updateTriggerContent() {
54
54
  const checkedInputs = this.inputTargets.filter(input => input.checked)
55
55
 
56
- if (checkedInputs.length == 0) {
56
+ if (checkedInputs.length === 0) {
57
57
  this.triggerContentTarget.innerText = this.triggerTarget.dataset.placeholder
58
- } else if (checkedInputs.length === 1) {
59
- this.triggerContentTarget.innerText = this.inputContent(checkedInputs[0])
60
- } else {
58
+ } else if (this.termValue && checkedInputs.length > 1) {
61
59
  this.triggerContentTarget.innerText = `${checkedInputs.length} ${this.termValue}`
60
+ } else {
61
+ this.triggerContentTarget.innerText = checkedInputs.map((input) => this.inputContent(input)).join(", ")
62
62
  }
63
63
  }
64
64
 
@@ -13,7 +13,8 @@ module RubyUI
13
13
  class: [
14
14
  "flex flex-row w-full text-wrap [&>span,&>div]:truncate gap-2 items-center rounded-sm px-2 py-1 text-sm outline-none cursor-pointer",
15
15
  "select-none has-[:checked]:bg-accent hover:bg-accent p-2",
16
- "[&>svg]:pointer-events-none [&>svg]:size-4 [&>svg]:shrink-0 aria-[current=true]:bg-accent aria-[current=true]:ring aria-[current=true]:ring-offset-2"
16
+ "[&>svg]:pointer-events-none [&>svg]:size-4 [&>svg]:shrink-0 aria-[current=true]:bg-accent aria-[current=true]:ring aria-[current=true]:ring-offset-2",
17
+ "has-disabled:opacity-50 has-disabled:cursor-not-allowed"
17
18
  ],
18
19
  role: "option",
19
20
  data: {
@@ -10,7 +10,14 @@ module RubyUI
10
10
 
11
11
  def default_attrs
12
12
  {
13
- class: "aspect-square h-4 w-4 rounded-full border border-primary accent-primary text-primary shadow focus:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
13
+ class: [
14
+ "aspect-square h-4 w-4 rounded-full border border-primary accent-primary text-primary shadow",
15
+ "focus:outline-none",
16
+ "focus-visible:ring-1 focus-visible:ring-ring",
17
+ "disabled:cursor-not-allowed disabled:opacity-50",
18
+ "checked:bg-primary checked:text-primary-foreground",
19
+ "aria-disabled:cursor-not-allowed aria-disabled:opacity-50 aria-disabled:pointer-events-none"
20
+ ],
14
21
  data: {
15
22
  ruby_ui__combobox_target: "input",
16
23
  ruby_ui__form_field_target: "input",
@@ -19,16 +19,22 @@ module RubyUI
19
19
  def default_attrs
20
20
  {
21
21
  type: "search",
22
- class: "flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-none border-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50",
23
22
  role: "searchbox",
23
+ autocorrect: "off",
24
+ autocomplete: "off",
25
+ spellcheck: "false",
24
26
  placeholder: @placeholder,
27
+ class: [
28
+ "flex h-9 w-full rounded-md bg-transparent py-3 text-sm outline-none border-none",
29
+ "focus:ring-0",
30
+ "placeholder:text-muted-foreground",
31
+ "disabled:cursor-not-allowed disabled:opacity-50",
32
+ "aria-disabled:cursor-not-allowed aria-disabled:opacity-50 aria-disabled:pointer-events-none"
33
+ ],
25
34
  data: {
26
35
  ruby_ui__combobox_target: "searchInput",
27
36
  action: "keyup->ruby-ui--combobox#filterItems search->ruby-ui--combobox#filterItems"
28
- },
29
- autocomplete: "off",
30
- autocorrect: "off",
31
- spellcheck: "false"
37
+ }
32
38
  }
33
39
  end
34
40
 
@@ -13,7 +13,8 @@ module RubyUI
13
13
  class: [
14
14
  "peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background accent-primary",
15
15
  "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
16
- "disabled:cursor-not-allowed disabled:opacity-50"
16
+ "disabled:cursor-not-allowed disabled:opacity-50",
17
+ "aria-disabled:cursor-not-allowed aria-disabled:opacity-50 aria-disabled:pointer-events-none"
17
18
  ],
18
19
  data: {
19
20
  ruby_ui__combobox_target: "toggleAll",
@@ -21,7 +21,13 @@ module RubyUI
21
21
  def default_attrs
22
22
  {
23
23
  type: "button",
24
- class: "flex h-full w-full items-center whitespace-nowrap 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 border border-input bg-background hover:bg-accent hover:text-accent-foreground h-10 px-4 py-2 justify-between",
24
+ class: [
25
+ "flex h-full w-full items-center whitespace-nowrap rounded-md text-sm ring-offset-background transition-colors border border-input bg-background h-9 px-4 py-2 justify-between",
26
+ "hover:bg-accent hover:text-accent-foreground",
27
+ "disabled:pointer-events-none disabled:opacity-50",
28
+ "aria-disabled:pointer-events-none aria-disabled:opacity-50 aria-disabled:cursor-not-allowed",
29
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
30
+ ],
25
31
  data: {
26
32
  placeholder: @placeholder,
27
33
  ruby_ui__combobox_target: "trigger",
@@ -34,7 +34,6 @@ export default class extends Controller {
34
34
  // allow scroll on body
35
35
  document.body.classList.remove("overflow-hidden");
36
36
  // remove the element
37
- console.log("this.element", this.element);
38
37
  this.element.remove();
39
38
  }
40
39
 
@@ -34,7 +34,7 @@ module RubyUI
34
34
  {
35
35
  data_state: "open",
36
36
  class: [
37
- "fixed flex flex-col pointer-events-auto left-[50%] top-[50%] z-50 w-full max-h-screen overflow-y-auto translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg md:w-full",
37
+ "fixed flex flex-col pointer-events-auto left-[50%] top-[50%] z-50 w-full max-h-screen overflow-y-auto translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 sm:rounded-lg md:w-full",
38
38
  SIZES[@size]
39
39
  ]
40
40
  }
@@ -70,8 +70,7 @@ module RubyUI
70
70
  div(
71
71
  data_state: "open",
72
72
  data_action: "click->ruby-ui--dialog#dismiss esc->ruby-ui--dialog#dismiss",
73
- class:
74
- "fixed pointer-events-auto inset-0 z-50 bg-background/80 backdrop-blur-sm data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0"
73
+ class: "fixed pointer-events-auto inset-0 z-50 bg-background/80 backdrop-blur-sm data-[state=open]:animate-in data-[state=open]:fade-in-0"
75
74
  )
76
75
  end
77
76
  end
@@ -17,7 +17,7 @@ export default class extends Controller {
17
17
  }
18
18
 
19
19
  open(e) {
20
- e.preventDefault()
20
+ e?.preventDefault();
21
21
  document.body.insertAdjacentHTML('beforeend', this.contentTarget.innerHTML)
22
22
  // prevent scroll on body
23
23
  document.body.classList.add('overflow-hidden')
@@ -15,6 +15,11 @@ module RubyUI
15
15
 
16
16
  def default_attrs
17
17
  {
18
+ class: [
19
+ "z-50",
20
+ "group/dropdown-menu",
21
+ (strategy == "absolute") ? "is-absolute" : "is-fixed"
22
+ ],
18
23
  data: {
19
24
  controller: "ruby-ui--dropdown-menu",
20
25
  action: "click@window->ruby-ui--dropdown-menu#onClickOutside",
@@ -22,5 +27,9 @@ module RubyUI
22
27
  }
23
28
  }
24
29
  end
30
+
31
+ def strategy
32
+ @_strategy ||= @options[:strategy] || "absolute"
33
+ end
25
34
  end
26
35
  end
@@ -3,7 +3,7 @@
3
3
  module RubyUI
4
4
  class DropdownMenuContent < Base
5
5
  def view_template(&block)
6
- div(data: {ruby_ui__dropdown_menu_target: "content"}, class: "hidden", style: "width: max-content; position: absolute; top: 0; left: 0;") do
6
+ div(**wrapper_attrs) do
7
7
  div(**attrs, &block)
8
8
  end
9
9
  end
@@ -15,7 +15,22 @@ module RubyUI
15
15
  data: {
16
16
  state: :open
17
17
  },
18
- class: "z-50 min-w-[8rem] overflow-hidden rounded-md border bg-background p-1 text-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 w-56"
18
+ class: "z-50 min-w-[8rem] rounded-md border bg-background p-1 text-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 w-56"
19
+ }
20
+ end
21
+
22
+ def wrapper_attrs
23
+ {
24
+ class: [
25
+ "z-50 hidden group-[.is-absolute]/dropdown-menu:absolute",
26
+ "group-[.is-fixed]/dropdown-menu:fixed"
27
+ ],
28
+ data: {ruby_ui__dropdown_menu_target: "content"},
29
+ style: {
30
+ width: "max-content",
31
+ top: "0",
32
+ left: "0"
33
+ }
19
34
  }
20
35
  end
21
36
  end
@@ -1,5 +1,11 @@
1
1
  import { Controller } from "@hotwired/stimulus";
2
- import { computePosition, flip, shift, offset } from "@floating-ui/dom";
2
+ import {
3
+ computePosition,
4
+ flip,
5
+ shift,
6
+ offset,
7
+ autoUpdate,
8
+ } from "@floating-ui/dom";
3
9
 
4
10
  export default class extends Controller {
5
11
  static targets = ["trigger", "content", "menuItem"];
@@ -12,17 +18,34 @@ export default class extends Controller {
12
18
  type: Object,
13
19
  default: {},
14
20
  },
15
- }
21
+ };
16
22
 
17
23
  connect() {
18
24
  this.boundHandleKeydown = this.#handleKeydown.bind(this); // Bind the function so we can remove it later
19
25
  this.selectedIndex = -1;
26
+
27
+ this.#setupAutoUpdate();
28
+ }
29
+
30
+ disconnect() {
31
+ if (this.autoUpdateCleanup) {
32
+ this.autoUpdateCleanup();
33
+ }
34
+ }
35
+
36
+ #setupAutoUpdate() {
37
+ this.autoUpdateCleanup = autoUpdate(
38
+ this.triggerTarget,
39
+ this.contentTarget,
40
+ this.#computeTooltip.bind(this),
41
+ );
20
42
  }
21
43
 
22
44
  #computeTooltip() {
23
45
  computePosition(this.triggerTarget, this.contentTarget, {
24
46
  placement: this.optionsValue.placement || "top",
25
47
  middleware: [flip(), shift(), offset(8)],
48
+ strategy: this.optionsValue.strategy || "absolute",
26
49
  }).then(({ x, y }) => {
27
50
  Object.assign(this.contentTarget.style, {
28
51
  left: `${x}px`,
@@ -40,14 +63,16 @@ export default class extends Controller {
40
63
  }
41
64
 
42
65
  toggle() {
43
- this.contentTarget.classList.contains("hidden") ? this.#open() : this.close();
66
+ this.contentTarget.classList.contains("hidden")
67
+ ? this.#open()
68
+ : this.close();
44
69
  }
45
70
 
46
71
  #open() {
47
72
  this.openValue = true;
48
73
  this.#deselectAll();
49
74
  this.#addEventListeners();
50
- this.#computeTooltip()
75
+ this.#computeTooltip();
51
76
  this.contentTarget.classList.remove("hidden");
52
77
  }
53
78
 
@@ -59,15 +84,17 @@ export default class extends Controller {
59
84
 
60
85
  #handleKeydown(e) {
61
86
  // return if no menu items (one line fix for)
62
- if (this.menuItemTargets.length === 0) { return; }
87
+ if (this.menuItemTargets.length === 0) {
88
+ return;
89
+ }
63
90
 
64
- if (e.key === 'ArrowDown') {
91
+ if (e.key === "ArrowDown") {
65
92
  e.preventDefault();
66
93
  this.#updateSelectedItem(1);
67
- } else if (e.key === 'ArrowUp') {
94
+ } else if (e.key === "ArrowUp") {
68
95
  e.preventDefault();
69
96
  this.#updateSelectedItem(-1);
70
- } else if (e.key === 'Enter' && this.selectedIndex !== -1) {
97
+ } else if (e.key === "Enter" && this.selectedIndex !== -1) {
71
98
  e.preventDefault();
72
99
  this.menuItemTargets[this.selectedIndex].click();
73
100
  }
@@ -76,7 +103,7 @@ export default class extends Controller {
76
103
  #updateSelectedItem(direction) {
77
104
  // Check if any of the menuItemTargets have aria-selected="true" and set the selectedIndex to that index
78
105
  this.menuItemTargets.forEach((item, index) => {
79
- if (item.getAttribute('aria-selected') === 'true') {
106
+ if (item.getAttribute("aria-selected") === "true") {
80
107
  this.selectedIndex = index;
81
108
  }
82
109
  });
@@ -99,22 +126,24 @@ export default class extends Controller {
99
126
  #toggleAriaSelected(element, isSelected) {
100
127
  // Add or remove attribute
101
128
  if (isSelected) {
102
- element.setAttribute('aria-selected', 'true');
129
+ element.setAttribute("aria-selected", "true");
103
130
  } else {
104
- element.removeAttribute('aria-selected');
131
+ element.removeAttribute("aria-selected");
105
132
  }
106
133
  }
107
134
 
108
135
  #deselectAll() {
109
- this.menuItemTargets.forEach(item => this.#toggleAriaSelected(item, false));
136
+ this.menuItemTargets.forEach((item) =>
137
+ this.#toggleAriaSelected(item, false),
138
+ );
110
139
  this.selectedIndex = -1;
111
140
  }
112
141
 
113
142
  #addEventListeners() {
114
- document.addEventListener('keydown', this.boundHandleKeydown);
143
+ document.addEventListener("keydown", this.boundHandleKeydown);
115
144
  }
116
145
 
117
146
  #removeEventListeners() {
118
- document.removeEventListener('keydown', this.boundHandleKeydown);
147
+ document.removeEventListener("keydown", this.boundHandleKeydown);
119
148
  }
120
149
  }
@@ -9,7 +9,13 @@ module RubyUI
9
9
  private
10
10
 
11
11
  def default_attrs
12
- {class: "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"}
12
+ {
13
+ class: [
14
+ "text-sm font-medium leading-none",
15
+ "peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
16
+ "peer-aria-disabled:cursor-not-allowed peer-aria-disabled:opacity-70 peer-aria-disabled:pointer-events-none"
17
+ ]
18
+ }
13
19
  end
14
20
  end
15
21
  end