odt-ui-rails 2.2.0 → 2.2.2
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 +308 -44
- data/app/assets/stylesheets/odt_ui/styles.css +420 -0
- data/app/javascript/controllers/odt_modal_controller.js +61 -0
- data/app/javascript/controllers/odt_toast_controller.js +40 -0
- data/lib/generators/odt/install_generator.rb +12 -4
- data/lib/generators/odt/templates/odt_modal_controller.js +61 -0
- data/lib/generators/odt/templates/odt_toast_controller.js +40 -0
- data/lib/generators/odt/templates/odt_ui.rbs +302 -0
- data/lib/odt_ui/helper.rb +265 -0
- data/lib/odt_ui/version.rb +1 -1
- data/sig/odt_ui.rbs +72 -0
- metadata +22 -4
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
module OdtUi
|
|
2
|
+
VERSION: String
|
|
3
|
+
|
|
4
|
+
module Helper
|
|
5
|
+
type button_variant = :filled | :frosted | :capsule | :ghost | :light | :outline | :outlined | :subtle | String
|
|
6
|
+
type button_color = :primary | :secondary | :surface | :muted | :inverse | :success | :warning | :danger | :info | :neutral | String
|
|
7
|
+
type button_size = :xs | :sm | :md | :lg | :xl | String
|
|
8
|
+
type radius_size = :none | :xs | :sm | :md | :lg | :xl | :"2xl" | :"3xl" | :"4xl" | :full | Symbol | String
|
|
9
|
+
type shadow_size = :none | :card | :dropdown | :modal | :popover | Symbol | String
|
|
10
|
+
|
|
11
|
+
type badge_variant = :subtle | :filled | :outlined | :outline | String
|
|
12
|
+
type badge_color = :primary | :secondary | :success | :warning | :danger | :info | :neutral | String
|
|
13
|
+
type badge_size = :sm | :md | String
|
|
14
|
+
|
|
15
|
+
type card_variant = :elevated | :outlined | :subtle | :muted | :ghost | :frosted | String
|
|
16
|
+
type card_background = :base | :surface | :surface_elevated | :surface_muted | :surface_subtle | :gradient_primary | :gradient_secondary | :gradient_vibrant | :gradient_hero | :frosted | :frosted_muted | :frosted_subtle | :frosted_gradient_primary | :frosted_gradient_secondary | :transparent | Symbol | String
|
|
17
|
+
type card_border = :none | :muted | :base | :strong | :primary | :secondary | :danger | :success | bool | Symbol | String
|
|
18
|
+
type card_padding = :none | :xs | :sm | :md | :lg | :xl | :"2xl" | Symbol | String
|
|
19
|
+
type card_tag = :div | :article | :section | :aside | :main | Symbol | String
|
|
20
|
+
|
|
21
|
+
type alert_variant = :success | :warning | :danger | :info | String
|
|
22
|
+
type heading_tag = :h1 | :h2 | :h3 | :h4 | :h5 | :h6 | String
|
|
23
|
+
type heading_size = :"5xl" | :"4xl" | :"3xl" | :"2xl" | :xl | :lg | Symbol | String
|
|
24
|
+
type font_weight = :light | :normal | :medium | :semibold | :bold | :extrabold | Symbol | String
|
|
25
|
+
type typography_color = :strong | :default | :muted | :subtle | :disabled | :primary | :secondary | :success | :warning | :danger | :info | Symbol | String
|
|
26
|
+
|
|
27
|
+
type text_tag = :p | :span | :div | :label | :small | Symbol | String
|
|
28
|
+
type text_size = :xs | :sm | :base | :lg | :xl | :"2xl" | Symbol | String
|
|
29
|
+
|
|
30
|
+
type avatar_size = :xs | :sm | :md | :lg | :xl | :"2xl" | String
|
|
31
|
+
type avatar_color = :secondary | :primary | :neutral | String
|
|
32
|
+
|
|
33
|
+
type input_variant = :outlined | :filled | :frosted | String
|
|
34
|
+
type input_size = :sm | :md | :lg | String
|
|
35
|
+
type input_resize = :none | :vertical | :horizontal | :both | String
|
|
36
|
+
type checkbox_color = :primary | :secondary | :success | :warning | :danger | :dark | :neutral | String
|
|
37
|
+
type checkbox_size = :sm | :md | :lg | String
|
|
38
|
+
|
|
39
|
+
# Renders an ODT Button component.
|
|
40
|
+
def odt_button: (
|
|
41
|
+
?String? text,
|
|
42
|
+
?variant: button_variant,
|
|
43
|
+
?color: button_color,
|
|
44
|
+
?size: button_size,
|
|
45
|
+
?radius: radius_size?,
|
|
46
|
+
?shadow: shadow_size?,
|
|
47
|
+
?icon_only: bool,
|
|
48
|
+
?full_width: bool,
|
|
49
|
+
?loading: bool,
|
|
50
|
+
?disabled: bool,
|
|
51
|
+
?href: String?,
|
|
52
|
+
?icon: String?,
|
|
53
|
+
?right_icon: String?,
|
|
54
|
+
?type: String,
|
|
55
|
+
**untyped
|
|
56
|
+
) ?{ () -> untyped } -> untyped
|
|
57
|
+
|
|
58
|
+
# Renders an ODT Badge component.
|
|
59
|
+
def odt_badge: (
|
|
60
|
+
?String? text,
|
|
61
|
+
?variant: badge_variant,
|
|
62
|
+
?color: badge_color,
|
|
63
|
+
?size: badge_size,
|
|
64
|
+
?radius: radius_size?,
|
|
65
|
+
?dot: bool,
|
|
66
|
+
?icon: String?,
|
|
67
|
+
**untyped
|
|
68
|
+
) ?{ () -> untyped } -> untyped
|
|
69
|
+
|
|
70
|
+
# Renders an ODT Card component.
|
|
71
|
+
def odt_card: (
|
|
72
|
+
?title: String?,
|
|
73
|
+
?subtitle: String?,
|
|
74
|
+
?variant: card_variant,
|
|
75
|
+
?background: card_background?,
|
|
76
|
+
?border: card_border?,
|
|
77
|
+
?shadow: shadow_size?,
|
|
78
|
+
?radius: radius_size?,
|
|
79
|
+
?padding: card_padding?,
|
|
80
|
+
?hoverable: bool,
|
|
81
|
+
?hover: bool,
|
|
82
|
+
?as: card_tag,
|
|
83
|
+
**untyped
|
|
84
|
+
) ?{ () -> untyped } -> untyped
|
|
85
|
+
|
|
86
|
+
# Renders an ODT Alert banner.
|
|
87
|
+
def odt_alert: (
|
|
88
|
+
?String? message,
|
|
89
|
+
?variant: alert_variant,
|
|
90
|
+
?icon: String?,
|
|
91
|
+
**untyped
|
|
92
|
+
) ?{ () -> untyped } -> untyped
|
|
93
|
+
|
|
94
|
+
# Renders an ODT Heading element.
|
|
95
|
+
def odt_heading: (
|
|
96
|
+
?String? text,
|
|
97
|
+
?as: heading_tag,
|
|
98
|
+
?size: heading_size?,
|
|
99
|
+
?weight: font_weight?,
|
|
100
|
+
?color: typography_color?,
|
|
101
|
+
?truncate: bool,
|
|
102
|
+
**untyped
|
|
103
|
+
) ?{ () -> untyped } -> untyped
|
|
104
|
+
|
|
105
|
+
# Renders styled body text.
|
|
106
|
+
def odt_text: (
|
|
107
|
+
?String? text,
|
|
108
|
+
?as: text_tag,
|
|
109
|
+
?size: text_size?,
|
|
110
|
+
?weight: font_weight?,
|
|
111
|
+
?color: typography_color?,
|
|
112
|
+
?truncate: bool,
|
|
113
|
+
**untyped
|
|
114
|
+
) ?{ () -> untyped } -> untyped
|
|
115
|
+
|
|
116
|
+
# Renders an ODT Avatar element.
|
|
117
|
+
def odt_avatar: (
|
|
118
|
+
?name: String?,
|
|
119
|
+
?src: String?,
|
|
120
|
+
?size: avatar_size,
|
|
121
|
+
?color: avatar_color,
|
|
122
|
+
?icon: String,
|
|
123
|
+
?fallback: String?,
|
|
124
|
+
**untyped
|
|
125
|
+
) -> untyped
|
|
126
|
+
|
|
127
|
+
# Renders a styled input field.
|
|
128
|
+
def odt_input: (
|
|
129
|
+
?label: String?,
|
|
130
|
+
?variant: input_variant,
|
|
131
|
+
?size: input_size,
|
|
132
|
+
?radius: radius_size?,
|
|
133
|
+
?type: String,
|
|
134
|
+
?placeholder: String?,
|
|
135
|
+
?error: String?,
|
|
136
|
+
?helper: String?,
|
|
137
|
+
?helper_text: String?,
|
|
138
|
+
?success: String?,
|
|
139
|
+
?left_icon: String?,
|
|
140
|
+
?right_icon: String?,
|
|
141
|
+
?prefix: String?,
|
|
142
|
+
?suffix: String?,
|
|
143
|
+
?addon_before: String?,
|
|
144
|
+
?addon_after: String?,
|
|
145
|
+
?disabled: bool,
|
|
146
|
+
?full_width: bool,
|
|
147
|
+
**untyped
|
|
148
|
+
) -> untyped
|
|
149
|
+
|
|
150
|
+
# Renders a styled textarea field.
|
|
151
|
+
def odt_textarea: (
|
|
152
|
+
?label: String?,
|
|
153
|
+
?variant: input_variant,
|
|
154
|
+
?size: input_size,
|
|
155
|
+
?radius: radius_size?,
|
|
156
|
+
?rows: Integer,
|
|
157
|
+
?resize: input_resize,
|
|
158
|
+
?error: String?,
|
|
159
|
+
?helper: String?,
|
|
160
|
+
?helper_text: String?,
|
|
161
|
+
?success: String?,
|
|
162
|
+
?disabled: bool,
|
|
163
|
+
?full_width: bool,
|
|
164
|
+
**untyped
|
|
165
|
+
) ?{ () -> untyped } -> untyped
|
|
166
|
+
|
|
167
|
+
# Renders a styled native <select> dropdown.
|
|
168
|
+
def odt_select: (
|
|
169
|
+
?label: String?,
|
|
170
|
+
?options: untyped,
|
|
171
|
+
?variant: input_variant,
|
|
172
|
+
?size: input_size,
|
|
173
|
+
?radius: radius_size?,
|
|
174
|
+
?prompt: String?,
|
|
175
|
+
?error: String?,
|
|
176
|
+
?helper: String?,
|
|
177
|
+
?helper_text: String?,
|
|
178
|
+
?success: String?,
|
|
179
|
+
?disabled: bool,
|
|
180
|
+
?full_width: bool,
|
|
181
|
+
**untyped
|
|
182
|
+
) -> untyped
|
|
183
|
+
|
|
184
|
+
# Renders a styled checkbox.
|
|
185
|
+
def odt_checkbox: (
|
|
186
|
+
?label: String?,
|
|
187
|
+
?description: String?,
|
|
188
|
+
?checked: bool,
|
|
189
|
+
?color: checkbox_color,
|
|
190
|
+
?size: checkbox_size,
|
|
191
|
+
?radius: radius_size?,
|
|
192
|
+
?disabled: bool,
|
|
193
|
+
?error: String?,
|
|
194
|
+
**untyped
|
|
195
|
+
) -> untyped
|
|
196
|
+
|
|
197
|
+
# Renders a styled radio button.
|
|
198
|
+
def odt_radio: (
|
|
199
|
+
?label: String?,
|
|
200
|
+
?description: String?,
|
|
201
|
+
?checked: bool,
|
|
202
|
+
?color: checkbox_color,
|
|
203
|
+
?size: checkbox_size,
|
|
204
|
+
?disabled: bool,
|
|
205
|
+
?error: String?,
|
|
206
|
+
**untyped
|
|
207
|
+
) -> untyped
|
|
208
|
+
|
|
209
|
+
# Renders a form field wrapper.
|
|
210
|
+
def odt_form_group: (
|
|
211
|
+
?label: String?,
|
|
212
|
+
?error: String?,
|
|
213
|
+
?helper: String?,
|
|
214
|
+
?helper_text: String?,
|
|
215
|
+
?success: String?,
|
|
216
|
+
?required: bool,
|
|
217
|
+
**untyped
|
|
218
|
+
) ?{ () -> untyped } -> untyped
|
|
219
|
+
|
|
220
|
+
type toaster_position = :top_right | :top_center | :top_left | :bottom_right | :bottom_center | :bottom_left | Symbol | String
|
|
221
|
+
type toast_variant = :success | :warning | :danger | :error | :info | :loading | :frosted | Symbol | String
|
|
222
|
+
|
|
223
|
+
# Renders an ODT Toaster viewport container.
|
|
224
|
+
def odt_toaster: (
|
|
225
|
+
?position: toaster_position,
|
|
226
|
+
?id: String,
|
|
227
|
+
**untyped
|
|
228
|
+
) ?{ () -> untyped } -> untyped
|
|
229
|
+
|
|
230
|
+
# Renders a single ODT Toast card.
|
|
231
|
+
def odt_toast: (
|
|
232
|
+
?String? message,
|
|
233
|
+
?title: String?,
|
|
234
|
+
?variant: toast_variant,
|
|
235
|
+
?icon: String?,
|
|
236
|
+
?duration: Integer,
|
|
237
|
+
?dismissible: bool,
|
|
238
|
+
?radius: radius_size?,
|
|
239
|
+
**untyped
|
|
240
|
+
) ?{ () -> untyped } -> untyped
|
|
241
|
+
|
|
242
|
+
# Renders active flash messages as ODT Toasts.
|
|
243
|
+
def odt_flash_toasts: (
|
|
244
|
+
?untyped flash_hash,
|
|
245
|
+
?duration: Integer
|
|
246
|
+
) -> untyped
|
|
247
|
+
|
|
248
|
+
# Generates a Turbo Stream action for a toast.
|
|
249
|
+
def odt_toast_stream: (
|
|
250
|
+
?String? message,
|
|
251
|
+
?title: String?,
|
|
252
|
+
?variant: toast_variant,
|
|
253
|
+
?target: String,
|
|
254
|
+
?action: Symbol | String,
|
|
255
|
+
?duration: Integer,
|
|
256
|
+
**untyped
|
|
257
|
+
) -> untyped
|
|
258
|
+
|
|
259
|
+
type modal_size = :sm | :md | :lg | :xl | :full | Symbol | String
|
|
260
|
+
|
|
261
|
+
# Renders an ODT Modal dialog container.
|
|
262
|
+
def odt_modal: (
|
|
263
|
+
?id: String?,
|
|
264
|
+
?title: String?,
|
|
265
|
+
?subtitle: String?,
|
|
266
|
+
?size: modal_size,
|
|
267
|
+
?radius: radius_size?,
|
|
268
|
+
?dismissible: bool,
|
|
269
|
+
?open: bool,
|
|
270
|
+
?prevent_backdrop_close: bool,
|
|
271
|
+
**untyped
|
|
272
|
+
) ?{ () -> untyped } -> untyped
|
|
273
|
+
|
|
274
|
+
# Renders an ODT Modal Header subcomponent.
|
|
275
|
+
def odt_modal_header: (
|
|
276
|
+
?title: String?,
|
|
277
|
+
?subtitle: String?,
|
|
278
|
+
?dismissible: bool,
|
|
279
|
+
**untyped
|
|
280
|
+
) ?{ () -> untyped } -> untyped
|
|
281
|
+
|
|
282
|
+
# Renders an ODT Modal Body subcomponent.
|
|
283
|
+
def odt_modal_body: (
|
|
284
|
+
**untyped
|
|
285
|
+
) ?{ () -> untyped } -> untyped
|
|
286
|
+
|
|
287
|
+
# Renders an ODT Modal Footer subcomponent.
|
|
288
|
+
def odt_modal_footer: (
|
|
289
|
+
**untyped
|
|
290
|
+
) ?{ () -> untyped } -> untyped
|
|
291
|
+
end
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
module ActionView
|
|
295
|
+
class Base
|
|
296
|
+
include OdtUi::Helper
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
module Helpers
|
|
300
|
+
include OdtUi::Helper
|
|
301
|
+
end
|
|
302
|
+
end
|
data/lib/odt_ui/helper.rb
CHANGED
|
@@ -677,6 +677,271 @@ module OdtUi
|
|
|
677
677
|
end
|
|
678
678
|
end
|
|
679
679
|
|
|
680
|
+
# Renders an ODT Toaster viewport container.
|
|
681
|
+
#
|
|
682
|
+
# @param position [:top_right, :top_center, :top_left, :bottom_right, :bottom_center, :bottom_left]
|
|
683
|
+
# @param id [String] Element ID for Turbo Stream targeting (default: "odt-toasts")
|
|
684
|
+
# @return [ActiveSupport::SafeBuffer]
|
|
685
|
+
def odt_toaster(position: :top_right, id: "odt-toasts", **html_options, &block)
|
|
686
|
+
pos_key = position.to_s.tr("_", "-")
|
|
687
|
+
classes = [
|
|
688
|
+
"odt-toaster",
|
|
689
|
+
"odt-toaster--#{pos_key}",
|
|
690
|
+
html_options.delete(:class)
|
|
691
|
+
]
|
|
692
|
+
|
|
693
|
+
opts = html_options.merge(
|
|
694
|
+
id: id,
|
|
695
|
+
class: classes.compact.reject(&:blank?).join(" "),
|
|
696
|
+
role: "region",
|
|
697
|
+
aria: { live: "polite" }
|
|
698
|
+
)
|
|
699
|
+
|
|
700
|
+
inner_content = block_given? ? capture(&block) : nil
|
|
701
|
+
tag.div(inner_content, **opts)
|
|
702
|
+
end
|
|
703
|
+
|
|
704
|
+
# Renders a single ODT Toast card with Stimulus controller.
|
|
705
|
+
#
|
|
706
|
+
# @param message [String, nil]
|
|
707
|
+
# @param title [String, nil]
|
|
708
|
+
# @param variant [:success, :warning, :danger, :error, :info, :loading, :frosted]
|
|
709
|
+
# @param icon [String, nil]
|
|
710
|
+
# @param duration [Integer] Auto-dismiss duration in milliseconds (0 to disable)
|
|
711
|
+
# @param dismissible [Boolean] Whether to display close button
|
|
712
|
+
# @param radius [:none, :xs, :sm, :md, :lg, :xl, :"2xl", :"3xl", :"4xl", :full, nil]
|
|
713
|
+
# @return [ActiveSupport::SafeBuffer]
|
|
714
|
+
def odt_toast(message = nil, title: nil, variant: :success, icon: nil, duration: 4000, dismissible: true, radius: nil, **html_options, &block)
|
|
715
|
+
variant_key = variant.to_sym == :error ? :danger : variant.to_sym
|
|
716
|
+
default_icons = {
|
|
717
|
+
success: "fa-solid fa-circle-check",
|
|
718
|
+
warning: "fa-solid fa-triangle-exclamation",
|
|
719
|
+
danger: "fa-solid fa-circle-xmark",
|
|
720
|
+
info: "fa-solid fa-circle-info",
|
|
721
|
+
loading: "fa-solid fa-spinner"
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
icon_name = icon || default_icons[variant_key] || default_icons[:info]
|
|
725
|
+
|
|
726
|
+
classes = [
|
|
727
|
+
"odt-toast",
|
|
728
|
+
"odt-toast--#{variant_key.to_s.tr('_', '-')}"
|
|
729
|
+
]
|
|
730
|
+
classes << "odt-radius-#{radius.to_s.tr('_', '-')}" if radius.present?
|
|
731
|
+
classes << html_options.delete(:class)
|
|
732
|
+
|
|
733
|
+
data_attrs = {
|
|
734
|
+
controller: "odt-toast",
|
|
735
|
+
odt_toast_duration_value: duration,
|
|
736
|
+
action: "mouseenter->odt-toast#pauseTimer mouseleave->odt-toast#resumeTimer"
|
|
737
|
+
}.merge(html_options.delete(:data) || {})
|
|
738
|
+
|
|
739
|
+
opts = html_options.merge(
|
|
740
|
+
class: classes.compact.reject(&:blank?).join(" "),
|
|
741
|
+
role: "status",
|
|
742
|
+
data: data_attrs
|
|
743
|
+
)
|
|
744
|
+
|
|
745
|
+
parts = []
|
|
746
|
+
if icon_name.present?
|
|
747
|
+
icon_classes = ["odt-toast__icon", "odt-toast__icon--#{variant_key}"]
|
|
748
|
+
parts << tag.span(render_icon_element(icon_name), class: icon_classes.join(" "))
|
|
749
|
+
end
|
|
750
|
+
|
|
751
|
+
body_content = block_given? ? capture(&block) : message
|
|
752
|
+
|
|
753
|
+
parts << tag.div(class: "odt-toast__content") do
|
|
754
|
+
c_parts = []
|
|
755
|
+
c_parts << tag.p(title, class: "odt-toast__title") if title.present?
|
|
756
|
+
c_parts << tag.p(body_content, class: "odt-toast__message") if body_content.present?
|
|
757
|
+
safe_join(c_parts)
|
|
758
|
+
end
|
|
759
|
+
|
|
760
|
+
if dismissible
|
|
761
|
+
parts << tag.button(
|
|
762
|
+
"×".html_safe,
|
|
763
|
+
type: "button",
|
|
764
|
+
class: "odt-toast__close",
|
|
765
|
+
aria: { label: "Close" },
|
|
766
|
+
data: { action: "click->odt-toast#dismiss" }
|
|
767
|
+
)
|
|
768
|
+
end
|
|
769
|
+
|
|
770
|
+
inner_content = safe_join(parts.compact)
|
|
771
|
+
tag.div(inner_content, **opts)
|
|
772
|
+
end
|
|
773
|
+
|
|
774
|
+
# Renders all active flash messages as ODT Toasts.
|
|
775
|
+
#
|
|
776
|
+
# @param flash_hash [ActionDispatch::Flash::FlashHash, Hash] Flash messages hash (default: flash)
|
|
777
|
+
# @param duration [Integer] Auto-dismiss duration in milliseconds
|
|
778
|
+
# @return [ActiveSupport::SafeBuffer]
|
|
779
|
+
def odt_flash_toasts(flash_hash = flash, duration: 4000)
|
|
780
|
+
return "".html_safe if flash_hash.blank?
|
|
781
|
+
|
|
782
|
+
toasts = []
|
|
783
|
+
flash_hash.each do |type, message|
|
|
784
|
+
next if message.blank?
|
|
785
|
+
|
|
786
|
+
variant = case type.to_sym
|
|
787
|
+
when :notice, :success
|
|
788
|
+
:success
|
|
789
|
+
when :alert, :error, :danger
|
|
790
|
+
:danger
|
|
791
|
+
when :warning
|
|
792
|
+
:warning
|
|
793
|
+
else
|
|
794
|
+
:info
|
|
795
|
+
end
|
|
796
|
+
|
|
797
|
+
toasts << odt_toast(message, variant: variant, duration: duration)
|
|
798
|
+
end
|
|
799
|
+
|
|
800
|
+
safe_join(toasts)
|
|
801
|
+
end
|
|
802
|
+
|
|
803
|
+
# Generates a Turbo Stream action to render a toast asynchronously.
|
|
804
|
+
#
|
|
805
|
+
# @param message [String, nil]
|
|
806
|
+
# @param title [String, nil]
|
|
807
|
+
# @param variant [:success, :warning, :danger, :error, :info]
|
|
808
|
+
# @param target [String] Target container ID (default: "odt-toasts")
|
|
809
|
+
# @param action [:append, :prepend] Turbo stream action (default: :append)
|
|
810
|
+
# @param duration [Integer] Auto-dismiss duration in ms (default: 4000)
|
|
811
|
+
# @return [ActiveSupport::SafeBuffer]
|
|
812
|
+
def odt_toast_stream(message = nil, title: nil, variant: :success, target: "odt-toasts", action: :append, duration: 4000, **options)
|
|
813
|
+
toast_html = odt_toast(message, title: title, variant: variant, duration: duration, **options)
|
|
814
|
+
|
|
815
|
+
if respond_to?(:turbo_stream)
|
|
816
|
+
turbo_stream.public_send(action, target, toast_html)
|
|
817
|
+
else
|
|
818
|
+
tag.public_send("turbo-stream", action: action, target: target) do
|
|
819
|
+
tag.template(toast_html)
|
|
820
|
+
end
|
|
821
|
+
end
|
|
822
|
+
end
|
|
823
|
+
|
|
824
|
+
# Renders an ODT Modal dialog container with Stimulus controller.
|
|
825
|
+
#
|
|
826
|
+
# @param id [String, nil] Unique HTML ID of the modal
|
|
827
|
+
# @param title [String, nil] Header title text
|
|
828
|
+
# @param subtitle [String, nil] Header subtitle text
|
|
829
|
+
# @param size [:sm, :md, :lg, :xl, :full] Dialog width size (default: :md)
|
|
830
|
+
# @param radius [:none, :xs, :sm, :md, :lg, :xl, :"2xl", :"3xl", :"4xl", :full, nil]
|
|
831
|
+
# @param dismissible [Boolean] Whether to render the header close button (default: true)
|
|
832
|
+
# @param open [Boolean] Whether modal is initially open (default: false)
|
|
833
|
+
# @param prevent_backdrop_close [Boolean] Prevent closing when clicking overlay (default: false)
|
|
834
|
+
# @return [ActiveSupport::SafeBuffer]
|
|
835
|
+
def odt_modal(id: nil, title: nil, subtitle: nil, size: :md, radius: :"2xl", dismissible: true, open: false, prevent_backdrop_close: false, **html_options, &block)
|
|
836
|
+
size_key = size.to_s.tr("_", "-")
|
|
837
|
+
radius_key = radius.to_s.tr("_", "-")
|
|
838
|
+
|
|
839
|
+
overlay_classes = [
|
|
840
|
+
"odt-modal-overlay",
|
|
841
|
+
("hidden" unless open),
|
|
842
|
+
html_options.delete(:class)
|
|
843
|
+
]
|
|
844
|
+
|
|
845
|
+
data_attrs = {
|
|
846
|
+
controller: "odt-modal",
|
|
847
|
+
odt_modal_open_value: open,
|
|
848
|
+
odt_modal_prevent_backdrop_close_value: prevent_backdrop_close,
|
|
849
|
+
action: "click->odt-modal#backdropClose keydown.esc@window->odt-modal#close"
|
|
850
|
+
}.merge(html_options.delete(:data) || {})
|
|
851
|
+
|
|
852
|
+
opts = html_options.merge(
|
|
853
|
+
id: id,
|
|
854
|
+
class: overlay_classes.compact.reject(&:blank?).join(" "),
|
|
855
|
+
role: "dialog",
|
|
856
|
+
aria: { modal: "true", hidden: (!open).to_s },
|
|
857
|
+
data: data_attrs
|
|
858
|
+
)
|
|
859
|
+
|
|
860
|
+
modal_classes = [
|
|
861
|
+
"odt-modal",
|
|
862
|
+
"odt-modal--#{size_key}",
|
|
863
|
+
"odt-radius-#{radius_key}"
|
|
864
|
+
]
|
|
865
|
+
|
|
866
|
+
body_content = block_given? ? capture(&block) : nil
|
|
867
|
+
|
|
868
|
+
tag.div(**opts) do
|
|
869
|
+
tag.div(class: modal_classes.join(" "), data: { odt_modal_target: "dialog" }) do
|
|
870
|
+
parts = []
|
|
871
|
+
if title.present? || subtitle.present? || dismissible
|
|
872
|
+
parts << odt_modal_header(title: title, subtitle: subtitle, dismissible: dismissible)
|
|
873
|
+
end
|
|
874
|
+
|
|
875
|
+
if body_content.present?
|
|
876
|
+
if body_content.include?("odt-modal__body") || body_content.include?("odt-modal__header")
|
|
877
|
+
parts << body_content
|
|
878
|
+
else
|
|
879
|
+
parts << tag.div(body_content, class: "odt-modal__body")
|
|
880
|
+
end
|
|
881
|
+
end
|
|
882
|
+
|
|
883
|
+
safe_join(parts)
|
|
884
|
+
end
|
|
885
|
+
end
|
|
886
|
+
end
|
|
887
|
+
|
|
888
|
+
# Renders an ODT Modal Header subcomponent.
|
|
889
|
+
#
|
|
890
|
+
# @param title [String, nil]
|
|
891
|
+
# @param subtitle [String, nil]
|
|
892
|
+
# @param dismissible [Boolean]
|
|
893
|
+
# @return [ActiveSupport::SafeBuffer]
|
|
894
|
+
def odt_modal_header(title: nil, subtitle: nil, dismissible: true, **html_options, &block)
|
|
895
|
+
classes = ["odt-modal__header", html_options.delete(:class)]
|
|
896
|
+
opts = html_options.merge(class: classes.compact.reject(&:blank?).join(" "))
|
|
897
|
+
|
|
898
|
+
tag.div(**opts) do
|
|
899
|
+
parts = []
|
|
900
|
+
if block_given?
|
|
901
|
+
parts << capture(&block)
|
|
902
|
+
else
|
|
903
|
+
parts << tag.div(class: "odt-modal__header-content") do
|
|
904
|
+
c_parts = []
|
|
905
|
+
c_parts << tag.h3(title, class: "odt-modal__title") if title.present?
|
|
906
|
+
c_parts << tag.p(subtitle, class: "odt-modal__subtitle") if subtitle.present?
|
|
907
|
+
safe_join(c_parts)
|
|
908
|
+
end
|
|
909
|
+
end
|
|
910
|
+
|
|
911
|
+
if dismissible
|
|
912
|
+
parts << tag.button(
|
|
913
|
+
"×".html_safe,
|
|
914
|
+
type: "button",
|
|
915
|
+
class: "odt-modal__close",
|
|
916
|
+
aria: { label: "Close" },
|
|
917
|
+
data: { action: "click->odt-modal#close" }
|
|
918
|
+
)
|
|
919
|
+
end
|
|
920
|
+
|
|
921
|
+
safe_join(parts)
|
|
922
|
+
end
|
|
923
|
+
end
|
|
924
|
+
|
|
925
|
+
# Renders an ODT Modal Body subcomponent.
|
|
926
|
+
#
|
|
927
|
+
# @return [ActiveSupport::SafeBuffer]
|
|
928
|
+
def odt_modal_body(**html_options, &block)
|
|
929
|
+
classes = ["odt-modal__body", html_options.delete(:class)]
|
|
930
|
+
opts = html_options.merge(class: classes.compact.reject(&:blank?).join(" "))
|
|
931
|
+
content = block_given? ? capture(&block) : nil
|
|
932
|
+
tag.div(content, **opts)
|
|
933
|
+
end
|
|
934
|
+
|
|
935
|
+
# Renders an ODT Modal Footer subcomponent.
|
|
936
|
+
#
|
|
937
|
+
# @return [ActiveSupport::SafeBuffer]
|
|
938
|
+
def odt_modal_footer(**html_options, &block)
|
|
939
|
+
classes = ["odt-modal__footer", html_options.delete(:class)]
|
|
940
|
+
opts = html_options.merge(class: classes.compact.reject(&:blank?).join(" "))
|
|
941
|
+
content = block_given? ? capture(&block) : nil
|
|
942
|
+
tag.div(content, **opts)
|
|
943
|
+
end
|
|
944
|
+
|
|
680
945
|
private
|
|
681
946
|
|
|
682
947
|
# Normalizes button variant key with backwards compatibility
|
data/lib/odt_ui/version.rb
CHANGED
data/sig/odt_ui.rbs
CHANGED
|
@@ -216,6 +216,78 @@ module OdtUi
|
|
|
216
216
|
?required: bool,
|
|
217
217
|
**untyped
|
|
218
218
|
) ?{ () -> untyped } -> untyped
|
|
219
|
+
|
|
220
|
+
type toaster_position = :top_right | :top_center | :top_left | :bottom_right | :bottom_center | :bottom_left | Symbol | String
|
|
221
|
+
type toast_variant = :success | :warning | :danger | :error | :info | :loading | :frosted | Symbol | String
|
|
222
|
+
|
|
223
|
+
# Renders an ODT Toaster viewport container.
|
|
224
|
+
def odt_toaster: (
|
|
225
|
+
?position: toaster_position,
|
|
226
|
+
?id: String,
|
|
227
|
+
**untyped
|
|
228
|
+
) ?{ () -> untyped } -> untyped
|
|
229
|
+
|
|
230
|
+
# Renders a single ODT Toast card.
|
|
231
|
+
def odt_toast: (
|
|
232
|
+
?String? message,
|
|
233
|
+
?title: String?,
|
|
234
|
+
?variant: toast_variant,
|
|
235
|
+
?icon: String?,
|
|
236
|
+
?duration: Integer,
|
|
237
|
+
?dismissible: bool,
|
|
238
|
+
?radius: radius_size?,
|
|
239
|
+
**untyped
|
|
240
|
+
) ?{ () -> untyped } -> untyped
|
|
241
|
+
|
|
242
|
+
# Renders active flash messages as ODT Toasts.
|
|
243
|
+
def odt_flash_toasts: (
|
|
244
|
+
?untyped flash_hash,
|
|
245
|
+
?duration: Integer
|
|
246
|
+
) -> untyped
|
|
247
|
+
|
|
248
|
+
# Generates a Turbo Stream action for a toast.
|
|
249
|
+
def odt_toast_stream: (
|
|
250
|
+
?String? message,
|
|
251
|
+
?title: String?,
|
|
252
|
+
?variant: toast_variant,
|
|
253
|
+
?target: String,
|
|
254
|
+
?action: Symbol | String,
|
|
255
|
+
?duration: Integer,
|
|
256
|
+
**untyped
|
|
257
|
+
) -> untyped
|
|
258
|
+
|
|
259
|
+
type modal_size = :sm | :md | :lg | :xl | :full | Symbol | String
|
|
260
|
+
|
|
261
|
+
# Renders an ODT Modal dialog container.
|
|
262
|
+
def odt_modal: (
|
|
263
|
+
?id: String?,
|
|
264
|
+
?title: String?,
|
|
265
|
+
?subtitle: String?,
|
|
266
|
+
?size: modal_size,
|
|
267
|
+
?radius: radius_size?,
|
|
268
|
+
?dismissible: bool,
|
|
269
|
+
?open: bool,
|
|
270
|
+
?prevent_backdrop_close: bool,
|
|
271
|
+
**untyped
|
|
272
|
+
) ?{ () -> untyped } -> untyped
|
|
273
|
+
|
|
274
|
+
# Renders an ODT Modal Header subcomponent.
|
|
275
|
+
def odt_modal_header: (
|
|
276
|
+
?title: String?,
|
|
277
|
+
?subtitle: String?,
|
|
278
|
+
?dismissible: bool,
|
|
279
|
+
**untyped
|
|
280
|
+
) ?{ () -> untyped } -> untyped
|
|
281
|
+
|
|
282
|
+
# Renders an ODT Modal Body subcomponent.
|
|
283
|
+
def odt_modal_body: (
|
|
284
|
+
**untyped
|
|
285
|
+
) ?{ () -> untyped } -> untyped
|
|
286
|
+
|
|
287
|
+
# Renders an ODT Modal Footer subcomponent.
|
|
288
|
+
def odt_modal_footer: (
|
|
289
|
+
**untyped
|
|
290
|
+
) ?{ () -> untyped } -> untyped
|
|
219
291
|
end
|
|
220
292
|
end
|
|
221
293
|
|