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
|
@@ -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,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
|