ruby_ui 1.4.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/lib/generators/ruby_ui/dependencies.yml +12 -2
- data/lib/generators/ruby_ui/javascript_utils.rb +0 -13
- data/lib/ruby_ui/accordion/accordion_controller.js +16 -9
- 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/dialog/dialog_content.rb +1 -1
- 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/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/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/toast/toaster_controller.js +24 -8
- data/lib/ruby_ui.rb +1 -1
- metadata +34 -1
|
@@ -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
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyUI
|
|
4
|
+
class MessageScrollerContent < Base
|
|
5
|
+
def view_template(&)
|
|
6
|
+
div(**attrs, &)
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
private
|
|
10
|
+
|
|
11
|
+
def default_attrs
|
|
12
|
+
{
|
|
13
|
+
role: "log",
|
|
14
|
+
aria_relevant: "additions text",
|
|
15
|
+
data: {
|
|
16
|
+
slot: "message-scroller-content",
|
|
17
|
+
ruby_ui__message_scroller_target: "content"
|
|
18
|
+
},
|
|
19
|
+
class: "flex h-max min-h-full flex-col gap-8"
|
|
20
|
+
}
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|