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.
Files changed (69) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +6 -0
  3. data/lib/generators/ruby_ui/component/all_generator.rb +6 -4
  4. data/lib/generators/ruby_ui/component_generator.rb +51 -31
  5. data/lib/generators/ruby_ui/dependencies.yml +12 -2
  6. data/lib/generators/ruby_ui/javascript_utils.rb +0 -13
  7. data/lib/ruby_ui/accordion/accordion_content.rb +4 -2
  8. data/lib/ruby_ui/accordion/accordion_controller.js +28 -6
  9. data/lib/ruby_ui/avatar/avatar_image.rb +5 -1
  10. data/lib/ruby_ui/bubble/bubble.rb +37 -0
  11. data/lib/ruby_ui/bubble/bubble_content.rb +23 -0
  12. data/lib/ruby_ui/bubble/bubble_docs.rb +167 -0
  13. data/lib/ruby_ui/bubble/bubble_group.rb +18 -0
  14. data/lib/ruby_ui/bubble/bubble_reactions.rb +38 -0
  15. data/lib/ruby_ui/combobox/combobox.rb +3 -1
  16. data/lib/ruby_ui/combobox/combobox_checkbox.rb +8 -1
  17. data/lib/ruby_ui/combobox/combobox_controller.js +3 -2
  18. data/lib/ruby_ui/combobox/combobox_trigger.rb +1 -4
  19. data/lib/ruby_ui/context_menu/context_menu_content.rb +5 -6
  20. data/lib/ruby_ui/context_menu/context_menu_controller.js +87 -75
  21. data/lib/ruby_ui/data_table/data_table_column_toggle.rb +8 -4
  22. data/lib/ruby_ui/data_table/data_table_column_visibility_controller.js +12 -2
  23. data/lib/ruby_ui/data_table/data_table_docs.rb +1 -1
  24. data/lib/ruby_ui/dialog/dialog_content.rb +7 -19
  25. data/lib/ruby_ui/dialog/dialog_controller.js +22 -10
  26. data/lib/ruby_ui/dropdown_menu/dropdown_menu.rb +0 -1
  27. data/lib/ruby_ui/dropdown_menu/dropdown_menu_controller.js +5 -1
  28. data/lib/ruby_ui/dropdown_menu/dropdown_menu_docs.rb +1 -1
  29. data/lib/ruby_ui/dropdown_menu/dropdown_menu_item.rb +10 -4
  30. data/lib/ruby_ui/empty/empty.rb +18 -0
  31. data/lib/ruby_ui/empty/empty_content.rb +18 -0
  32. data/lib/ruby_ui/empty/empty_description.rb +18 -0
  33. data/lib/ruby_ui/empty/empty_docs.rb +69 -0
  34. data/lib/ruby_ui/empty/empty_header.rb +18 -0
  35. data/lib/ruby_ui/empty/empty_media.rb +31 -0
  36. data/lib/ruby_ui/empty/empty_title.rb +18 -0
  37. data/lib/ruby_ui/form/form_docs.rb +89 -0
  38. data/lib/ruby_ui/hover_card/hover_card_content.rb +5 -6
  39. data/lib/ruby_ui/hover_card/hover_card_controller.js +113 -76
  40. data/lib/ruby_ui/input_otp/input_otp.rb +42 -0
  41. data/lib/ruby_ui/input_otp/input_otp_controller.js +128 -0
  42. data/lib/ruby_ui/input_otp/input_otp_docs.rb +213 -0
  43. data/lib/ruby_ui/input_otp/input_otp_group.rb +15 -0
  44. data/lib/ruby_ui/input_otp/input_otp_separator.rb +39 -0
  45. data/lib/ruby_ui/input_otp/input_otp_slot.rb +33 -0
  46. data/lib/ruby_ui/message/message.rb +23 -0
  47. data/lib/ruby_ui/message/message_avatar.rb +18 -0
  48. data/lib/ruby_ui/message/message_content.rb +18 -0
  49. data/lib/ruby_ui/message/message_docs.rb +173 -0
  50. data/lib/ruby_ui/message/message_footer.rb +18 -0
  51. data/lib/ruby_ui/message/message_group.rb +18 -0
  52. data/lib/ruby_ui/message/message_header.rb +18 -0
  53. data/lib/ruby_ui/message_scroller/message_scroller.rb +18 -0
  54. data/lib/ruby_ui/message_scroller/message_scroller_button.rb +56 -0
  55. data/lib/ruby_ui/message_scroller/message_scroller_content.rb +23 -0
  56. data/lib/ruby_ui/message_scroller/message_scroller_controller.js +335 -0
  57. data/lib/ruby_ui/message_scroller/message_scroller_docs.rb +208 -0
  58. data/lib/ruby_ui/message_scroller/message_scroller_item.rb +28 -0
  59. data/lib/ruby_ui/message_scroller/message_scroller_provider.rb +34 -0
  60. data/lib/ruby_ui/message_scroller/message_scroller_viewport.rb +22 -0
  61. data/lib/ruby_ui/native_select/native_select_icon.rb +1 -1
  62. data/lib/ruby_ui/popover/popover_content.rb +2 -1
  63. data/lib/ruby_ui/popover/popover_controller.js +95 -32
  64. data/lib/ruby_ui/table/table_docs.rb +2 -2
  65. data/lib/ruby_ui/tabs/tabs_docs.rb +1 -1
  66. data/lib/ruby_ui/tabs/tabs_trigger.rb +10 -4
  67. data/lib/ruby_ui/toast/toaster_controller.js +24 -8
  68. data/lib/ruby_ui.rb +1 -1
  69. metadata +35 -2
@@ -0,0 +1,213 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Views::Docs::InputOtp < Views::Base
4
+ def view_template
5
+ component = "InputOtp"
6
+
7
+ div(class: "max-w-2xl mx-auto w-full py-10 space-y-10") do
8
+ render Docs::Header.new(title: "Input OTP", description: "Accessible one-time-password input with keyboard navigation and paste support.")
9
+
10
+ Heading(level: 2) { "Usage" }
11
+
12
+ render Docs::VisualCodeExample.new(title: "Example", context: self) do
13
+ <<~RUBY
14
+ InputOtp(length: 6, name: "otp") do
15
+ InputOtpGroup do
16
+ InputOtpSlot(index: 0)
17
+ InputOtpSlot(index: 1)
18
+ InputOtpSlot(index: 2)
19
+ InputOtpSlot(index: 3)
20
+ InputOtpSlot(index: 4)
21
+ InputOtpSlot(index: 5)
22
+ end
23
+ end
24
+ RUBY
25
+ end
26
+
27
+ Heading(level: 2) { "Composition" }
28
+
29
+ Text { "InputOtpGroup and InputOtpSeparator compose freely — split slots into however many groups make sense, with a separator between each." }
30
+
31
+ render Docs::VisualCodeExample.new(title: "Example", context: self) do
32
+ <<~RUBY
33
+ InputOtp(length: 6, name: "otp") do
34
+ InputOtpGroup do
35
+ InputOtpSlot(index: 0)
36
+ InputOtpSlot(index: 1)
37
+ end
38
+ InputOtpSeparator()
39
+ InputOtpGroup do
40
+ InputOtpSlot(index: 2)
41
+ InputOtpSlot(index: 3)
42
+ end
43
+ InputOtpSeparator()
44
+ InputOtpGroup do
45
+ InputOtpSlot(index: 4)
46
+ InputOtpSlot(index: 5)
47
+ end
48
+ end
49
+ RUBY
50
+ end
51
+
52
+ Heading(level: 2) { "Pattern" }
53
+
54
+ Text { "Pass pattern: with a single-character regex class to define what InputOtp accepts. The default is \"[0-9]\" (digits only)." }
55
+
56
+ render Docs::VisualCodeExample.new(title: "Alphanumeric", context: self) do
57
+ <<~RUBY
58
+ InputOtp(length: 6, name: "otp", pattern: "[0-9A-Za-z]") do
59
+ InputOtpGroup do
60
+ InputOtpSlot(index: 0)
61
+ InputOtpSlot(index: 1)
62
+ InputOtpSlot(index: 2)
63
+ InputOtpSlot(index: 3)
64
+ InputOtpSlot(index: 4)
65
+ InputOtpSlot(index: 5)
66
+ end
67
+ end
68
+ RUBY
69
+ end
70
+
71
+ Heading(level: 2) { "Four digits" }
72
+
73
+ Text { "A common pattern for PIN codes — just pass a shorter length." }
74
+
75
+ render Docs::VisualCodeExample.new(title: "Example", context: self) do
76
+ <<~RUBY
77
+ InputOtp(length: 4, name: "pin") do
78
+ InputOtpGroup do
79
+ InputOtpSlot(index: 0)
80
+ InputOtpSlot(index: 1)
81
+ InputOtpSlot(index: 2)
82
+ InputOtpSlot(index: 3)
83
+ end
84
+ end
85
+ RUBY
86
+ end
87
+
88
+ Heading(level: 2) { "Disabled" }
89
+
90
+ render Docs::VisualCodeExample.new(title: "Example", context: self) do
91
+ <<~RUBY
92
+ InputOtp(length: 6, name: "otp", disabled: true) do
93
+ InputOtpGroup do
94
+ InputOtpSlot(index: 0)
95
+ InputOtpSlot(index: 1)
96
+ InputOtpSlot(index: 2)
97
+ InputOtpSlot(index: 3)
98
+ InputOtpSlot(index: 4)
99
+ InputOtpSlot(index: 5)
100
+ end
101
+ end
102
+ RUBY
103
+ end
104
+
105
+ Heading(level: 2) { "Invalid" }
106
+
107
+ Text { "Pass aria_invalid: \"true\" to each InputOtpSlot to show an error state." }
108
+
109
+ render Docs::VisualCodeExample.new(title: "Example", context: self) do
110
+ <<~RUBY
111
+ InputOtp(length: 6, name: "otp") do
112
+ InputOtpGroup do
113
+ InputOtpSlot(index: 0, aria_invalid: "true")
114
+ InputOtpSlot(index: 1, aria_invalid: "true")
115
+ InputOtpSlot(index: 2, aria_invalid: "true")
116
+ InputOtpSlot(index: 3, aria_invalid: "true")
117
+ InputOtpSlot(index: 4, aria_invalid: "true")
118
+ InputOtpSlot(index: 5, aria_invalid: "true")
119
+ end
120
+ end
121
+ RUBY
122
+ end
123
+
124
+ Heading(level: 2) { "Form" }
125
+
126
+ Text { "A full example combining InputOtp with Card, Button, and InlineLink — bigger slots via class:, a resend action, and fallback links." }
127
+
128
+ render Docs::VisualCodeExample.new(title: "Verify your login", context: self) do
129
+ <<~RUBY
130
+ Card(class: "mx-auto max-w-md") do
131
+ CardHeader do
132
+ CardTitle { "Verify your login" }
133
+ CardDescription do
134
+ plain "Enter the verification code we sent to your email address: "
135
+ span(class: "font-medium") { "m@example.com" }
136
+ plain "."
137
+ end
138
+ end
139
+ CardContent(class: "space-y-4") do
140
+ div(class: "flex items-center justify-between") do
141
+ label(class: "text-sm font-medium") { "Verification code" }
142
+ Button(variant: :outline, size: :sm) do
143
+ svg(
144
+ xmlns: "http://www.w3.org/2000/svg",
145
+ viewbox: "0 0 24 24",
146
+ fill: "none",
147
+ stroke: "currentColor",
148
+ stroke_width: "2",
149
+ stroke_linecap: "round",
150
+ stroke_linejoin: "round",
151
+ class: "w-4 h-4 mr-2"
152
+ ) do |s|
153
+ s.path(d: "M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8")
154
+ s.path(d: "M21 3v5h-5")
155
+ s.path(d: "M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16")
156
+ s.path(d: "M8 16H3v5")
157
+ end
158
+ plain "Resend Code"
159
+ end
160
+ end
161
+ InputOtp(length: 6, name: "otp", required: true) do
162
+ InputOtpGroup do
163
+ InputOtpSlot(index: 0, class: "h-12 w-11 text-xl")
164
+ InputOtpSlot(index: 1, class: "h-12 w-11 text-xl")
165
+ InputOtpSlot(index: 2, class: "h-12 w-11 text-xl")
166
+ end
167
+ InputOtpSeparator(class: "mx-2")
168
+ InputOtpGroup do
169
+ InputOtpSlot(index: 3, class: "h-12 w-11 text-xl")
170
+ InputOtpSlot(index: 4, class: "h-12 w-11 text-xl")
171
+ InputOtpSlot(index: 5, class: "h-12 w-11 text-xl")
172
+ end
173
+ end
174
+ InlineLink(href: "#") { "I no longer have access to this email address." }
175
+ end
176
+ CardFooter(class: "flex flex-col items-stretch gap-2") do
177
+ Button(class: "w-full") { "Verify" }
178
+ Text(size: "sm", weight: "muted") do
179
+ plain "Having trouble signing in? "
180
+ InlineLink(href: "#") { "Contact support" }
181
+ end
182
+ end
183
+ end
184
+ RUBY
185
+ end
186
+
187
+ Heading(level: 2) { "Reacting to completion" }
188
+
189
+ Text { "The controller dispatches a ruby-ui--input-otp:complete custom event (detail: { value }) once the value reaches length characters, and a ruby-ui--input-otp:input event on every change. Wire a Stimulus action on a parent element to react — for example, to auto-submit a form:" }
190
+
191
+ Codeblock(<<~JS, syntax: :javascript)
192
+ // app/javascript/controllers/otp_form_controller.js
193
+ import { Controller } from "@hotwired/stimulus"
194
+
195
+ export default class extends Controller {
196
+ submit() {
197
+ this.element.requestSubmit()
198
+ }
199
+ }
200
+ JS
201
+
202
+ Codeblock(<<~HTML, syntax: :html)
203
+ <form data-controller="otp-form" data-action="ruby-ui--input-otp:complete->otp-form#submit">
204
+ <!-- InputOtp(...) here -->
205
+ </form>
206
+ HTML
207
+
208
+ render Components::ComponentSetup::Tabs.new(component_name: component)
209
+
210
+ render Docs::ComponentsTable.new(component_files(component))
211
+ end
212
+ end
213
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyUI
4
+ class InputOtpGroup < Base
5
+ def view_template(&block)
6
+ div(**attrs, &block)
7
+ end
8
+
9
+ private
10
+
11
+ def default_attrs
12
+ {class: "flex items-center"}
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyUI
4
+ class InputOtpSeparator < Base
5
+ def view_template(&block)
6
+ div(**attrs) do
7
+ if block
8
+ block.call
9
+ else
10
+ icon
11
+ end
12
+ end
13
+ end
14
+
15
+ def icon
16
+ svg(
17
+ xmlns: "http://www.w3.org/2000/svg",
18
+ viewbox: "0 0 24 24",
19
+ fill: "none",
20
+ stroke: "currentColor",
21
+ stroke_width: "2",
22
+ stroke_linecap: "round",
23
+ stroke_linejoin: "round",
24
+ class: "h-4 w-4"
25
+ ) do |s|
26
+ s.path(d: "M5 12h14")
27
+ end
28
+ end
29
+
30
+ private
31
+
32
+ def default_attrs
33
+ {
34
+ role: "separator",
35
+ class: "text-muted-foreground"
36
+ }
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyUI
4
+ class InputOtpSlot < Base
5
+ def initialize(index:, **attrs)
6
+ @index = index
7
+ super(**attrs)
8
+ end
9
+
10
+ def view_template
11
+ div(**attrs)
12
+ end
13
+
14
+ private
15
+
16
+ def default_attrs
17
+ {
18
+ aria_hidden: "true",
19
+ data: {
20
+ ruby_ui__input_otp_target: "slot",
21
+ index: @index
22
+ },
23
+ class: [
24
+ "relative flex h-9 w-9 items-center justify-center border-y border-r border-input text-sm shadow-xs transition-all",
25
+ "first:rounded-l-md first:border-l last:rounded-r-md",
26
+ "data-[active=true]:z-10 data-[active=true]:border-ring data-[active=true]:ring-2 data-[active=true]:ring-ring/50",
27
+ "data-[caret=true]:after:content-[''] data-[caret=true]:after:absolute data-[caret=true]:after:h-4 data-[caret=true]:after:w-px data-[caret=true]:after:animate-caret-blink data-[caret=true]:after:bg-foreground",
28
+ "aria-invalid:border-destructive data-[active=true]:aria-invalid:border-destructive data-[active=true]:aria-invalid:ring-destructive/20 dark:data-[active=true]:aria-invalid:ring-destructive/40"
29
+ ]
30
+ }
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyUI
4
+ class Message < Base
5
+ def initialize(align: :start, **attrs)
6
+ @align = align
7
+ super(**attrs)
8
+ end
9
+
10
+ def view_template(&)
11
+ div(**attrs, &)
12
+ end
13
+
14
+ private
15
+
16
+ def default_attrs
17
+ {
18
+ data: {slot: "message", align: @align},
19
+ class: "group/message relative flex w-full min-w-0 gap-2 text-sm data-[align=end]:flex-row-reverse"
20
+ }
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyUI
4
+ class MessageAvatar < Base
5
+ def view_template(&)
6
+ div(**attrs, &)
7
+ end
8
+
9
+ private
10
+
11
+ def default_attrs
12
+ {
13
+ data: {slot: "message-avatar"},
14
+ class: "flex w-fit min-w-8 shrink-0 items-center justify-center self-end overflow-hidden rounded-full bg-muted group-has-data-[slot=message-footer]/message:-translate-y-8"
15
+ }
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyUI
4
+ class MessageContent < Base
5
+ def view_template(&)
6
+ div(**attrs, &)
7
+ end
8
+
9
+ private
10
+
11
+ def default_attrs
12
+ {
13
+ data: {slot: "message-content"},
14
+ class: "flex w-full min-w-0 flex-col gap-2.5 wrap-break-word group-data-[align=end]/message:[&>[data-slot]]:self-end"
15
+ }
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,173 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Views::Docs::Message < Views::Base
4
+ def view_template
5
+ component = "Message"
6
+
7
+ div(class: "max-w-2xl mx-auto w-full py-10 space-y-10") do
8
+ render Docs::Header.new(title: "Message", description: "A chat message layout that pairs an avatar with bubbles, headers, and footers. Built on top of Avatar and Bubble.")
9
+
10
+ Heading(level: 2) { "Usage" }
11
+
12
+ render Docs::VisualCodeExample.new(title: "Conversation", context: self) do
13
+ <<~RUBY
14
+ div(class: "flex flex-col gap-6") do
15
+ Message(align: :end) do
16
+ MessageAvatar do
17
+ Avatar(size: :sm) do
18
+ AvatarImage(src: "https://github.com/joeldrapper.png", alt: "@me")
19
+ AvatarFallback { "ME" }
20
+ end
21
+ end
22
+ MessageContent do
23
+ Bubble do
24
+ BubbleContent { "Deploying to prod real quick." }
25
+ end
26
+ end
27
+ end
28
+
29
+ Message do
30
+ MessageAvatar do
31
+ Avatar(size: :sm) do
32
+ AvatarImage(src: "https://github.com/shadcn.png", alt: "@rabbit")
33
+ AvatarFallback { "R" }
34
+ end
35
+ end
36
+ MessageContent do
37
+ Bubble(variant: :muted) do
38
+ BubbleContent { "It's 4:55 PM. On a Friday." }
39
+ end
40
+ end
41
+ end
42
+
43
+ Message(align: :end) do
44
+ MessageAvatar do
45
+ Avatar(size: :sm) do
46
+ AvatarImage(src: "https://github.com/joeldrapper.png", alt: "@me")
47
+ AvatarFallback { "ME" }
48
+ end
49
+ end
50
+ MessageContent do
51
+ Bubble do
52
+ BubbleContent { "It's a one-line change." }
53
+ end
54
+ MessageFooter { "Delivered" }
55
+ end
56
+ end
57
+
58
+ Message do
59
+ MessageAvatar do
60
+ Avatar(size: :sm) do
61
+ AvatarImage(src: "https://github.com/shadcn.png", alt: "@rabbit")
62
+ AvatarFallback { "R" }
63
+ end
64
+ end
65
+ MessageContent do
66
+ BubbleGroup do
67
+ Bubble(variant: :muted) do
68
+ BubbleContent { "It's always a one-line change 😭." }
69
+ end
70
+ Bubble(variant: :muted) do
71
+ BubbleContent { "Alright, let me take a look." }
72
+ BubbleReactions(role: "img", aria_label: "Reaction: thumbs up") do
73
+ span { "👍" }
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
80
+ RUBY
81
+ end
82
+
83
+ Heading(level: 2) { "With header" }
84
+
85
+ render Docs::VisualCodeExample.new(title: "Header and footer", context: self) do
86
+ <<~RUBY
87
+ Message do
88
+ MessageAvatar do
89
+ Avatar(size: :sm) do
90
+ AvatarImage(src: "https://github.com/shadcn.png", alt: "@oliver")
91
+ AvatarFallback { "OL" }
92
+ end
93
+ end
94
+ MessageContent do
95
+ MessageHeader { "Oliver" }
96
+ Bubble(variant: :muted) do
97
+ BubbleContent { "Pushed the fix, can you review?" }
98
+ end
99
+ MessageFooter { "9:41 AM" }
100
+ end
101
+ end
102
+ RUBY
103
+ end
104
+
105
+ Heading(level: 2) { "Alignment" }
106
+
107
+ render Docs::VisualCodeExample.new(title: "Sender and receiver", context: self) do
108
+ <<~RUBY
109
+ div(class: "flex flex-col gap-6") do
110
+ Message do
111
+ MessageAvatar do
112
+ Avatar(size: :sm) do
113
+ AvatarImage(src: "https://github.com/shadcn.png", alt: "@rabbit")
114
+ AvatarFallback { "R" }
115
+ end
116
+ end
117
+ MessageContent do
118
+ Bubble(variant: :muted) { BubbleContent { "Aligned to the start." } }
119
+ end
120
+ end
121
+ Message(align: :end) do
122
+ MessageAvatar do
123
+ Avatar(size: :sm) do
124
+ AvatarImage(src: "https://github.com/joeldrapper.png", alt: "@me")
125
+ AvatarFallback { "ME" }
126
+ end
127
+ end
128
+ MessageContent do
129
+ Bubble { BubbleContent { "Aligned to the end." } }
130
+ end
131
+ end
132
+ end
133
+ RUBY
134
+ end
135
+
136
+ Heading(level: 2) { "Group" }
137
+
138
+ render Docs::VisualCodeExample.new(title: "Message group", context: self) do
139
+ <<~RUBY
140
+ MessageGroup do
141
+ Message do
142
+ MessageAvatar do
143
+ Avatar(size: :sm) do
144
+ AvatarImage(src: "https://github.com/shadcn.png", alt: "@rabbit")
145
+ AvatarFallback { "R" }
146
+ end
147
+ end
148
+ MessageContent do
149
+ Bubble(variant: :muted) { BubbleContent { "First message." } }
150
+ end
151
+ end
152
+ Message do
153
+ MessageAvatar do
154
+ Avatar(size: :sm) do
155
+ AvatarImage(src: "https://github.com/shadcn.png", alt: "@rabbit")
156
+ AvatarFallback { "R" }
157
+ end
158
+ end
159
+ MessageContent do
160
+ Bubble(variant: :muted) { BubbleContent { "Second, tighter spacing." } }
161
+ end
162
+ end
163
+ end
164
+ RUBY
165
+ end
166
+
167
+ render Components::ComponentSetup::Tabs.new(component_name: component)
168
+
169
+ # components
170
+ render Docs::ComponentsTable.new(component_files(component))
171
+ end
172
+ end
173
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyUI
4
+ class MessageFooter < Base
5
+ def view_template(&)
6
+ div(**attrs, &)
7
+ end
8
+
9
+ private
10
+
11
+ def default_attrs
12
+ {
13
+ data: {slot: "message-footer"},
14
+ class: "flex max-w-full min-w-0 items-center px-3 text-xs font-medium text-muted-foreground group-data-[align=end]/message:justify-end group-has-data-[variant=ghost]/message:px-0"
15
+ }
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyUI
4
+ class MessageGroup < Base
5
+ def view_template(&)
6
+ div(**attrs, &)
7
+ end
8
+
9
+ private
10
+
11
+ def default_attrs
12
+ {
13
+ data: {slot: "message-group"},
14
+ class: "flex min-w-0 flex-col gap-2"
15
+ }
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyUI
4
+ class MessageHeader < Base
5
+ def view_template(&)
6
+ div(**attrs, &)
7
+ end
8
+
9
+ private
10
+
11
+ def default_attrs
12
+ {
13
+ data: {slot: "message-header"},
14
+ class: "flex max-w-full min-w-0 items-center px-3 text-xs font-medium text-muted-foreground group-has-data-[variant=ghost]/message:px-0"
15
+ }
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyUI
4
+ class MessageScroller < Base
5
+ def view_template(&)
6
+ div(**attrs, &)
7
+ end
8
+
9
+ private
10
+
11
+ def default_attrs
12
+ {
13
+ data: {slot: "message-scroller"},
14
+ class: "group/message-scroller relative flex size-full min-h-0 flex-col overflow-hidden"
15
+ }
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyUI
4
+ class MessageScrollerButton < Base
5
+ def initialize(direction: :end, **attrs)
6
+ @direction = direction
7
+ super(**attrs)
8
+ end
9
+
10
+ def view_template(&)
11
+ button(**attrs) do
12
+ if block_given?
13
+ yield
14
+ else
15
+ default_icon
16
+ span(class: "sr-only") { (@direction == :start) ? "Scroll to start" : "Scroll to end" }
17
+ end
18
+ end
19
+ end
20
+
21
+ private
22
+
23
+ def default_attrs
24
+ {
25
+ type: "button",
26
+ tabindex: "-1",
27
+ data: {
28
+ slot: "message-scroller-button",
29
+ direction: @direction,
30
+ active: "false",
31
+ ruby_ui__message_scroller_target: "button",
32
+ action: "click->ruby-ui--message-scroller#jump"
33
+ },
34
+ class: "absolute left-1/2 z-10 -translate-x-1/2 inline-flex size-8 items-center justify-center rounded-full border border-border bg-background text-foreground shadow-sm transition-[translate,scale,opacity] duration-200 hover:bg-muted hover:text-foreground data-[active=false]:pointer-events-none data-[active=false]:scale-95 data-[active=false]:opacity-0 data-[active=true]:scale-100 data-[active=true]:opacity-100 data-[direction=end]:bottom-4 data-[direction=end]:data-[active=false]:translate-y-full data-[direction=start]:top-4 data-[direction=start]:data-[active=false]:-translate-y-full data-[direction=start]:[&_svg]:rotate-180"
35
+ }
36
+ end
37
+
38
+ def default_icon
39
+ svg(
40
+ xmlns: "http://www.w3.org/2000/svg",
41
+ width: "24",
42
+ height: "24",
43
+ viewbox: "0 0 24 24",
44
+ fill: "none",
45
+ stroke: "currentColor",
46
+ stroke_width: "2",
47
+ stroke_linecap: "round",
48
+ stroke_linejoin: "round",
49
+ class: "size-4"
50
+ ) do |s|
51
+ s.path(d: "M12 5v14")
52
+ s.path(d: "m19 12-7 7-7-7")
53
+ end
54
+ end
55
+ end
56
+ end