ruby_ui 1.2.0 → 1.4.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.
- checksums.yaml +4 -4
- data/README.md +6 -0
- data/lib/generators/ruby_ui/component/all_generator.rb +6 -4
- data/lib/generators/ruby_ui/component_generator.rb +51 -31
- data/lib/generators/ruby_ui/dependencies.yml +32 -10
- data/lib/generators/ruby_ui/install/templates/tailwind.css.erb +1 -1
- data/lib/generators/ruby_ui/javascript_utils.rb +24 -7
- data/lib/ruby_ui/accordion/accordion_content.rb +4 -2
- data/lib/ruby_ui/accordion/accordion_controller.js +19 -4
- data/lib/ruby_ui/avatar/avatar.rb +3 -0
- data/lib/ruby_ui/avatar/avatar_controller.js +33 -0
- data/lib/ruby_ui/avatar/avatar_fallback.rb +3 -0
- data/lib/ruby_ui/avatar/avatar_image.rb +9 -1
- data/lib/ruby_ui/base.rb +6 -0
- data/lib/ruby_ui/calendar/calendar.rb +3 -1
- data/lib/ruby_ui/calendar/calendar_controller.js +66 -7
- data/lib/ruby_ui/calendar/calendar_days.rb +20 -0
- data/lib/ruby_ui/calendar/calendar_docs.rb +9 -0
- data/lib/ruby_ui/combobox/combobox.rb +1 -7
- data/lib/ruby_ui/combobox/combobox_checkbox.rb +7 -1
- data/lib/ruby_ui/combobox/combobox_controller.js +56 -244
- data/lib/ruby_ui/combobox/combobox_item.rb +7 -5
- data/lib/ruby_ui/combobox/combobox_list_group.rb +1 -1
- data/lib/ruby_ui/combobox/combobox_popover.rb +5 -0
- data/lib/ruby_ui/combobox/combobox_radio.rb +8 -1
- data/lib/ruby_ui/combobox/combobox_toggle_all_checkbox.rb +7 -1
- data/lib/ruby_ui/combobox/combobox_trigger.rb +19 -19
- data/lib/ruby_ui/command/command_controller.js +10 -19
- data/lib/ruby_ui/command/command_dialog.rb +4 -1
- data/lib/ruby_ui/command/command_dialog_content.rb +2 -2
- data/lib/ruby_ui/command/command_dialog_controller.js +34 -0
- data/lib/ruby_ui/command/command_dialog_trigger.rb +2 -2
- data/lib/ruby_ui/data_table/data_table_docs.rb +1 -1
- data/lib/ruby_ui/date_picker/date_picker.rb +85 -0
- data/lib/ruby_ui/date_picker/date_picker_docs.rb +23 -0
- data/lib/ruby_ui/dialog/dialog_content.rb +7 -19
- data/lib/ruby_ui/dialog/dialog_controller.js +22 -10
- data/lib/ruby_ui/dropdown_menu/dropdown_menu_item.rb +10 -4
- data/lib/ruby_ui/form/form_docs.rb +89 -0
- data/lib/ruby_ui/masked_input/masked_input.rb +1 -11
- data/lib/ruby_ui/masked_input/masked_input_controller.js +0 -13
- data/lib/ruby_ui/select/select_value.rb +2 -1
- data/lib/ruby_ui/sheet/sheet.rb +9 -1
- data/lib/ruby_ui/sheet/sheet_controller.js +6 -0
- data/lib/ruby_ui/table/table_docs.rb +2 -2
- data/lib/ruby_ui/tabs/tabs_docs.rb +1 -1
- data/lib/ruby_ui/tabs/tabs_trigger.rb +10 -4
- data/lib/ruby_ui/theme_toggle/theme_toggle.rb +14 -2
- data/lib/ruby_ui/theme_toggle/theme_toggle_controller.js +27 -19
- data/lib/ruby_ui/theme_toggle/theme_toggle_docs.rb +12 -42
- data/lib/ruby_ui/toast/toast.rb +18 -0
- data/lib/ruby_ui/toast/toast_action.rb +27 -0
- data/lib/ruby_ui/toast/toast_cancel.rb +27 -0
- data/lib/ruby_ui/toast/toast_close.rb +40 -0
- data/lib/ruby_ui/toast/toast_controller.js +151 -0
- data/lib/ruby_ui/toast/toast_description.rb +18 -0
- data/lib/ruby_ui/toast/toast_docs.rb +12 -0
- data/lib/ruby_ui/toast/toast_icon.rb +65 -0
- data/lib/ruby_ui/toast/toast_item.rb +72 -0
- data/lib/ruby_ui/toast/toast_region.rb +124 -0
- data/lib/ruby_ui/toast/toast_title.rb +18 -0
- data/lib/ruby_ui/toast/toaster_controller.js +306 -0
- data/lib/ruby_ui/toggle/toggle.rb +101 -0
- data/lib/ruby_ui/toggle/toggle_controller.js +33 -0
- data/lib/ruby_ui/toggle_group/toggle_group.rb +119 -0
- data/lib/ruby_ui/toggle_group/toggle_group_controller.js +126 -0
- data/lib/ruby_ui/toggle_group/toggle_group_item.rb +67 -0
- data/lib/ruby_ui/tooltip/tooltip_content.rb +12 -5
- data/lib/ruby_ui/tooltip/tooltip_controller.js +58 -22
- data/lib/ruby_ui/tooltip/tooltip_docs.rb +13 -0
- data/lib/ruby_ui/tooltip/tooltip_trigger.rb +10 -3
- data/lib/ruby_ui.rb +3 -1
- metadata +30 -14
- data/lib/ruby_ui/theme_toggle/set_dark_mode.rb +0 -16
- 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:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c8dd00d73934f8ff9e2ac4e92994de33c910192f92df634def1e1726bde86393
|
|
4
|
+
data.tar.gz: 50ba081c5c25aa85581620726935ca1469b402751eade75cdb8adbc2042160d5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 44150fd27560c0b3a7d5193501abfd755e36a15ae74dc555088b9a30a1b228adc521dd8a82fc705e3c2396a45bb0baf48aed1351840d96aaa87eae6a9c2d545d
|
|
7
|
+
data.tar.gz: fc536b32afeabc805ad4815ce9be98090caf2402e6c5cc920011d9166b721690eb35938195b5079bf44b7400c4ec5454642f00ea844df22062ef326e32316bc4
|
data/README.md
CHANGED
|
@@ -55,6 +55,12 @@ You can generate your components using `ruby_ui:component` generator.
|
|
|
55
55
|
bin/rails g ruby_ui:component Accordion
|
|
56
56
|
```
|
|
57
57
|
|
|
58
|
+
You can also generate multiple components at once.
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
bin/rails g ruby_ui:component Button Link Input Textarea
|
|
62
|
+
```
|
|
63
|
+
|
|
58
64
|
You also can generate all components using `ruby_ui:component:all` generator
|
|
59
65
|
|
|
60
66
|
## Documentation 📖
|
|
@@ -10,11 +10,13 @@ module RubyUI
|
|
|
10
10
|
def generate_components
|
|
11
11
|
say "Generating all components..."
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
13
|
+
# Each component lives in its own directory; select directories only so stray
|
|
14
|
+
# files (e.g. base.rb or a macOS .DS_Store) are never passed as component names.
|
|
15
|
+
folder_names = Dir.children(self.class.source_root).select do |entry|
|
|
16
|
+
File.directory?(File.join(self.class.source_root, entry))
|
|
17
17
|
end
|
|
18
|
+
|
|
19
|
+
run "bin/rails generate ruby_ui:component #{folder_names.join(" ")} --force #{options["force"]}"
|
|
18
20
|
end
|
|
19
21
|
end
|
|
20
22
|
end
|
|
@@ -7,69 +7,89 @@ module RubyUI
|
|
|
7
7
|
namespace "ruby_ui:component"
|
|
8
8
|
|
|
9
9
|
source_root File.expand_path("../../ruby_ui", __dir__)
|
|
10
|
-
argument :
|
|
10
|
+
argument :component_names, type: :array, required: true, banner: "Button Link Input"
|
|
11
11
|
class_option :force, type: :boolean, default: false
|
|
12
12
|
class_option :with_docs, type: :boolean, default: false
|
|
13
13
|
|
|
14
|
-
def
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
14
|
+
def generate_components
|
|
15
|
+
validate_components!
|
|
16
|
+
|
|
17
|
+
component_names.each do |component_name|
|
|
18
|
+
say "Generating #{component_name} files..."
|
|
19
|
+
copy_related_component_files(component_name)
|
|
20
|
+
copy_js_files(component_name)
|
|
21
|
+
install_dependencies(component_name)
|
|
18
22
|
end
|
|
19
23
|
|
|
20
|
-
|
|
24
|
+
update_stimulus_manifest
|
|
21
25
|
end
|
|
22
26
|
|
|
23
|
-
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def validate_components!
|
|
30
|
+
missing = component_names.reject { |name| component_exists?(name) }
|
|
31
|
+
return if missing.empty?
|
|
32
|
+
|
|
33
|
+
say "Component(s) not found: #{missing.join(", ")}", :red
|
|
34
|
+
exit 1
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def copy_related_component_files(component_name)
|
|
24
38
|
say "Generating components"
|
|
25
39
|
|
|
26
|
-
components_file_paths.each do |file_path|
|
|
40
|
+
components_file_paths(component_name).each do |file_path|
|
|
27
41
|
component_file_name = file_path.split("/").last
|
|
28
|
-
copy_file file_path, Rails.root.join("app/components/ruby_ui", component_folder_name, component_file_name), force: options["force"]
|
|
42
|
+
copy_file file_path, Rails.root.join("app/components/ruby_ui", component_folder_name(component_name), component_file_name), force: options["force"]
|
|
29
43
|
end
|
|
30
44
|
end
|
|
31
45
|
|
|
32
|
-
def copy_js_files
|
|
33
|
-
|
|
46
|
+
def copy_js_files(component_name)
|
|
47
|
+
paths = js_controller_file_paths(component_name)
|
|
48
|
+
return if paths.empty?
|
|
34
49
|
|
|
35
50
|
say "Generating Stimulus controllers"
|
|
36
51
|
|
|
37
|
-
|
|
52
|
+
paths.each do |file_path|
|
|
38
53
|
controller_file_name = file_path.split("/").last
|
|
39
54
|
copy_file file_path, Rails.root.join("app/javascript/controllers/ruby_ui", controller_file_name), force: options["force"]
|
|
40
55
|
end
|
|
41
56
|
|
|
57
|
+
@stimulus_controllers_added = true
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def update_stimulus_manifest
|
|
61
|
+
return unless @stimulus_controllers_added
|
|
62
|
+
|
|
42
63
|
# Importmap doesn't have controller manifest, instead it uses `eagerLoadControllersFrom("controllers", application)`
|
|
43
|
-
if
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
64
|
+
return if using_importmap?
|
|
65
|
+
|
|
66
|
+
say "Updating Stimulus controllers manifest"
|
|
67
|
+
run "rake stimulus:manifest:update"
|
|
47
68
|
end
|
|
48
69
|
|
|
49
|
-
def install_dependencies
|
|
50
|
-
|
|
70
|
+
def install_dependencies(component_name)
|
|
71
|
+
deps = dependencies(component_name)
|
|
72
|
+
return if deps.blank?
|
|
51
73
|
|
|
52
74
|
say "Installing dependencies"
|
|
53
75
|
|
|
54
|
-
install_components_dependencies(
|
|
55
|
-
install_gems_dependencies(
|
|
56
|
-
install_js_packages(
|
|
76
|
+
install_components_dependencies(deps["components"])
|
|
77
|
+
install_gems_dependencies(deps["gems"])
|
|
78
|
+
install_js_packages(deps["js_packages"])
|
|
57
79
|
end
|
|
58
80
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
def component_not_found? = !Dir.exist?(component_folder_path)
|
|
81
|
+
def component_exists?(component_name) = Dir.exist?(component_folder_path(component_name))
|
|
62
82
|
|
|
63
|
-
def component_folder_name = component_name.underscore
|
|
83
|
+
def component_folder_name(component_name) = component_name.underscore
|
|
64
84
|
|
|
65
|
-
def component_folder_path = File.join(self.class.source_root, component_folder_name)
|
|
85
|
+
def component_folder_path(component_name) = File.join(self.class.source_root, component_folder_name(component_name))
|
|
66
86
|
|
|
67
|
-
def components_file_paths
|
|
68
|
-
files = Dir.glob(File.join(component_folder_path, "*.rb"))
|
|
87
|
+
def components_file_paths(component_name)
|
|
88
|
+
files = Dir.glob(File.join(component_folder_path(component_name), "*.rb"))
|
|
69
89
|
options["with_docs"] ? files : files.reject { |f| f.end_with?("_docs.rb") }
|
|
70
90
|
end
|
|
71
91
|
|
|
72
|
-
def js_controller_file_paths = Dir.glob(File.join(component_folder_path, "*.js"))
|
|
92
|
+
def js_controller_file_paths(component_name) = Dir.glob(File.join(component_folder_path(component_name), "*.js"))
|
|
73
93
|
|
|
74
94
|
def install_components_dependencies(components)
|
|
75
95
|
components&.each do |component|
|
|
@@ -89,10 +109,10 @@ module RubyUI
|
|
|
89
109
|
end
|
|
90
110
|
end
|
|
91
111
|
|
|
92
|
-
def dependencies
|
|
112
|
+
def dependencies(component_name)
|
|
93
113
|
@dependencies ||= YAML.load_file(File.join(__dir__, "dependencies.yml")).freeze
|
|
94
114
|
|
|
95
|
-
@dependencies[component_folder_name]
|
|
115
|
+
@dependencies[component_folder_name(component_name)]
|
|
96
116
|
end
|
|
97
117
|
end
|
|
98
118
|
end
|
|
@@ -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,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?(
|
|
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?(
|
|
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
|
|
43
|
+
def using_pnpm? = File.exist?(rails_root.join("pnpm-lock.yaml"))
|
|
38
44
|
|
|
39
|
-
def
|
|
45
|
+
def using_yarn? = File.exist?(rails_root.join("yarn.lock"))
|
|
40
46
|
|
|
41
|
-
def
|
|
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
|
|
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
|
|
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
|
|
@@ -11,10 +11,12 @@ module RubyUI
|
|
|
11
11
|
def default_attrs
|
|
12
12
|
{
|
|
13
13
|
data: {
|
|
14
|
-
ruby_ui__accordion_target: "content"
|
|
14
|
+
ruby_ui__accordion_target: "content",
|
|
15
|
+
state: "closed"
|
|
15
16
|
},
|
|
16
17
|
class: "overflow-y-hidden",
|
|
17
|
-
style: "height: 0px;"
|
|
18
|
+
style: "height: 0px;",
|
|
19
|
+
hidden: true
|
|
18
20
|
}
|
|
19
21
|
end
|
|
20
22
|
end
|
|
@@ -65,9 +65,15 @@ export default class extends Controller {
|
|
|
65
65
|
|
|
66
66
|
// Reveal the accordion content with animation
|
|
67
67
|
revealContent() {
|
|
68
|
-
const
|
|
68
|
+
const content = this.contentTarget;
|
|
69
|
+
|
|
70
|
+
// Remove hidden so the element participates in layout before measuring
|
|
71
|
+
content.removeAttribute("hidden");
|
|
72
|
+
content.dataset.state = "open";
|
|
73
|
+
|
|
74
|
+
const contentHeight = content.scrollHeight;
|
|
69
75
|
animate(
|
|
70
|
-
|
|
76
|
+
content,
|
|
71
77
|
{ height: `${contentHeight}px` },
|
|
72
78
|
{
|
|
73
79
|
duration: this.animationDurationValue,
|
|
@@ -78,14 +84,23 @@ export default class extends Controller {
|
|
|
78
84
|
|
|
79
85
|
// Hide the accordion content with animation
|
|
80
86
|
hideContent() {
|
|
87
|
+
const content = this.contentTarget;
|
|
88
|
+
content.dataset.state = "closed";
|
|
89
|
+
|
|
81
90
|
animate(
|
|
82
|
-
|
|
91
|
+
content,
|
|
83
92
|
{ height: 0 },
|
|
84
93
|
{
|
|
85
94
|
duration: this.animationDurationValue,
|
|
86
95
|
easing: this.animationEasingValue,
|
|
87
96
|
},
|
|
88
|
-
)
|
|
97
|
+
).finished.then(() => {
|
|
98
|
+
// After animation completes, truly hide the element so it is removed
|
|
99
|
+
// from layout and form focus — prevents trapped validation errors
|
|
100
|
+
if (content.dataset.state === "closed") {
|
|
101
|
+
content.setAttribute("hidden", "");
|
|
102
|
+
}
|
|
103
|
+
});
|
|
89
104
|
}
|
|
90
105
|
|
|
91
106
|
// Rotate the accordion icon 180deg using animate function
|
|
@@ -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
|
+
}
|
|
@@ -16,7 +16,15 @@ module RubyUI
|
|
|
16
16
|
|
|
17
17
|
def default_attrs
|
|
18
18
|
{
|
|
19
|
-
loading: "lazy"
|
|
19
|
+
# NB: do not set loading: "lazy" here. avatar_controller hides a not-yet-loaded
|
|
20
|
+
# image with `display:none` (the `hidden` class) so the fallback shows. The
|
|
21
|
+
# browser never fetches a `loading="lazy"` image that generates no box, so its
|
|
22
|
+
# `load` event never fires and the image stays hidden forever (#415). shadcn/radix
|
|
23
|
+
# do not lazy-load the avatar image either.
|
|
24
|
+
data: {
|
|
25
|
+
ruby_ui__avatar_target: "image",
|
|
26
|
+
action: "load->ruby-ui--avatar#showImage error->ruby-ui--avatar#showFallback"
|
|
27
|
+
},
|
|
20
28
|
class: "aspect-square h-full w-full",
|
|
21
29
|
alt: @alt,
|
|
22
30
|
src: @src
|
data/lib/ruby_ui/base.rb
CHANGED
|
@@ -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(
|
|
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(
|
|
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 (
|
|
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
|
|
163
|
+
return this.parseDate(this.selectedDateValue);
|
|
141
164
|
}
|
|
142
165
|
|
|
143
166
|
viewDate() {
|
|
144
|
-
return
|
|
145
|
-
|
|
146
|
-
|
|
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))
|