ruby_ui 1.3.0 → 1.6.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 +12 -2
- data/lib/generators/ruby_ui/javascript_utils.rb +0 -13
- data/lib/ruby_ui/accordion/accordion_content.rb +4 -2
- data/lib/ruby_ui/accordion/accordion_controller.js +28 -6
- data/lib/ruby_ui/avatar/avatar_image.rb +5 -1
- data/lib/ruby_ui/bubble/bubble.rb +37 -0
- data/lib/ruby_ui/bubble/bubble_content.rb +23 -0
- data/lib/ruby_ui/bubble/bubble_docs.rb +167 -0
- data/lib/ruby_ui/bubble/bubble_group.rb +18 -0
- data/lib/ruby_ui/bubble/bubble_reactions.rb +38 -0
- data/lib/ruby_ui/combobox/combobox.rb +3 -1
- data/lib/ruby_ui/combobox/combobox_checkbox.rb +8 -1
- data/lib/ruby_ui/combobox/combobox_controller.js +3 -2
- data/lib/ruby_ui/combobox/combobox_trigger.rb +1 -4
- data/lib/ruby_ui/context_menu/context_menu_content.rb +5 -6
- data/lib/ruby_ui/context_menu/context_menu_controller.js +87 -75
- data/lib/ruby_ui/data_table/data_table_column_toggle.rb +8 -4
- data/lib/ruby_ui/data_table/data_table_column_visibility_controller.js +12 -2
- data/lib/ruby_ui/data_table/data_table_docs.rb +1 -1
- 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.rb +0 -1
- data/lib/ruby_ui/dropdown_menu/dropdown_menu_controller.js +5 -1
- data/lib/ruby_ui/dropdown_menu/dropdown_menu_docs.rb +1 -1
- data/lib/ruby_ui/dropdown_menu/dropdown_menu_item.rb +10 -4
- data/lib/ruby_ui/empty/empty.rb +18 -0
- data/lib/ruby_ui/empty/empty_content.rb +18 -0
- data/lib/ruby_ui/empty/empty_description.rb +18 -0
- data/lib/ruby_ui/empty/empty_docs.rb +69 -0
- data/lib/ruby_ui/empty/empty_header.rb +18 -0
- data/lib/ruby_ui/empty/empty_media.rb +31 -0
- data/lib/ruby_ui/empty/empty_title.rb +18 -0
- data/lib/ruby_ui/form/form_docs.rb +89 -0
- data/lib/ruby_ui/hover_card/hover_card_content.rb +5 -6
- data/lib/ruby_ui/hover_card/hover_card_controller.js +113 -76
- data/lib/ruby_ui/input_otp/input_otp.rb +42 -0
- data/lib/ruby_ui/input_otp/input_otp_controller.js +128 -0
- data/lib/ruby_ui/input_otp/input_otp_docs.rb +213 -0
- data/lib/ruby_ui/input_otp/input_otp_group.rb +15 -0
- data/lib/ruby_ui/input_otp/input_otp_separator.rb +39 -0
- data/lib/ruby_ui/input_otp/input_otp_slot.rb +33 -0
- data/lib/ruby_ui/message/message.rb +23 -0
- data/lib/ruby_ui/message/message_avatar.rb +18 -0
- data/lib/ruby_ui/message/message_content.rb +18 -0
- data/lib/ruby_ui/message/message_docs.rb +173 -0
- data/lib/ruby_ui/message/message_footer.rb +18 -0
- data/lib/ruby_ui/message/message_group.rb +18 -0
- data/lib/ruby_ui/message/message_header.rb +18 -0
- data/lib/ruby_ui/message_scroller/message_scroller.rb +18 -0
- data/lib/ruby_ui/message_scroller/message_scroller_button.rb +56 -0
- data/lib/ruby_ui/message_scroller/message_scroller_content.rb +23 -0
- data/lib/ruby_ui/message_scroller/message_scroller_controller.js +335 -0
- data/lib/ruby_ui/message_scroller/message_scroller_docs.rb +208 -0
- data/lib/ruby_ui/message_scroller/message_scroller_item.rb +28 -0
- data/lib/ruby_ui/message_scroller/message_scroller_provider.rb +34 -0
- data/lib/ruby_ui/message_scroller/message_scroller_viewport.rb +22 -0
- data/lib/ruby_ui/native_select/native_select_icon.rb +1 -1
- data/lib/ruby_ui/popover/popover_content.rb +2 -1
- data/lib/ruby_ui/popover/popover_controller.js +95 -32
- 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/toast/toaster_controller.js +24 -8
- data/lib/ruby_ui.rb +1 -1
- metadata +35 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2d65c0b25a3f006ce2a97107b84779fb313f55fe0937048f033a8a4477ee5bda
|
|
4
|
+
data.tar.gz: fccde45b64530b69c9c1b3330f2203da5d4921a2b89a48dbc25ae2d27f8d36fd
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ed144e39bf1a8cdf62f5550a9fdf1426cbc609979dbc7049c33c9c0e6fac737adffad32bbe5f21dd21c6de8b974ffe5c61a9ebd24011001411edac378629b416
|
|
7
|
+
data.tar.gz: 158aa2d122d94feadfb41ebd67e6e2078cf3b8ef9675fb94f23d228e0d27fcfb3b206e46a6f8bf47c3f8ede71b8c68e32c18db7ff9a8e3188f5a000138142d4e
|
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
|
|
@@ -40,7 +40,7 @@ command:
|
|
|
40
40
|
|
|
41
41
|
context_menu:
|
|
42
42
|
js_packages:
|
|
43
|
-
- "
|
|
43
|
+
- "@floating-ui/dom"
|
|
44
44
|
|
|
45
45
|
date_picker:
|
|
46
46
|
components:
|
|
@@ -64,7 +64,7 @@ dropdown_menu:
|
|
|
64
64
|
|
|
65
65
|
hover_card:
|
|
66
66
|
js_packages:
|
|
67
|
-
- "
|
|
67
|
+
- "@floating-ui/dom"
|
|
68
68
|
|
|
69
69
|
masked_input:
|
|
70
70
|
components:
|
|
@@ -73,6 +73,16 @@ masked_input:
|
|
|
73
73
|
js_packages:
|
|
74
74
|
- "maska"
|
|
75
75
|
|
|
76
|
+
message:
|
|
77
|
+
components:
|
|
78
|
+
- "Avatar"
|
|
79
|
+
- "Bubble"
|
|
80
|
+
|
|
81
|
+
message_scroller:
|
|
82
|
+
components:
|
|
83
|
+
- "Message"
|
|
84
|
+
- "Bubble"
|
|
85
|
+
|
|
76
86
|
pagination:
|
|
77
87
|
components:
|
|
78
88
|
- "Button"
|
|
@@ -25,8 +25,6 @@ module RubyUI
|
|
|
25
25
|
pin_motion
|
|
26
26
|
when "tw-animate-css"
|
|
27
27
|
pin_tw_animate_css
|
|
28
|
-
when "tippy.js"
|
|
29
|
-
pin_tippy_js
|
|
30
28
|
else
|
|
31
29
|
run "bin/importmap pin #{package}"
|
|
32
30
|
end
|
|
@@ -65,17 +63,6 @@ module RubyUI
|
|
|
65
63
|
RUBY
|
|
66
64
|
end
|
|
67
65
|
|
|
68
|
-
def pin_tippy_js
|
|
69
|
-
say <<~TEXT
|
|
70
|
-
WARNING: Installing tippy.js from CDN because `bin/importmap pin tippy.js` doesn't download the correct file.
|
|
71
|
-
TEXT
|
|
72
|
-
|
|
73
|
-
inject_into_file rails_root.join("config/importmap.rb"), <<~RUBY
|
|
74
|
-
pin "tippy.js", to: "https://cdn.jsdelivr.net/npm/tippy.js@6.3.7/+esm"
|
|
75
|
-
pin "@popperjs/core", to: "https://cdn.jsdelivr.net/npm/@popperjs/core@2.11.8/+esm"\n
|
|
76
|
-
RUBY
|
|
77
|
-
end
|
|
78
|
-
|
|
79
66
|
def rails_root = Rails.root
|
|
80
67
|
end
|
|
81
68
|
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,27 +65,49 @@ 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,
|
|
74
80
|
easing: this.animationEasingValue,
|
|
75
81
|
},
|
|
76
|
-
)
|
|
82
|
+
)
|
|
83
|
+
.finished.then(() => {
|
|
84
|
+
if (content.dataset.state === "open") content.style.height = "auto";
|
|
85
|
+
})
|
|
86
|
+
.catch(() => {});
|
|
77
87
|
}
|
|
78
88
|
|
|
79
89
|
// Hide the accordion content with animation
|
|
80
90
|
hideContent() {
|
|
91
|
+
const content = this.contentTarget;
|
|
92
|
+
content.dataset.state = "closed";
|
|
93
|
+
|
|
94
|
+
const currentHeight = content.getBoundingClientRect().height;
|
|
81
95
|
animate(
|
|
82
|
-
|
|
83
|
-
{ height:
|
|
96
|
+
content,
|
|
97
|
+
{ height: [`${currentHeight}px`, "0px"] },
|
|
84
98
|
{
|
|
85
99
|
duration: this.animationDurationValue,
|
|
86
100
|
easing: this.animationEasingValue,
|
|
87
101
|
},
|
|
88
|
-
)
|
|
102
|
+
)
|
|
103
|
+
.finished.then(() => {
|
|
104
|
+
// After animation completes, truly hide the element so it is removed
|
|
105
|
+
// from layout and form focus — prevents trapped validation errors
|
|
106
|
+
if (content.dataset.state === "closed") {
|
|
107
|
+
content.setAttribute("hidden", "");
|
|
108
|
+
}
|
|
109
|
+
})
|
|
110
|
+
.catch(() => {});
|
|
89
111
|
}
|
|
90
112
|
|
|
91
113
|
// Rotate the accordion icon 180deg using animate function
|
|
@@ -16,7 +16,11 @@ 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.
|
|
20
24
|
data: {
|
|
21
25
|
ruby_ui__avatar_target: "image",
|
|
22
26
|
action: "load->ruby-ui--avatar#showImage error->ruby-ui--avatar#showFallback"
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyUI
|
|
4
|
+
class Bubble < Base
|
|
5
|
+
VARIANTS = {
|
|
6
|
+
default: "*:data-[slot=bubble-content]:bg-primary *:data-[slot=bubble-content]:text-primary-foreground [&>[data-slot=bubble-content]:is(button,a):hover]:bg-primary/80",
|
|
7
|
+
secondary: "*:data-[slot=bubble-content]:bg-secondary *:data-[slot=bubble-content]:text-secondary-foreground [&>[data-slot=bubble-content]:is(button,a):hover]:bg-[color-mix(in_oklch,var(--secondary),var(--foreground)_5%)]",
|
|
8
|
+
muted: "*:data-[slot=bubble-content]:bg-muted [&>[data-slot=bubble-content]:is(button,a):hover]:bg-[color-mix(in_oklch,var(--muted),var(--foreground)_5%)]",
|
|
9
|
+
tinted: "*:data-[slot=bubble-content]:bg-[oklch(from_var(--primary)_0.93_calc(c*0.4)_h)] dark:*:data-[slot=bubble-content]:bg-[oklch(from_var(--primary)_0.3_calc(c*0.4)_h)] *:data-[slot=bubble-content]:text-foreground [&>[data-slot=bubble-content]:is(button,a):hover]:bg-[oklch(from_var(--primary)_0.88_calc(c*0.5)_h)] dark:[&>[data-slot=bubble-content]:is(button,a):hover]:bg-[oklch(from_var(--primary)_0.35_calc(c*0.5)_h)]",
|
|
10
|
+
outline: "*:data-[slot=bubble-content]:bg-background *:data-[slot=bubble-content]:border-border [&>[data-slot=bubble-content]:is(button,a):hover]:bg-muted [&>[data-slot=bubble-content]:is(button,a):hover]:text-foreground dark:[&>[data-slot=bubble-content]:is(button,a):hover]:bg-input/30",
|
|
11
|
+
ghost: "*:data-[slot=bubble-content]:rounded-none *:data-[slot=bubble-content]:bg-transparent *:data-[slot=bubble-content]:p-0 [&>[data-slot=bubble-content]:is(button,a):hover]:bg-muted [&>[data-slot=bubble-content]:is(button,a):hover]:text-foreground dark:[&>[data-slot=bubble-content]:is(button,a):hover]:bg-muted/50 border-none",
|
|
12
|
+
destructive: "*:data-[slot=bubble-content]:bg-destructive/10 dark:*:data-[slot=bubble-content]:bg-destructive/20 *:data-[slot=bubble-content]:text-destructive [&>[data-slot=bubble-content]:is(button,a):hover]:bg-destructive/20 dark:[&>[data-slot=bubble-content]:is(button,a):hover]:bg-destructive/30"
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
def initialize(variant: :default, align: :start, **attrs)
|
|
16
|
+
@variant = variant
|
|
17
|
+
@align = align
|
|
18
|
+
super(**attrs)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def view_template(&)
|
|
22
|
+
div(**attrs, &)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def default_attrs
|
|
28
|
+
{
|
|
29
|
+
data: {slot: "bubble", variant: @variant, align: @align},
|
|
30
|
+
class: [
|
|
31
|
+
"group/bubble relative flex w-fit min-w-0 flex-col gap-1 max-w-[80%] data-[align=end]:self-end data-[variant=ghost]:max-w-full",
|
|
32
|
+
VARIANTS[@variant]
|
|
33
|
+
]
|
|
34
|
+
}
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyUI
|
|
4
|
+
class BubbleContent < Base
|
|
5
|
+
def initialize(as: :div, **attrs)
|
|
6
|
+
@as = as
|
|
7
|
+
super(**attrs)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def view_template(&)
|
|
11
|
+
send(@as, **attrs, &)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
private
|
|
15
|
+
|
|
16
|
+
def default_attrs
|
|
17
|
+
{
|
|
18
|
+
data: {slot: "bubble-content"},
|
|
19
|
+
class: "w-fit max-w-full min-w-0 overflow-hidden wrap-break-word rounded-3xl border border-transparent px-3 py-2.5 text-sm leading-relaxed group-data-[align=end]/bubble:self-end [&:is(button,a)]:text-left [&:is(button,a)]:outline-none [&:is(button,a)]:transition-colors [&:is(button,a):focus-visible]:border-ring [&:is(button,a):focus-visible]:ring-3 [&:is(button,a):focus-visible]:ring-ring/30"
|
|
20
|
+
}
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Views::Docs::Bubble < Views::Base
|
|
4
|
+
def view_template
|
|
5
|
+
component = "Bubble"
|
|
6
|
+
|
|
7
|
+
div(class: "max-w-2xl mx-auto w-full py-10 space-y-10") do
|
|
8
|
+
render Docs::Header.new(title: "Bubble", description: "A chat bubble surface for displaying conversational content, with variants, alignment, grouping, and reactions.")
|
|
9
|
+
|
|
10
|
+
Heading(level: 2) { "Usage" }
|
|
11
|
+
|
|
12
|
+
render Docs::VisualCodeExample.new(title: "Default", context: self) do
|
|
13
|
+
<<~RUBY
|
|
14
|
+
Bubble(align: :end) do
|
|
15
|
+
BubbleContent { "Hey there! what's up?" }
|
|
16
|
+
end
|
|
17
|
+
RUBY
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
render Docs::VisualCodeExample.new(title: "Conversation", context: self) do
|
|
21
|
+
<<~RUBY
|
|
22
|
+
div(class: "flex flex-col gap-8") do
|
|
23
|
+
Bubble(align: :end) do
|
|
24
|
+
BubbleContent { "Hey there! what's up?" }
|
|
25
|
+
end
|
|
26
|
+
BubbleGroup do
|
|
27
|
+
Bubble(variant: :muted) do
|
|
28
|
+
BubbleContent { "Hey! Want to see chat bubbles?" }
|
|
29
|
+
end
|
|
30
|
+
Bubble(variant: :muted) do
|
|
31
|
+
BubbleContent { "I can group messages, switch sides, and keep the whole thread easy to scan." }
|
|
32
|
+
BubbleReactions(role: "img", aria_label: "Reaction: thumbs up") do
|
|
33
|
+
span { "👍" }
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
Bubble(align: :end) do
|
|
38
|
+
BubbleContent { "Sure. Hit me with your best demo." }
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
RUBY
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
Heading(level: 2) { "Variants" }
|
|
45
|
+
|
|
46
|
+
render Docs::VisualCodeExample.new(title: "Variants", context: self) do
|
|
47
|
+
<<~RUBY
|
|
48
|
+
div(class: "flex flex-col gap-4 w-full") do
|
|
49
|
+
Bubble(variant: :default) { BubbleContent { "Default" } }
|
|
50
|
+
Bubble(variant: :secondary) { BubbleContent { "Secondary" } }
|
|
51
|
+
Bubble(variant: :muted) { BubbleContent { "Muted" } }
|
|
52
|
+
Bubble(variant: :tinted) { BubbleContent { "Tinted" } }
|
|
53
|
+
Bubble(variant: :outline) { BubbleContent { "Outline" } }
|
|
54
|
+
Bubble(variant: :ghost) { BubbleContent { "Ghost — unframed, full width for assistant text or markdown." } }
|
|
55
|
+
Bubble(variant: :destructive) { BubbleContent { "Destructive — something went wrong." } }
|
|
56
|
+
end
|
|
57
|
+
RUBY
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
Heading(level: 2) { "Alignment" }
|
|
61
|
+
|
|
62
|
+
render Docs::VisualCodeExample.new(title: "Start and end", context: self) do
|
|
63
|
+
<<~RUBY
|
|
64
|
+
div(class: "flex flex-col gap-4 w-full") do
|
|
65
|
+
Bubble(align: :start, variant: :muted) do
|
|
66
|
+
BubbleContent { "Aligned to the start (receiver)." }
|
|
67
|
+
end
|
|
68
|
+
Bubble(align: :end) do
|
|
69
|
+
BubbleContent { "Aligned to the end (sender)." }
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
RUBY
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
Heading(level: 2) { "Reactions" }
|
|
76
|
+
|
|
77
|
+
render Docs::VisualCodeExample.new(title: "Reactions", context: self) do
|
|
78
|
+
<<~RUBY
|
|
79
|
+
div(class: "flex flex-col gap-10 w-full py-6") do
|
|
80
|
+
Bubble(variant: :muted) do
|
|
81
|
+
BubbleContent { "Reactions anchor to the bubble edge." }
|
|
82
|
+
BubbleReactions(role: "img", aria_label: "Reactions: thumbs up, fire, eyes, and 2 more") do
|
|
83
|
+
span { "👍" }
|
|
84
|
+
span { "🔥" }
|
|
85
|
+
span { "👀" }
|
|
86
|
+
span { "+2" }
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
Bubble(align: :end) do
|
|
90
|
+
BubbleContent { "Place them on top and to the start too." }
|
|
91
|
+
BubbleReactions(side: :top, align: :start, role: "img", aria_label: "Reaction: heart") do
|
|
92
|
+
span { "❤️" }
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
RUBY
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
Heading(level: 2) { "Group" }
|
|
100
|
+
|
|
101
|
+
render Docs::VisualCodeExample.new(title: "Bubble group", context: self) do
|
|
102
|
+
<<~RUBY
|
|
103
|
+
BubbleGroup do
|
|
104
|
+
Bubble(variant: :muted) { BubbleContent { "First message in the group." } }
|
|
105
|
+
Bubble(variant: :muted) { BubbleContent { "Second one, tighter spacing." } }
|
|
106
|
+
Bubble(variant: :muted) { BubbleContent { "Third, all stacked together." } }
|
|
107
|
+
end
|
|
108
|
+
RUBY
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
Heading(level: 2) { "Link or button bubble" }
|
|
112
|
+
|
|
113
|
+
render Docs::VisualCodeExample.new(title: "Interactive content", context: self) do
|
|
114
|
+
<<~RUBY
|
|
115
|
+
div(class: "flex flex-col gap-4 w-full") do
|
|
116
|
+
Bubble(align: :end) do
|
|
117
|
+
BubbleContent(as: :a, href: "#") { "Tap to open the link →" }
|
|
118
|
+
end
|
|
119
|
+
Bubble(variant: :outline) do
|
|
120
|
+
BubbleContent(as: :button, type: "button") { "Retry sending" }
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
RUBY
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
Heading(level: 2) { "With Tooltip" }
|
|
127
|
+
|
|
128
|
+
render Docs::VisualCodeExample.new(title: "Reveal metadata on hover", context: self) do
|
|
129
|
+
<<~RUBY
|
|
130
|
+
Tooltip do
|
|
131
|
+
TooltipTrigger(class: "w-fit") do
|
|
132
|
+
Bubble(variant: :muted, class: "max-w-none") do
|
|
133
|
+
BubbleContent { "Read 9:41 AM" }
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
TooltipContent do
|
|
137
|
+
Text { "Delivered and read" }
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
RUBY
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
Heading(level: 2) { "With Popover" }
|
|
144
|
+
|
|
145
|
+
render Docs::VisualCodeExample.new(title: "Surface details on demand", context: self) do
|
|
146
|
+
<<~RUBY
|
|
147
|
+
Popover do
|
|
148
|
+
PopoverTrigger do
|
|
149
|
+
Bubble(variant: :destructive, class: "max-w-none") do
|
|
150
|
+
BubbleContent { "Message failed to send" }
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
PopoverContent(class: "w-64") do
|
|
154
|
+
Text(weight: :semibold) { "Delivery error" }
|
|
155
|
+
Text(size: :sm, class: "text-muted-foreground") { "The recipient's inbox is full. Try again later." }
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
RUBY
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
render Components::ComponentSetup::Tabs.new(component_name: component)
|
|
162
|
+
|
|
163
|
+
# components
|
|
164
|
+
render Docs::ComponentsTable.new(component_files(component))
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyUI
|
|
4
|
+
class BubbleGroup < Base
|
|
5
|
+
def view_template(&)
|
|
6
|
+
div(**attrs, &)
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
private
|
|
10
|
+
|
|
11
|
+
def default_attrs
|
|
12
|
+
{
|
|
13
|
+
data: {slot: "bubble-group"},
|
|
14
|
+
class: "flex min-w-0 flex-col gap-2"
|
|
15
|
+
}
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyUI
|
|
4
|
+
class BubbleReactions < Base
|
|
5
|
+
SIDES = {
|
|
6
|
+
top: "top-0 -translate-y-3/4",
|
|
7
|
+
bottom: "bottom-0 translate-y-3/4"
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
ALIGNS = {
|
|
11
|
+
start: "left-3",
|
|
12
|
+
end: "right-3"
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
def initialize(side: :bottom, align: :end, **attrs)
|
|
16
|
+
@side = side
|
|
17
|
+
@align = align
|
|
18
|
+
super(**attrs)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def view_template(&)
|
|
22
|
+
div(**attrs, &)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def default_attrs
|
|
28
|
+
{
|
|
29
|
+
data: {slot: "bubble-reactions", side: @side, align: @align},
|
|
30
|
+
class: [
|
|
31
|
+
"absolute z-10 flex w-fit items-center justify-center rounded-full ring-3 ring-card bg-muted shrink-0 gap-1 px-1.5 py-0.5 has-[button]:p-0 text-sm",
|
|
32
|
+
SIDES[@side],
|
|
33
|
+
ALIGNS[@align]
|
|
34
|
+
]
|
|
35
|
+
}
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
module RubyUI
|
|
4
4
|
class Combobox < Base
|
|
5
|
-
def initialize(term: nil, **)
|
|
5
|
+
def initialize(term: nil, placement: "bottom-start", **)
|
|
6
6
|
@term = term
|
|
7
|
+
@placement = placement
|
|
7
8
|
super(**)
|
|
8
9
|
end
|
|
9
10
|
|
|
@@ -19,6 +20,7 @@ module RubyUI
|
|
|
19
20
|
data: {
|
|
20
21
|
controller: "ruby-ui--combobox",
|
|
21
22
|
ruby_ui__combobox_term_value: @term,
|
|
23
|
+
ruby_ui__combobox_placement_value: @placement,
|
|
22
24
|
action: "turbo:morph@window->ruby-ui--combobox#updateTriggerContent"
|
|
23
25
|
}
|
|
24
26
|
}
|
|
@@ -19,7 +19,14 @@ module RubyUI
|
|
|
19
19
|
],
|
|
20
20
|
data: {
|
|
21
21
|
ruby_ui__combobox_target: "input",
|
|
22
|
-
|
|
22
|
+
ruby_ui__checkbox_group_target: "checkbox",
|
|
23
|
+
ruby_ui__form_field_target: "input",
|
|
24
|
+
action: %w[
|
|
25
|
+
ruby-ui--combobox#inputChanged
|
|
26
|
+
change->ruby-ui--checkbox-group#onChange
|
|
27
|
+
change->ruby-ui--form-field#onInput
|
|
28
|
+
invalid->ruby-ui--form-field#onInvalid
|
|
29
|
+
]
|
|
23
30
|
}
|
|
24
31
|
}
|
|
25
32
|
end
|
|
@@ -5,7 +5,8 @@ import { computePosition, autoUpdate, offset, flip } from "@floating-ui/dom";
|
|
|
5
5
|
export default class extends Controller {
|
|
6
6
|
static values = {
|
|
7
7
|
term: String,
|
|
8
|
-
minPopoverWidth: { type: Number, default: 240 }
|
|
8
|
+
minPopoverWidth: { type: Number, default: 240 },
|
|
9
|
+
placement: { type: String, default: "bottom-start" }
|
|
9
10
|
}
|
|
10
11
|
|
|
11
12
|
static targets = [
|
|
@@ -175,7 +176,7 @@ export default class extends Controller {
|
|
|
175
176
|
updatePopoverPosition() {
|
|
176
177
|
this.cleanup = autoUpdate(this.triggerTarget, this.popoverTarget, () => {
|
|
177
178
|
computePosition(this.triggerTarget, this.popoverTarget, {
|
|
178
|
-
placement:
|
|
179
|
+
placement: this.placementValue,
|
|
179
180
|
middleware: [offset(4), flip()],
|
|
180
181
|
}).then(({ x, y }) => {
|
|
181
182
|
Object.assign(this.popoverTarget.style, {
|