odt-ui-rails 2.0.1 → 2.2.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 +80 -18
- data/app/assets/stylesheets/odt_ui/styles.css +907 -3195
- data/app/assets/stylesheets/odt_ui/theme.css +126 -0
- data/app/assets/stylesheets/odt_ui/tokens.css +189 -46
- data/lib/generators/odt/install_generator.rb +45 -4
- data/lib/odt_ui/helper.rb +628 -241
- data/lib/odt_ui/version.rb +1 -1
- data/sig/odt_ui.rbs +194 -61
- metadata +2 -1
data/lib/odt_ui/helper.rb
CHANGED
|
@@ -2,154 +2,68 @@
|
|
|
2
2
|
|
|
3
3
|
module OdtUi
|
|
4
4
|
module Helper
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
success: "bg-success text-white hover:opacity-90 active:opacity-100 shadow-sm focus-visible:ring-green-400",
|
|
10
|
-
warning: "bg-warning text-slate-900 hover:opacity-90 active:opacity-100 shadow-sm focus-visible:ring-amber-400",
|
|
11
|
-
danger: "bg-danger text-white hover:opacity-90 active:opacity-100 shadow-sm focus-visible:ring-red-400",
|
|
12
|
-
neutral: "bg-neutral-800 text-white hover:bg-neutral-900 active:bg-neutral-950 shadow-sm focus-visible:ring-neutral-400"
|
|
13
|
-
},
|
|
14
|
-
light: {
|
|
15
|
-
primary: "bg-primary-50 text-primary-700 hover:bg-primary-100 active:bg-primary-200 focus-visible:ring-primary-300",
|
|
16
|
-
secondary: "bg-secondary-50 text-secondary-900 hover:bg-secondary-100 active:bg-secondary-200 focus-visible:ring-secondary-300",
|
|
17
|
-
success: "bg-surface-success text-fg-success hover:bg-green-100 active:bg-green-200 focus-visible:ring-green-300",
|
|
18
|
-
warning: "bg-surface-warning text-fg-warning hover:bg-amber-100 active:bg-amber-200 focus-visible:ring-amber-300",
|
|
19
|
-
danger: "bg-surface-danger text-fg-danger hover:bg-red-100 active:bg-red-200 focus-visible:ring-red-300",
|
|
20
|
-
neutral: "bg-surface-muted text-fg-muted hover:bg-surface-subtle active:bg-surface-subtle focus-visible:ring-neutral-300"
|
|
21
|
-
},
|
|
22
|
-
outline: {
|
|
23
|
-
primary: "border border-primary-300 text-primary-600 bg-transparent hover:bg-primary-50 active:bg-primary-100 focus-visible:ring-primary-300",
|
|
24
|
-
secondary: "border border-secondary-300 text-secondary-900 bg-transparent hover:bg-secondary-50 active:bg-secondary-100 focus-visible:ring-secondary-300",
|
|
25
|
-
danger: "border border-danger text-danger bg-transparent hover:bg-surface-danger active:bg-red-100 focus-visible:ring-red-300",
|
|
26
|
-
neutral: "border border-line text-fg bg-transparent hover:bg-surface-subtle active:bg-surface-muted focus-visible:ring-neutral-300"
|
|
27
|
-
},
|
|
28
|
-
subtle: {
|
|
29
|
-
primary: "bg-transparent text-primary-600 hover:bg-primary-50 active:bg-primary-100",
|
|
30
|
-
secondary: "bg-transparent text-secondary-900 hover:bg-secondary-50 active:bg-secondary-100",
|
|
31
|
-
neutral: "bg-transparent text-fg hover:bg-surface-subtle active:bg-surface-muted",
|
|
32
|
-
danger: "bg-transparent text-danger hover:bg-surface-danger active:bg-red-100"
|
|
33
|
-
},
|
|
34
|
-
ghost: {
|
|
35
|
-
primary: "bg-transparent text-fg-primary hover:text-primary-600 hover:bg-primary-50 active:bg-primary-100",
|
|
36
|
-
secondary: "bg-transparent text-fg hover:bg-surface-muted active:bg-surface-subtle",
|
|
37
|
-
neutral: "bg-transparent text-fg-muted hover:text-fg hover:bg-surface-muted active:bg-surface-subtle"
|
|
38
|
-
}
|
|
39
|
-
}.freeze
|
|
40
|
-
|
|
41
|
-
BUTTON_SIZES = {
|
|
42
|
-
xs: "px-2.5 py-1 text-xs gap-1",
|
|
43
|
-
sm: "px-3.5 py-1.5 text-xs gap-1.5 font-medium",
|
|
44
|
-
md: "px-5 py-2.5 text-sm gap-2 font-medium",
|
|
45
|
-
lg: "px-6 py-3 text-base gap-2.5 font-semibold",
|
|
46
|
-
xl: "px-7 py-3.5 text-lg gap-3 font-semibold"
|
|
47
|
-
}.freeze
|
|
48
|
-
|
|
49
|
-
RADIUS_CLASSES = {
|
|
50
|
-
none: "rounded-none",
|
|
51
|
-
xs: "rounded-xs",
|
|
52
|
-
sm: "rounded-sm",
|
|
53
|
-
md: "rounded-md",
|
|
54
|
-
lg: "rounded-lg",
|
|
55
|
-
xl: "rounded-xl",
|
|
56
|
-
"2xl": "rounded-2xl",
|
|
57
|
-
"3xl": "rounded-3xl",
|
|
58
|
-
"4xl": "rounded-4xl",
|
|
59
|
-
full: "rounded-full"
|
|
60
|
-
}.freeze
|
|
61
|
-
|
|
62
|
-
BADGE_VARIANTS = {
|
|
63
|
-
subtle: {
|
|
64
|
-
primary: "bg-primary-50 text-primary-600",
|
|
65
|
-
secondary: "bg-secondary-100 text-secondary-900",
|
|
66
|
-
success: "bg-surface-success text-success",
|
|
67
|
-
warning: "bg-surface-warning text-warning",
|
|
68
|
-
danger: "bg-surface-danger text-danger",
|
|
69
|
-
info: "bg-surface-info text-info",
|
|
70
|
-
neutral: "bg-surface-muted text-fg-muted"
|
|
71
|
-
},
|
|
72
|
-
filled: {
|
|
73
|
-
primary: "bg-primary-500 text-white",
|
|
74
|
-
secondary: "bg-secondary-500 text-slate-900",
|
|
75
|
-
success: "bg-success text-white",
|
|
76
|
-
warning: "bg-warning text-slate-900",
|
|
77
|
-
danger: "bg-danger text-white",
|
|
78
|
-
info: "bg-info text-white",
|
|
79
|
-
neutral: "bg-neutral-800 text-white"
|
|
80
|
-
},
|
|
81
|
-
outline: {
|
|
82
|
-
primary: "border border-primary-300 text-primary-600 bg-transparent",
|
|
83
|
-
secondary: "border border-secondary-300 text-secondary-800 bg-transparent",
|
|
84
|
-
success: "border border-line-success text-success bg-transparent",
|
|
85
|
-
warning: "border border-line-warning text-warning bg-transparent",
|
|
86
|
-
danger: "border border-line-danger text-danger bg-transparent",
|
|
87
|
-
info: "border border-info text-info bg-transparent",
|
|
88
|
-
neutral: "border border-line text-fg-muted bg-transparent"
|
|
89
|
-
}
|
|
90
|
-
}.freeze
|
|
91
|
-
|
|
92
|
-
BADGE_SIZES = {
|
|
93
|
-
sm: "px-3 py-1.5 text-xs font-semibold",
|
|
94
|
-
md: "px-3.5 py-1.5 text-sm font-semibold"
|
|
95
|
-
}.freeze
|
|
96
|
-
|
|
97
|
-
CARD_VARIANTS = {
|
|
98
|
-
elevated: "bg-surface shadow-card",
|
|
99
|
-
frosted: "bg-surface/70 backdrop-blur-xl shadow-card",
|
|
100
|
-
subtle: "bg-surface-subtle",
|
|
101
|
-
muted: "bg-surface-muted",
|
|
102
|
-
outlined: "bg-surface border border-line shadow-none"
|
|
103
|
-
}.freeze
|
|
104
|
-
|
|
105
|
-
AVATAR_SIZES = {
|
|
106
|
-
xs: "w-6 h-6 min-w-6 min-h-6 text-[10px]",
|
|
107
|
-
sm: "w-8 h-8 min-w-8 min-h-8 text-xs",
|
|
108
|
-
md: "w-10 h-10 min-w-10 min-h-10 text-sm",
|
|
109
|
-
lg: "w-12 h-12 min-w-12 min-h-12 text-base",
|
|
110
|
-
xl: "w-16 h-16 min-w-16 min-h-16 text-xl"
|
|
111
|
-
}.freeze
|
|
112
|
-
|
|
113
|
-
AVATAR_COLORS = {
|
|
114
|
-
secondary: "bg-secondary-100 text-secondary-900",
|
|
115
|
-
primary: "bg-primary-100 text-primary-700",
|
|
116
|
-
neutral: "bg-surface-muted text-fg"
|
|
117
|
-
}.freeze
|
|
5
|
+
# =========================================================================
|
|
6
|
+
# Helpers for ODT-Lightweight-UI
|
|
7
|
+
# Standalone CSS First with seamless Tailwind CSS v4 support.
|
|
8
|
+
# =========================================================================
|
|
118
9
|
|
|
10
|
+
# Renders an ODT Button component.
|
|
11
|
+
#
|
|
119
12
|
# @param text [String, nil]
|
|
120
|
-
# @param variant [:filled, :light, :outline, :
|
|
121
|
-
# @param color [:primary, :secondary, :success, :warning, :danger, :neutral]
|
|
13
|
+
# @param variant [:filled, :frosted, :capsule, :ghost, :light, :outline, :outlined, :subtle]
|
|
14
|
+
# @param color [:primary, :secondary, :surface, :muted, :inverse, :success, :warning, :danger, :info, :neutral]
|
|
122
15
|
# @param size [:xs, :sm, :md, :lg, :xl]
|
|
123
|
-
# @param radius [:none, :xs, :sm, :md, :lg, :xl, :"2xl", :"3xl", :"4xl", :full]
|
|
16
|
+
# @param radius [:none, :xs, :sm, :md, :lg, :xl, :"2xl", :"3xl", :"4xl", :full, nil]
|
|
17
|
+
# @param shadow [:none, :card, :dropdown, :modal, :popover, nil]
|
|
18
|
+
# @param icon_only [Boolean]
|
|
19
|
+
# @param full_width [Boolean]
|
|
20
|
+
# @param loading [Boolean]
|
|
21
|
+
# @param disabled [Boolean]
|
|
124
22
|
# @param href [String, nil]
|
|
125
23
|
# @param icon [String, nil]
|
|
126
24
|
# @param right_icon [String, nil]
|
|
127
|
-
# @param full_width [Boolean]
|
|
128
|
-
# @param disabled [Boolean]
|
|
129
25
|
# @param type [String]
|
|
130
26
|
# @return [ActiveSupport::SafeBuffer]
|
|
131
|
-
def odt_button(text = nil, variant: :filled, color: :primary, size: :md, radius:
|
|
132
|
-
variant_key = variant
|
|
133
|
-
color_key = color.
|
|
134
|
-
size_key = size.
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
classes
|
|
146
|
-
|
|
147
|
-
|
|
27
|
+
def odt_button(text = nil, variant: :filled, color: :primary, size: :md, radius: nil, shadow: nil, icon_only: false, full_width: false, loading: false, disabled: false, href: nil, icon: nil, right_icon: nil, type: "button", **html_options, &block)
|
|
28
|
+
variant_key = normalize_button_variant(variant)
|
|
29
|
+
color_key = color.to_s.tr("_", "-")
|
|
30
|
+
size_key = size.to_s.tr("_", "-")
|
|
31
|
+
|
|
32
|
+
classes = [
|
|
33
|
+
"odt-btn",
|
|
34
|
+
"odt-btn--#{variant_key}-#{color_key}",
|
|
35
|
+
"odt-btn--#{size_key}"
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
classes << "odt-btn--capsule" if variant.to_sym == :capsule
|
|
39
|
+
classes << "odt-radius-#{radius.to_s.tr('_', '-')}" if radius.present?
|
|
40
|
+
classes << "odt-shadow-#{shadow.to_s.tr('_', '-')}" if shadow.present?
|
|
41
|
+
classes << "odt-btn--full-width" if full_width
|
|
42
|
+
classes << "odt-btn--icon-only" if icon_only
|
|
43
|
+
classes << "odt-btn--loading" if loading
|
|
44
|
+
classes << "odt-btn--disabled" if disabled
|
|
45
|
+
classes << html_options.delete(:class)
|
|
46
|
+
|
|
47
|
+
opts = html_options.merge(class: classes.compact.reject(&:blank?).join(" "))
|
|
48
|
+
opts[:disabled] = true if disabled || loading
|
|
49
|
+
opts[:aria] = (opts[:aria] || {}).merge(busy: true) if loading
|
|
148
50
|
|
|
149
51
|
parts = []
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
52
|
+
if loading
|
|
53
|
+
parts << tag.span(class: "odt-spinner", aria: { hidden: "true" })
|
|
54
|
+
elsif icon.present?
|
|
55
|
+
parts << render_icon_element(icon)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
button_text = block_given? ? capture(&block) : text
|
|
59
|
+
if button_text.present?
|
|
60
|
+
parts << (icon_only ? button_text : tag.span(button_text))
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
if right_icon.present? && !loading
|
|
64
|
+
parts << render_icon_element(right_icon)
|
|
65
|
+
end
|
|
66
|
+
|
|
153
67
|
inner_content = safe_join(parts.compact, " ")
|
|
154
68
|
|
|
155
69
|
if href.present?
|
|
@@ -160,179 +74,652 @@ module OdtUi
|
|
|
160
74
|
end
|
|
161
75
|
end
|
|
162
76
|
|
|
77
|
+
# Renders an ODT Badge component.
|
|
78
|
+
#
|
|
163
79
|
# @param text [String, nil]
|
|
164
|
-
# @param variant [:subtle, :filled, :outline]
|
|
80
|
+
# @param variant [:subtle, :filled, :outlined, :outline]
|
|
165
81
|
# @param color [:primary, :secondary, :success, :warning, :danger, :info, :neutral]
|
|
166
82
|
# @param size [:sm, :md]
|
|
167
|
-
# @param radius [:none, :xs, :sm, :md, :lg, :xl, :"2xl", :"3xl", :"4xl", :full]
|
|
83
|
+
# @param radius [:none, :xs, :sm, :md, :lg, :xl, :"2xl", :"3xl", :"4xl", :full, nil]
|
|
168
84
|
# @param dot [Boolean]
|
|
169
85
|
# @param icon [String, nil]
|
|
170
86
|
# @return [ActiveSupport::SafeBuffer]
|
|
171
|
-
def odt_badge(text = nil, variant: :subtle, color: :
|
|
172
|
-
variant_key = variant.to_sym
|
|
173
|
-
color_key = color.
|
|
174
|
-
size_key = size.
|
|
175
|
-
radius_key = radius.to_s.to_sym
|
|
87
|
+
def odt_badge(text = nil, variant: :subtle, color: :primary, size: :md, radius: nil, dot: false, icon: nil, **html_options, &block)
|
|
88
|
+
variant_key = variant.to_sym == :outline ? "outlined" : variant.to_s.tr("_", "-")
|
|
89
|
+
color_key = color.to_s.tr("_", "-")
|
|
90
|
+
size_key = size.to_s.tr("_", "-")
|
|
176
91
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
92
|
+
classes = [
|
|
93
|
+
"odt-badge",
|
|
94
|
+
"odt-badge--#{variant_key}-#{color_key}",
|
|
95
|
+
"odt-badge--#{size_key}"
|
|
96
|
+
]
|
|
182
97
|
|
|
183
|
-
classes
|
|
184
|
-
|
|
98
|
+
classes << "odt-radius-#{radius.to_s.tr('_', '-')}" if radius.present?
|
|
99
|
+
classes << html_options.delete(:class)
|
|
100
|
+
|
|
101
|
+
opts = html_options.merge(class: classes.compact.reject(&:blank?).join(" "))
|
|
185
102
|
|
|
186
103
|
parts = []
|
|
187
|
-
parts << tag.span(class: "
|
|
188
|
-
parts << (icon
|
|
189
|
-
parts << (block_given? ? capture(&block) : text)
|
|
190
|
-
inner_content = safe_join(parts.compact, " ")
|
|
104
|
+
parts << tag.span(class: "odt-badge__dot", aria: { hidden: "true" }) if dot
|
|
105
|
+
parts << render_icon_element(icon) if icon.present?
|
|
191
106
|
|
|
107
|
+
badge_text = block_given? ? capture(&block) : text
|
|
108
|
+
parts << tag.span(badge_text) if badge_text.present?
|
|
109
|
+
|
|
110
|
+
inner_content = safe_join(parts.compact, " ")
|
|
192
111
|
tag.span(inner_content, **opts)
|
|
193
112
|
end
|
|
194
113
|
|
|
114
|
+
# Renders an ODT Card container.
|
|
115
|
+
#
|
|
195
116
|
# @param title [String, nil]
|
|
196
117
|
# @param subtitle [String, nil]
|
|
197
|
-
# @param variant [:elevated, :
|
|
198
|
-
# @param
|
|
199
|
-
# @param border [
|
|
200
|
-
# @param
|
|
118
|
+
# @param variant [:elevated, :outlined, :subtle, :muted, :ghost, :frosted]
|
|
119
|
+
# @param background [Symbol, String, nil]
|
|
120
|
+
# @param border [Symbol, String, Boolean, nil]
|
|
121
|
+
# @param shadow [Symbol, String, nil]
|
|
122
|
+
# @param radius [Symbol, String, nil]
|
|
123
|
+
# @param padding [Symbol, String, nil]
|
|
124
|
+
# @param hoverable [Boolean]
|
|
125
|
+
# @param hover [Boolean] Alias for hoverable
|
|
126
|
+
# @param as [:div, :article, :section, :aside, :main]
|
|
201
127
|
# @return [ActiveSupport::SafeBuffer]
|
|
202
|
-
def odt_card(title: nil, subtitle: nil, variant: :
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
"
|
|
210
|
-
|
|
211
|
-
"border-0"
|
|
128
|
+
def odt_card(title: nil, subtitle: nil, variant: :elevated, background: nil, border: nil, shadow: nil, radius: nil, padding: nil, hoverable: false, hover: false, as: :div, **html_options, &block)
|
|
129
|
+
tag_name = as.to_sym
|
|
130
|
+
variant_key = variant.to_s.tr("_", "-")
|
|
131
|
+
|
|
132
|
+
classes = ["odt-card", "odt-card--#{variant_key}"]
|
|
133
|
+
|
|
134
|
+
if background.present?
|
|
135
|
+
bg_key = background.to_s.tr("_", "-")
|
|
136
|
+
classes << "odt-card--bg-#{bg_key}"
|
|
212
137
|
end
|
|
213
138
|
|
|
214
|
-
|
|
215
|
-
|
|
139
|
+
if border.present?
|
|
140
|
+
if border == true
|
|
141
|
+
classes << "odt-card--border-base"
|
|
142
|
+
else
|
|
143
|
+
classes << "odt-card--border-#{border.to_s.tr('_', '-')}"
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
classes << "odt-shadow-#{shadow.to_s.tr('_', '-')}" if shadow.present?
|
|
148
|
+
classes << "odt-radius-#{radius.to_s.tr('_', '-')}" if radius.present?
|
|
149
|
+
classes << "odt-card--padding-#{padding.to_s.tr('_', '-')}" if padding.present?
|
|
150
|
+
classes << "odt-card--hoverable" if hoverable || hover
|
|
151
|
+
classes << html_options.delete(:class)
|
|
216
152
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
153
|
+
opts = html_options.merge(class: classes.compact.reject(&:blank?).join(" "))
|
|
154
|
+
|
|
155
|
+
header = if title.present? || subtitle.present?
|
|
156
|
+
tag.div(class: "odt-card__header") do
|
|
157
|
+
concat tag.h3(title, class: "odt-card__title") if title.present?
|
|
158
|
+
concat tag.p(subtitle, class: "odt-card__subtitle") if subtitle.present?
|
|
221
159
|
end
|
|
222
160
|
end
|
|
223
|
-
content = block_given? ? capture(&block) : nil
|
|
224
|
-
inner_content = safe_join([header, content].compact)
|
|
225
161
|
|
|
226
|
-
|
|
162
|
+
body_content = block_given? ? capture(&block) : nil
|
|
163
|
+
inner_content = safe_join([header, body_content].compact)
|
|
164
|
+
|
|
165
|
+
tag.public_send(tag_name, inner_content, **opts)
|
|
227
166
|
end
|
|
228
167
|
|
|
168
|
+
# Renders an ODT Alert banner.
|
|
169
|
+
#
|
|
229
170
|
# @param message [String, nil]
|
|
230
171
|
# @param variant [:success, :warning, :danger, :info]
|
|
231
172
|
# @param icon [String, nil]
|
|
232
173
|
# @return [ActiveSupport::SafeBuffer]
|
|
233
174
|
def odt_alert(message = nil, variant: :success, icon: nil, **html_options, &block)
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
175
|
+
variant_key = variant.to_sym
|
|
176
|
+
default_icons = {
|
|
177
|
+
success: "fa-solid fa-circle-check",
|
|
178
|
+
warning: "fa-solid fa-triangle-exclamation",
|
|
179
|
+
danger: "fa-solid fa-circle-xmark",
|
|
180
|
+
info: "fa-solid fa-circle-info"
|
|
239
181
|
}
|
|
240
182
|
|
|
241
|
-
|
|
242
|
-
icon_name = icon || config[:icon]
|
|
183
|
+
icon_name = icon || default_icons[variant_key] || default_icons[:info]
|
|
243
184
|
|
|
244
|
-
classes = [
|
|
245
|
-
|
|
185
|
+
classes = [
|
|
186
|
+
"odt-alert",
|
|
187
|
+
"odt-alert--#{variant_key}",
|
|
188
|
+
html_options.delete(:class)
|
|
189
|
+
]
|
|
190
|
+
|
|
191
|
+
opts = html_options.merge(class: classes.compact.reject(&:blank?).join(" "), role: "alert")
|
|
246
192
|
|
|
247
193
|
parts = []
|
|
248
|
-
parts << tag.
|
|
249
|
-
parts << (block_given? ? capture(&block) : tag.p(message, class: "font-medium"))
|
|
250
|
-
inner_content = safe_join(parts.compact)
|
|
194
|
+
parts << tag.span(render_icon_element(icon_name), class: "odt-alert__icon") if icon_name.present?
|
|
251
195
|
|
|
196
|
+
content = block_given? ? capture(&block) : message
|
|
197
|
+
parts << tag.div(content, class: "odt-alert__content") if content.present?
|
|
198
|
+
|
|
199
|
+
inner_content = safe_join(parts.compact)
|
|
252
200
|
tag.div(inner_content, **opts)
|
|
253
201
|
end
|
|
254
202
|
|
|
203
|
+
# Renders an ODT Heading element.
|
|
204
|
+
#
|
|
255
205
|
# @param text [String, nil]
|
|
256
|
-
# @param as [:h1, :h2, :h3, :h4, :h5]
|
|
206
|
+
# @param as [:h1, :h2, :h3, :h4, :h5, :h6]
|
|
207
|
+
# @param size [:5xl, :4xl, :3xl, :2xl, :xl, :lg, nil]
|
|
208
|
+
# @param weight [:light, :normal, :medium, :semibold, :bold, :extrabold, nil]
|
|
209
|
+
# @param color [:strong, :default, :muted, :subtle, :disabled, :primary, :secondary, :success, :warning, :danger, :info]
|
|
210
|
+
# @param truncate [Boolean]
|
|
257
211
|
# @return [ActiveSupport::SafeBuffer]
|
|
258
|
-
def odt_heading(text = nil, as: :h2, **html_options, &block)
|
|
212
|
+
def odt_heading(text = nil, as: :h2, size: nil, weight: nil, color: :strong, truncate: false, **html_options, &block)
|
|
259
213
|
tag_name = as.to_sym
|
|
260
|
-
|
|
261
|
-
h1: "
|
|
262
|
-
h2: "
|
|
263
|
-
h3: "
|
|
264
|
-
h4: "
|
|
265
|
-
h5: "
|
|
214
|
+
default_sizes = {
|
|
215
|
+
h1: "4xl",
|
|
216
|
+
h2: "3xl",
|
|
217
|
+
h3: "2xl",
|
|
218
|
+
h4: "xl",
|
|
219
|
+
h5: "lg",
|
|
220
|
+
h6: "lg"
|
|
266
221
|
}
|
|
267
222
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
223
|
+
resolved_size = (size || default_sizes[tag_name] || "2xl").to_s.tr("_", "-")
|
|
224
|
+
resolved_weight = (weight || "bold").to_s.tr("_", "-")
|
|
225
|
+
resolved_color = (color || "strong").to_s.tr("_", "-")
|
|
226
|
+
|
|
227
|
+
classes = [
|
|
228
|
+
"odt-heading",
|
|
229
|
+
"odt-heading--#{resolved_size}",
|
|
230
|
+
"odt-weight-#{resolved_weight}",
|
|
231
|
+
"odt-color-#{resolved_color}"
|
|
232
|
+
]
|
|
233
|
+
|
|
234
|
+
classes << "odt-truncate" if truncate
|
|
235
|
+
classes << html_options.delete(:class)
|
|
236
|
+
|
|
237
|
+
opts = html_options.merge(class: classes.compact.reject(&:blank?).join(" "))
|
|
271
238
|
inner_content = block_given? ? capture(&block) : text
|
|
272
239
|
|
|
273
240
|
tag.public_send(tag_name, inner_content, **opts)
|
|
274
241
|
end
|
|
275
242
|
|
|
243
|
+
# Renders styled typography body text.
|
|
244
|
+
#
|
|
276
245
|
# @param text [String, nil]
|
|
277
|
-
# @param
|
|
278
|
-
# @param size [:xs, :sm, :base, :lg]
|
|
246
|
+
# @param as [:p, :span, :div, :label, :small]
|
|
247
|
+
# @param size [:xs, :sm, :base, :lg, :xl, :"2xl"]
|
|
248
|
+
# @param weight [:light, :normal, :medium, :semibold, :bold, :extrabold, nil]
|
|
249
|
+
# @param color [:default, :strong, :muted, :subtle, :disabled, :primary, :secondary, :success, :warning, :danger, :info]
|
|
250
|
+
# @param truncate [Boolean]
|
|
279
251
|
# @return [ActiveSupport::SafeBuffer]
|
|
280
|
-
def odt_text(text = nil,
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
muted: "text-fg-muted",
|
|
285
|
-
subtle: "text-fg-subtle",
|
|
286
|
-
primary: "text-primary-600 font-medium"
|
|
287
|
-
}
|
|
252
|
+
def odt_text(text = nil, as: :p, size: :base, weight: nil, color: :default, truncate: false, **html_options, &block)
|
|
253
|
+
tag_name = as.to_sym
|
|
254
|
+
resolved_size = size.to_s.tr("_", "-")
|
|
255
|
+
resolved_color = color.to_s.tr("_", "-")
|
|
288
256
|
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
}
|
|
257
|
+
classes = [
|
|
258
|
+
"odt-text",
|
|
259
|
+
"odt-text--#{resolved_size}",
|
|
260
|
+
"odt-color-#{resolved_color}"
|
|
261
|
+
]
|
|
295
262
|
|
|
296
|
-
|
|
297
|
-
|
|
263
|
+
classes << "odt-weight-#{weight.to_s.tr('_', '-')}" if weight.present?
|
|
264
|
+
classes << "odt-truncate" if truncate
|
|
265
|
+
classes << html_options.delete(:class)
|
|
298
266
|
|
|
299
|
-
|
|
300
|
-
opts = html_options.merge(class: classes)
|
|
267
|
+
opts = html_options.merge(class: classes.compact.reject(&:blank?).join(" "))
|
|
301
268
|
inner_content = block_given? ? capture(&block) : text
|
|
302
269
|
|
|
303
|
-
tag.
|
|
270
|
+
tag.public_send(tag_name, inner_content, **opts)
|
|
304
271
|
end
|
|
305
272
|
|
|
273
|
+
# Renders an ODT Avatar element.
|
|
274
|
+
#
|
|
306
275
|
# @param name [String, nil]
|
|
307
276
|
# @param src [String, nil]
|
|
308
|
-
# @param size [:xs, :sm, :md, :lg, :xl]
|
|
277
|
+
# @param size [:xs, :sm, :md, :lg, :xl, :"2xl"]
|
|
309
278
|
# @param color [:secondary, :primary, :neutral]
|
|
310
279
|
# @param icon [String]
|
|
280
|
+
# @param fallback [String, nil]
|
|
311
281
|
# @return [ActiveSupport::SafeBuffer]
|
|
312
|
-
def odt_avatar(name: nil, src: nil, size: :
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
282
|
+
def odt_avatar(name: nil, src: nil, size: :md, color: :secondary, icon: "fa-solid fa-user", fallback: nil, **html_options)
|
|
283
|
+
size_key = size.to_s.tr("_", "-")
|
|
284
|
+
color_key = color.to_s.tr("_", "-")
|
|
285
|
+
|
|
286
|
+
classes = [
|
|
287
|
+
"odt-avatar",
|
|
288
|
+
"odt-avatar--#{size_key}",
|
|
289
|
+
"odt-avatar--#{color_key}",
|
|
290
|
+
html_options.delete(:class)
|
|
291
|
+
]
|
|
292
|
+
|
|
293
|
+
opts = html_options.merge(class: classes.compact.reject(&:blank?).join(" "))
|
|
294
|
+
|
|
295
|
+
initials = fallback.presence || compute_avatar_initials(name)
|
|
326
296
|
|
|
327
297
|
inner_content = if src.present?
|
|
328
|
-
image_tag(src, alt: name || "Avatar", class: "
|
|
298
|
+
image_tag(src, alt: name || "Avatar", class: "odt-avatar__image")
|
|
329
299
|
elsif initials.present?
|
|
330
|
-
tag.span(initials, class: "
|
|
300
|
+
tag.span(initials, class: "odt-avatar__fallback")
|
|
331
301
|
else
|
|
332
|
-
tag.
|
|
302
|
+
tag.span(render_icon_element(icon), class: "odt-avatar__fallback")
|
|
333
303
|
end
|
|
334
304
|
|
|
335
305
|
tag.div(inner_content, **opts)
|
|
336
306
|
end
|
|
307
|
+
|
|
308
|
+
# Renders an ODT text/number/email/password input.
|
|
309
|
+
#
|
|
310
|
+
# @param label [String, nil]
|
|
311
|
+
# @param variant [:outlined, :filled, :frosted]
|
|
312
|
+
# @param size [:sm, :md, :lg]
|
|
313
|
+
# @param radius [:none, :xs, :sm, :md, :lg, :xl, :"2xl", :"3xl", :"4xl", :full, nil]
|
|
314
|
+
# @param type [String]
|
|
315
|
+
# @param placeholder [String, nil]
|
|
316
|
+
# @param error [String, nil]
|
|
317
|
+
# @param helper [String, nil]
|
|
318
|
+
# @param helper_text [String, nil] Alias for helper
|
|
319
|
+
# @param success [String, nil]
|
|
320
|
+
# @param left_icon [String, nil]
|
|
321
|
+
# @param right_icon [String, nil]
|
|
322
|
+
# @param prefix [String, nil]
|
|
323
|
+
# @param suffix [String, nil]
|
|
324
|
+
# @param addon_before [String, nil]
|
|
325
|
+
# @param addon_after [String, nil]
|
|
326
|
+
# @param disabled [Boolean]
|
|
327
|
+
# @param full_width [Boolean]
|
|
328
|
+
# @return [ActiveSupport::SafeBuffer]
|
|
329
|
+
def odt_input(label: nil, variant: :outlined, size: :md, radius: nil, type: "text", placeholder: nil, error: nil, helper: nil, helper_text: nil, success: nil, left_icon: nil, right_icon: nil, prefix: nil, suffix: nil, addon_before: nil, addon_after: nil, disabled: false, full_width: true, **html_options)
|
|
330
|
+
variant_key = variant.to_s.tr("_", "-")
|
|
331
|
+
size_key = size.to_s.tr("_", "-")
|
|
332
|
+
resolved_helper = helper.presence || helper_text.presence
|
|
333
|
+
has_error = error.present?
|
|
334
|
+
has_success = success.present? && !has_error
|
|
335
|
+
|
|
336
|
+
input_classes = [
|
|
337
|
+
"odt-input",
|
|
338
|
+
"odt-input--#{variant_key}",
|
|
339
|
+
"odt-input--#{size_key}"
|
|
340
|
+
]
|
|
341
|
+
|
|
342
|
+
input_classes << "odt-radius-#{radius.to_s.tr('_', '-')}" if radius.present?
|
|
343
|
+
input_classes << "odt-input--error" if has_error
|
|
344
|
+
input_classes << "odt-input--success" if has_success
|
|
345
|
+
input_classes << "odt-input--has-left-icon" if left_icon.present? || prefix.present?
|
|
346
|
+
input_classes << "odt-input--has-right-icon" if right_icon.present? || suffix.present?
|
|
347
|
+
input_classes << "odt-input--has-addon-before" if addon_before.present?
|
|
348
|
+
input_classes << "odt-input--has-addon-after" if addon_after.present?
|
|
349
|
+
input_classes << html_options.delete(:class)
|
|
350
|
+
|
|
351
|
+
input_opts = html_options.merge(
|
|
352
|
+
type: type,
|
|
353
|
+
class: input_classes.compact.reject(&:blank?).join(" "),
|
|
354
|
+
disabled: disabled
|
|
355
|
+
)
|
|
356
|
+
input_opts[:placeholder] = placeholder if placeholder.present?
|
|
357
|
+
|
|
358
|
+
container_classes = ["odt-input-container"]
|
|
359
|
+
container_classes << "odt-input-container--full-width" if full_width
|
|
360
|
+
|
|
361
|
+
tag.div(class: container_classes.join(" ")) do
|
|
362
|
+
parts = []
|
|
363
|
+
parts << tag.label(label, class: "odt-label") if label.present?
|
|
364
|
+
|
|
365
|
+
input_field = tag.div(class: "odt-input-wrapper") do
|
|
366
|
+
wrap_parts = []
|
|
367
|
+
if left_icon.present?
|
|
368
|
+
wrap_parts << tag.span(render_icon_element(left_icon), class: "odt-input__left-icon")
|
|
369
|
+
elsif prefix.present?
|
|
370
|
+
wrap_parts << tag.span(prefix, class: "odt-input__left-icon")
|
|
371
|
+
end
|
|
372
|
+
|
|
373
|
+
wrap_parts << tag.input(**input_opts)
|
|
374
|
+
|
|
375
|
+
if right_icon.present?
|
|
376
|
+
wrap_parts << tag.span(render_icon_element(right_icon), class: "odt-input__right-icon")
|
|
377
|
+
elsif suffix.present?
|
|
378
|
+
wrap_parts << tag.span(suffix, class: "odt-input__right-icon")
|
|
379
|
+
end
|
|
380
|
+
|
|
381
|
+
safe_join(wrap_parts)
|
|
382
|
+
end
|
|
383
|
+
|
|
384
|
+
if addon_before.present? || addon_after.present?
|
|
385
|
+
parts << tag.div(class: "odt-input__addon-group") do
|
|
386
|
+
addon_parts = []
|
|
387
|
+
addon_parts << tag.span(addon_before, class: "odt-input__addon-before") if addon_before.present?
|
|
388
|
+
addon_parts << input_field
|
|
389
|
+
addon_parts << tag.span(addon_after, class: "odt-input__addon-after") if addon_after.present?
|
|
390
|
+
safe_join(addon_parts)
|
|
391
|
+
end
|
|
392
|
+
else
|
|
393
|
+
parts << input_field
|
|
394
|
+
end
|
|
395
|
+
|
|
396
|
+
if has_error
|
|
397
|
+
parts << tag.p(error, class: "odt-error-text", role: "alert")
|
|
398
|
+
elsif has_success
|
|
399
|
+
parts << tag.p(success, class: "odt-success-text")
|
|
400
|
+
elsif resolved_helper.present?
|
|
401
|
+
parts << tag.p(resolved_helper, class: "odt-helper-text")
|
|
402
|
+
end
|
|
403
|
+
|
|
404
|
+
safe_join(parts)
|
|
405
|
+
end
|
|
406
|
+
end
|
|
407
|
+
|
|
408
|
+
# Renders an ODT textarea field.
|
|
409
|
+
#
|
|
410
|
+
# @param label [String, nil]
|
|
411
|
+
# @param variant [:outlined, :filled, :frosted]
|
|
412
|
+
# @param size [:sm, :md, :lg]
|
|
413
|
+
# @param radius [:none, :xs, :sm, :md, :lg, :xl, :"2xl", :"3xl", :"4xl", :full, nil]
|
|
414
|
+
# @param rows [Integer]
|
|
415
|
+
# @param resize [:none, :vertical, :horizontal, :both]
|
|
416
|
+
# @param error [String, nil]
|
|
417
|
+
# @param helper [String, nil]
|
|
418
|
+
# @param helper_text [String, nil]
|
|
419
|
+
# @param success [String, nil]
|
|
420
|
+
# @param disabled [Boolean]
|
|
421
|
+
# @param full_width [Boolean]
|
|
422
|
+
# @return [ActiveSupport::SafeBuffer]
|
|
423
|
+
def odt_textarea(label: nil, variant: :outlined, size: :md, radius: nil, rows: 4, resize: :vertical, error: nil, helper: nil, helper_text: nil, success: nil, disabled: false, full_width: true, **html_options, &block)
|
|
424
|
+
variant_key = variant.to_s.tr("_", "-")
|
|
425
|
+
size_key = size.to_s.tr("_", "-")
|
|
426
|
+
resize_key = resize.to_s.tr("_", "-")
|
|
427
|
+
resolved_helper = helper.presence || helper_text.presence
|
|
428
|
+
has_error = error.present?
|
|
429
|
+
has_success = success.present? && !has_error
|
|
430
|
+
|
|
431
|
+
classes = [
|
|
432
|
+
"odt-textarea",
|
|
433
|
+
"odt-textarea--#{variant_key}",
|
|
434
|
+
"odt-textarea--#{size_key}",
|
|
435
|
+
"odt-textarea--resize-#{resize_key}"
|
|
436
|
+
]
|
|
437
|
+
|
|
438
|
+
classes << "odt-radius-#{radius.to_s.tr('_', '-')}" if radius.present?
|
|
439
|
+
classes << "odt-textarea--error" if has_error
|
|
440
|
+
classes << "odt-textarea--success" if has_success
|
|
441
|
+
classes << html_options.delete(:class)
|
|
442
|
+
|
|
443
|
+
opts = html_options.merge(
|
|
444
|
+
class: classes.compact.reject(&:blank?).join(" "),
|
|
445
|
+
rows: rows,
|
|
446
|
+
disabled: disabled
|
|
447
|
+
)
|
|
448
|
+
|
|
449
|
+
content = block_given? ? capture(&block) : html_options.delete(:value)
|
|
450
|
+
|
|
451
|
+
container_classes = ["odt-input-container"]
|
|
452
|
+
container_classes << "odt-input-container--full-width" if full_width
|
|
453
|
+
|
|
454
|
+
tag.div(class: container_classes.join(" ")) do
|
|
455
|
+
parts = []
|
|
456
|
+
parts << tag.label(label, class: "odt-label") if label.present?
|
|
457
|
+
parts << tag.textarea(content, **opts)
|
|
458
|
+
|
|
459
|
+
if has_error
|
|
460
|
+
parts << tag.p(error, class: "odt-error-text", role: "alert")
|
|
461
|
+
elsif has_success
|
|
462
|
+
parts << tag.p(success, class: "odt-success-text")
|
|
463
|
+
elsif resolved_helper.present?
|
|
464
|
+
parts << tag.p(resolved_helper, class: "odt-helper-text")
|
|
465
|
+
end
|
|
466
|
+
|
|
467
|
+
safe_join(parts)
|
|
468
|
+
end
|
|
469
|
+
end
|
|
470
|
+
|
|
471
|
+
# Renders an ODT select dropdown.
|
|
472
|
+
#
|
|
473
|
+
# @param label [String, nil]
|
|
474
|
+
# @param options [Array] Options compatible with Rails options_for_select
|
|
475
|
+
# @param variant [:outlined, :filled, :frosted]
|
|
476
|
+
# @param size [:sm, :md, :lg]
|
|
477
|
+
# @param radius [:none, :xs, :sm, :md, :lg, :xl, :"2xl", :"3xl", :"4xl", :full, nil]
|
|
478
|
+
# @param prompt [String, nil]
|
|
479
|
+
# @param error [String, nil]
|
|
480
|
+
# @param helper [String, nil]
|
|
481
|
+
# @param helper_text [String, nil]
|
|
482
|
+
# @param success [String, nil]
|
|
483
|
+
# @param disabled [Boolean]
|
|
484
|
+
# @param full_width [Boolean]
|
|
485
|
+
# @return [ActiveSupport::SafeBuffer]
|
|
486
|
+
def odt_select(label: nil, options: [], variant: :outlined, size: :md, radius: nil, prompt: nil, error: nil, helper: nil, helper_text: nil, success: nil, disabled: false, full_width: true, **html_options)
|
|
487
|
+
variant_key = variant.to_s.tr("_", "-")
|
|
488
|
+
size_key = size.to_s.tr("_", "-")
|
|
489
|
+
resolved_helper = helper.presence || helper_text.presence
|
|
490
|
+
has_error = error.present?
|
|
491
|
+
has_success = success.present? && !has_error
|
|
492
|
+
|
|
493
|
+
classes = [
|
|
494
|
+
"odt-select",
|
|
495
|
+
"odt-select--#{variant_key}",
|
|
496
|
+
"odt-select--#{size_key}"
|
|
497
|
+
]
|
|
498
|
+
|
|
499
|
+
classes << "odt-radius-#{radius.to_s.tr('_', '-')}" if radius.present?
|
|
500
|
+
classes << "odt-select--error" if has_error
|
|
501
|
+
classes << "odt-select--success" if has_success
|
|
502
|
+
classes << html_options.delete(:class)
|
|
503
|
+
|
|
504
|
+
opts = html_options.merge(
|
|
505
|
+
class: classes.compact.reject(&:blank?).join(" "),
|
|
506
|
+
disabled: disabled
|
|
507
|
+
)
|
|
508
|
+
|
|
509
|
+
option_tags = if prompt.present?
|
|
510
|
+
tag.option(prompt, value: "") + (options.is_a?(String) ? options.html_safe : options_for_select(options))
|
|
511
|
+
else
|
|
512
|
+
options.is_a?(String) ? options.html_safe : options_for_select(options)
|
|
513
|
+
end
|
|
514
|
+
|
|
515
|
+
container_classes = ["odt-input-container"]
|
|
516
|
+
container_classes << "odt-input-container--full-width" if full_width
|
|
517
|
+
|
|
518
|
+
tag.div(class: container_classes.join(" ")) do
|
|
519
|
+
parts = []
|
|
520
|
+
parts << tag.label(label, class: "odt-label") if label.present?
|
|
521
|
+
parts << tag.select(option_tags, **opts)
|
|
522
|
+
|
|
523
|
+
if has_error
|
|
524
|
+
parts << tag.p(error, class: "odt-error-text", role: "alert")
|
|
525
|
+
elsif has_success
|
|
526
|
+
parts << tag.p(success, class: "odt-success-text")
|
|
527
|
+
elsif resolved_helper.present?
|
|
528
|
+
parts << tag.p(resolved_helper, class: "odt-helper-text")
|
|
529
|
+
end
|
|
530
|
+
|
|
531
|
+
safe_join(parts)
|
|
532
|
+
end
|
|
533
|
+
end
|
|
534
|
+
|
|
535
|
+
# Renders an ODT styled Checkbox.
|
|
536
|
+
#
|
|
537
|
+
# @param label [String, nil]
|
|
538
|
+
# @param description [String, nil]
|
|
539
|
+
# @param checked [Boolean]
|
|
540
|
+
# @param color [:primary, :secondary, :success, :warning, :danger, :dark, :neutral]
|
|
541
|
+
# @param size [:sm, :md, :lg]
|
|
542
|
+
# @param radius [:none, :xs, :sm, :md, :lg, :full, nil]
|
|
543
|
+
# @param disabled [Boolean]
|
|
544
|
+
# @param error [String, nil]
|
|
545
|
+
# @return [ActiveSupport::SafeBuffer]
|
|
546
|
+
def odt_checkbox(label: nil, description: nil, checked: false, color: :primary, size: :md, radius: nil, disabled: false, error: nil, **html_options)
|
|
547
|
+
color_key = color.to_sym == :dark ? "neutral" : color.to_s.tr("_", "-")
|
|
548
|
+
size_key = size.to_s.tr("_", "-")
|
|
549
|
+
has_error = error.present?
|
|
550
|
+
|
|
551
|
+
classes = [
|
|
552
|
+
"odt-checkbox",
|
|
553
|
+
"odt-checkbox--#{color_key}",
|
|
554
|
+
"odt-checkbox--#{size_key}"
|
|
555
|
+
]
|
|
556
|
+
|
|
557
|
+
classes << "odt-radius-#{radius.to_s.tr('_', '-')}" if radius.present?
|
|
558
|
+
classes << "odt-checkbox--error" if has_error
|
|
559
|
+
classes << html_options.delete(:class)
|
|
560
|
+
|
|
561
|
+
opts = html_options.merge(
|
|
562
|
+
type: "checkbox",
|
|
563
|
+
class: classes.compact.reject(&:blank?).join(" "),
|
|
564
|
+
disabled: disabled
|
|
565
|
+
)
|
|
566
|
+
opts[:checked] = "checked" if checked
|
|
567
|
+
|
|
568
|
+
tag.div(class: "odt-checkbox-wrapper") do
|
|
569
|
+
parts = []
|
|
570
|
+
parts << tag.label(class: "odt-checkbox-container") do
|
|
571
|
+
label_parts = [tag.input(**opts)]
|
|
572
|
+
|
|
573
|
+
if label.present? || description.present?
|
|
574
|
+
label_parts << tag.div(class: "odt-checkbox__label-content") do
|
|
575
|
+
d_parts = []
|
|
576
|
+
d_parts << tag.span(label, class: "odt-checkbox__label") if label.present?
|
|
577
|
+
d_parts << tag.span(description, class: "odt-checkbox__description") if description.present?
|
|
578
|
+
safe_join(d_parts)
|
|
579
|
+
end
|
|
580
|
+
end
|
|
581
|
+
|
|
582
|
+
safe_join(label_parts)
|
|
583
|
+
end
|
|
584
|
+
|
|
585
|
+
parts << tag.p(error, class: "odt-error-text", role: "alert") if has_error
|
|
586
|
+
safe_join(parts)
|
|
587
|
+
end
|
|
588
|
+
end
|
|
589
|
+
|
|
590
|
+
# Renders an ODT styled Radio button.
|
|
591
|
+
#
|
|
592
|
+
# @param label [String, nil]
|
|
593
|
+
# @param description [String, nil]
|
|
594
|
+
# @param checked [Boolean]
|
|
595
|
+
# @param color [:primary, :secondary, :success, :warning, :danger, :dark, :neutral]
|
|
596
|
+
# @param size [:sm, :md, :lg]
|
|
597
|
+
# @param disabled [Boolean]
|
|
598
|
+
# @param error [String, nil]
|
|
599
|
+
# @return [ActiveSupport::SafeBuffer]
|
|
600
|
+
def odt_radio(label: nil, description: nil, checked: false, color: :primary, size: :md, disabled: false, error: nil, **html_options)
|
|
601
|
+
color_key = color.to_sym == :dark ? "neutral" : color.to_s.tr("_", "-")
|
|
602
|
+
size_key = size.to_s.tr("_", "-")
|
|
603
|
+
has_error = error.present?
|
|
604
|
+
|
|
605
|
+
classes = [
|
|
606
|
+
"odt-radio",
|
|
607
|
+
"odt-radio--#{color_key}",
|
|
608
|
+
"odt-radio--#{size_key}",
|
|
609
|
+
html_options.delete(:class)
|
|
610
|
+
]
|
|
611
|
+
|
|
612
|
+
opts = html_options.merge(
|
|
613
|
+
type: "radio",
|
|
614
|
+
class: classes.compact.reject(&:blank?).join(" "),
|
|
615
|
+
disabled: disabled
|
|
616
|
+
)
|
|
617
|
+
opts[:checked] = "checked" if checked
|
|
618
|
+
|
|
619
|
+
tag.div(class: "odt-radio-wrapper") do
|
|
620
|
+
parts = []
|
|
621
|
+
parts << tag.label(class: "odt-radio-container") do
|
|
622
|
+
label_parts = [tag.input(**opts)]
|
|
623
|
+
|
|
624
|
+
if label.present? || description.present?
|
|
625
|
+
label_parts << tag.div(class: "odt-radio__label-content") do
|
|
626
|
+
d_parts = []
|
|
627
|
+
d_parts << tag.span(label, class: "odt-radio__label") if label.present?
|
|
628
|
+
d_parts << tag.span(description, class: "odt-radio__description") if description.present?
|
|
629
|
+
safe_join(d_parts)
|
|
630
|
+
end
|
|
631
|
+
end
|
|
632
|
+
|
|
633
|
+
safe_join(label_parts)
|
|
634
|
+
end
|
|
635
|
+
|
|
636
|
+
parts << tag.p(error, class: "odt-error-text", role: "alert") if has_error
|
|
637
|
+
safe_join(parts)
|
|
638
|
+
end
|
|
639
|
+
end
|
|
640
|
+
|
|
641
|
+
# Renders a Form Group wrapper with label, slot, error, and helper texts.
|
|
642
|
+
#
|
|
643
|
+
# @param label [String, nil]
|
|
644
|
+
# @param error [String, nil]
|
|
645
|
+
# @param helper [String, nil]
|
|
646
|
+
# @param helper_text [String, nil]
|
|
647
|
+
# @param success [String, nil]
|
|
648
|
+
# @param required [Boolean]
|
|
649
|
+
# @return [ActiveSupport::SafeBuffer]
|
|
650
|
+
def odt_form_group(label: nil, error: nil, helper: nil, helper_text: nil, success: nil, required: false, **html_options, &block)
|
|
651
|
+
resolved_helper = helper.presence || helper_text.presence
|
|
652
|
+
has_error = error.present?
|
|
653
|
+
has_success = success.present? && !has_error
|
|
654
|
+
|
|
655
|
+
classes = ["odt-form-group", html_options.delete(:class)]
|
|
656
|
+
opts = html_options.merge(class: classes.compact.reject(&:blank?).join(" "))
|
|
657
|
+
|
|
658
|
+
tag.div(**opts) do
|
|
659
|
+
parts = []
|
|
660
|
+
if label.present?
|
|
661
|
+
label_classes = ["odt-label"]
|
|
662
|
+
label_classes << "odt-label--required" if required
|
|
663
|
+
parts << tag.label(label, class: label_classes.join(" "))
|
|
664
|
+
end
|
|
665
|
+
|
|
666
|
+
parts << capture(&block) if block_given?
|
|
667
|
+
|
|
668
|
+
if has_error
|
|
669
|
+
parts << tag.p(error, class: "odt-error-text", role: "alert")
|
|
670
|
+
elsif has_success
|
|
671
|
+
parts << tag.p(success, class: "odt-success-text")
|
|
672
|
+
elsif resolved_helper.present?
|
|
673
|
+
parts << tag.p(resolved_helper, class: "odt-helper-text")
|
|
674
|
+
end
|
|
675
|
+
|
|
676
|
+
safe_join(parts)
|
|
677
|
+
end
|
|
678
|
+
end
|
|
679
|
+
|
|
680
|
+
private
|
|
681
|
+
|
|
682
|
+
# Normalizes button variant key with backwards compatibility
|
|
683
|
+
def normalize_button_variant(variant)
|
|
684
|
+
case variant.to_sym
|
|
685
|
+
when :light
|
|
686
|
+
"light"
|
|
687
|
+
when :outline, :outlined
|
|
688
|
+
"outline"
|
|
689
|
+
when :subtle
|
|
690
|
+
"subtle"
|
|
691
|
+
when :frosted
|
|
692
|
+
"frosted"
|
|
693
|
+
when :capsule
|
|
694
|
+
"capsule"
|
|
695
|
+
when :ghost
|
|
696
|
+
"ghost"
|
|
697
|
+
else
|
|
698
|
+
"filled"
|
|
699
|
+
end
|
|
700
|
+
end
|
|
701
|
+
|
|
702
|
+
# Computes initials from a full name (e.g., "John Doe" -> "JD")
|
|
703
|
+
def compute_avatar_initials(name)
|
|
704
|
+
return nil if name.blank?
|
|
705
|
+
|
|
706
|
+
parts = name.strip.split(/\s+/)
|
|
707
|
+
if parts.length == 1
|
|
708
|
+
parts[0].slice(0, 2).upcase
|
|
709
|
+
else
|
|
710
|
+
"#{parts.first[0]}#{parts.last[0]}".upcase
|
|
711
|
+
end
|
|
712
|
+
end
|
|
713
|
+
|
|
714
|
+
# Safely renders an icon (SVG or HTML class)
|
|
715
|
+
def render_icon_element(icon)
|
|
716
|
+
return nil if icon.blank?
|
|
717
|
+
|
|
718
|
+
if icon.start_with?("<")
|
|
719
|
+
icon.html_safe
|
|
720
|
+
else
|
|
721
|
+
tag.i(class: icon, aria: { hidden: "true" })
|
|
722
|
+
end
|
|
723
|
+
end
|
|
337
724
|
end
|
|
338
725
|
end
|