Module: OdtUi::Helper

Defined in:
lib/odt_ui/helper.rb

Constant Summary collapse

BUTTON_VARIANTS =
{
  filled: {
    primary: "bg-primary-500 text-white hover:bg-primary-600 active:bg-primary-700 shadow-card hover:shadow-lg focus-visible:ring-primary-400",
    secondary: "bg-secondary-500 text-slate-900 hover:bg-secondary-600 active:bg-secondary-700 shadow-card hover:shadow-lg focus-visible:ring-secondary-400",
    success: "bg-success text-white hover:opacity-90 active:opacity-100 shadow-sm focus-visible:ring-green-400",
    warning: "bg-warning text-slate-900 hover:opacity-90 active:opacity-100 shadow-sm focus-visible:ring-amber-400",
    danger: "bg-danger text-white hover:opacity-90 active:opacity-100 shadow-sm focus-visible:ring-red-400",
    neutral: "bg-neutral-800 text-white hover:bg-neutral-900 active:bg-neutral-950 shadow-sm focus-visible:ring-neutral-400"
  },
  light: {
    primary: "bg-primary-50 text-primary-700 hover:bg-primary-100 active:bg-primary-200 focus-visible:ring-primary-300",
    secondary: "bg-secondary-50 text-secondary-900 hover:bg-secondary-100 active:bg-secondary-200 focus-visible:ring-secondary-300",
    success: "bg-surface-success text-fg-success hover:bg-green-100 active:bg-green-200 focus-visible:ring-green-300",
    warning: "bg-surface-warning text-fg-warning hover:bg-amber-100 active:bg-amber-200 focus-visible:ring-amber-300",
    danger: "bg-surface-danger text-fg-danger hover:bg-red-100 active:bg-red-200 focus-visible:ring-red-300",
    neutral: "bg-surface-muted text-fg-muted hover:bg-surface-subtle active:bg-surface-subtle focus-visible:ring-neutral-300"
  },
  outline: {
    primary: "border border-primary-300 text-primary-600 bg-transparent hover:bg-primary-50 active:bg-primary-100 focus-visible:ring-primary-300",
    secondary: "border border-secondary-300 text-secondary-900 bg-transparent hover:bg-secondary-50 active:bg-secondary-100 focus-visible:ring-secondary-300",
    danger: "border border-danger text-danger bg-transparent hover:bg-surface-danger active:bg-red-100 focus-visible:ring-red-300",
    neutral: "border border-line text-fg bg-transparent hover:bg-surface-subtle active:bg-surface-muted focus-visible:ring-neutral-300"
  },
  subtle: {
    primary: "bg-transparent text-primary-600 hover:bg-primary-50 active:bg-primary-100",
    secondary: "bg-transparent text-secondary-900 hover:bg-secondary-50 active:bg-secondary-100",
    neutral: "bg-transparent text-fg hover:bg-surface-subtle active:bg-surface-muted",
    danger: "bg-transparent text-danger hover:bg-surface-danger active:bg-red-100"
  },
  ghost: {
    primary: "bg-transparent text-fg-primary hover:text-primary-600 hover:bg-primary-50 active:bg-primary-100",
    secondary: "bg-transparent text-fg hover:bg-surface-muted active:bg-surface-subtle",
    neutral: "bg-transparent text-fg-muted hover:text-fg hover:bg-surface-muted active:bg-surface-subtle"
  }
}.freeze
BUTTON_SIZES =
{
  xs: "px-2.5 py-1 text-xs gap-1",
  sm: "px-3.5 py-1.5 text-xs gap-1.5 font-medium",
  md: "px-5 py-2.5 text-sm gap-2 font-medium",
  lg: "px-6 py-3 text-base gap-2.5 font-semibold",
  xl: "px-7 py-3.5 text-lg gap-3 font-semibold"
}.freeze
RADIUS_CLASSES =
{
  none: "rounded-none",
  xs: "rounded-xs",
  sm: "rounded-sm",
  md: "rounded-md",
  lg: "rounded-lg",
  xl: "rounded-xl",
  "2xl": "rounded-2xl",
  "3xl": "rounded-3xl",
  "4xl": "rounded-4xl",
  full: "rounded-full"
}.freeze
BADGE_VARIANTS =
{
  subtle: {
    primary: "bg-primary-50 text-primary-600",
    secondary: "bg-secondary-100 text-secondary-900",
    success: "bg-surface-success text-success",
    warning: "bg-surface-warning text-warning",
    danger: "bg-surface-danger text-danger",
    info: "bg-surface-info text-info",
    neutral: "bg-surface-muted text-fg-muted"
  },
  filled: {
    primary: "bg-primary-500 text-white",
    secondary: "bg-secondary-500 text-slate-900",
    success: "bg-success text-white",
    warning: "bg-warning text-slate-900",
    danger: "bg-danger text-white",
    info: "bg-info text-white",
    neutral: "bg-neutral-800 text-white"
  },
  outline: {
    primary: "border border-primary-300 text-primary-600 bg-transparent",
    secondary: "border border-secondary-300 text-secondary-800 bg-transparent",
    success: "border border-line-success text-success bg-transparent",
    warning: "border border-line-warning text-warning bg-transparent",
    danger: "border border-line-danger text-danger bg-transparent",
    info: "border border-info text-info bg-transparent",
    neutral: "border border-line text-fg-muted bg-transparent"
  }
}.freeze
BADGE_SIZES =
{
  sm: "px-3 py-1.5 text-xs font-semibold",
  md: "px-3.5 py-1.5 text-sm font-semibold"
}.freeze
CARD_VARIANTS =
{
  elevated: "bg-surface shadow-card",
  frosted: "bg-surface/70 backdrop-blur-xl shadow-card",
  subtle: "bg-surface-subtle",
  muted: "bg-surface-muted",
  outlined: "bg-surface border border-line shadow-none"
}.freeze
AVATAR_SIZES =
{
  xs: "w-6 h-6 min-w-6 min-h-6 text-[10px]",
  sm: "w-8 h-8 min-w-8 min-h-8 text-xs",
  md: "w-10 h-10 min-w-10 min-h-10 text-sm",
  lg: "w-12 h-12 min-w-12 min-h-12 text-base",
  xl: "w-16 h-16 min-w-16 min-h-16 text-xl"
}.freeze
AVATAR_COLORS =
{
  secondary: "bg-secondary-100 text-secondary-900",
  primary: "bg-primary-100 text-primary-700",
  neutral: "bg-surface-muted text-fg"
}.freeze

Instance Method Summary collapse

Instance Method Details

#odt_alert(message = nil, variant: :success, icon: nil, **html_options, &block) ⇒ ActiveSupport::SafeBuffer

Parameters:

  • message (String, nil) (defaults to: nil)
  • variant (:success, :warning, :danger, :info) (defaults to: :success)
  • icon (String, nil) (defaults to: nil)

Returns:

  • (ActiveSupport::SafeBuffer)


233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
# File 'lib/odt_ui/helper.rb', line 233

def odt_alert(message = nil, variant: :success, icon: nil, **html_options, &block)
  styles = {
    success: { bg: "bg-surface-success border-green-200 text-fg-success", icon: "fa-solid fa-circle-check" },
    warning: { bg: "bg-surface-warning border-amber-200 text-fg-warning", icon: "fa-solid fa-triangle-exclamation" },
    danger: { bg: "bg-surface-danger border-red-200 text-fg-danger", icon: "fa-solid fa-circle-xmark" },
    info: { bg: "bg-surface-info border-blue-200 text-fg-info", icon: "fa-solid fa-circle-info" }
  }

  config = styles[variant.to_sym] || styles[:info]
  icon_name = icon || config[:icon]

  classes = ["p-4 rounded-2xl border flex items-center gap-3 text-sm shadow-sm", config[:bg], html_options[:class]].compact.join(" ")
  opts = html_options.merge(class: classes)

  parts = []
  parts << tag.i(class: "#{icon_name} text-base shrink-0") if icon_name.present?
  parts << (block_given? ? capture(&block) : tag.p(message, class: "font-medium"))
  inner_content = safe_join(parts.compact)

  tag.div(inner_content, **opts)
end

#odt_avatar(name: nil, src: nil, size: :sm, color: :secondary, icon: "fa-solid fa-user", **html_options) ⇒ ActiveSupport::SafeBuffer

Parameters:

  • name (String, nil) (defaults to: nil)
  • src (String, nil) (defaults to: nil)
  • size (:xs, :sm, :md, :lg, :xl) (defaults to: :sm)
  • color (:secondary, :primary, :neutral) (defaults to: :secondary)
  • icon (String) (defaults to: "fa-solid fa-user")

Returns:

  • (ActiveSupport::SafeBuffer)


312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
# File 'lib/odt_ui/helper.rb', line 312

def odt_avatar(name: nil, src: nil, size: :sm, color: :secondary, icon: "fa-solid fa-user", **html_options)
  s_class = AVATAR_SIZES[size.to_sym] || AVATAR_SIZES[:sm]
  c_class = AVATAR_COLORS[color.to_sym] || AVATAR_COLORS[:secondary]
  classes = ["aspect-square rounded-full flex items-center justify-center font-bold uppercase select-none shrink-0 overflow-hidden ring-2 ring-white shadow-xs", s_class, c_class, html_options[:class]].compact.join(" ")
  opts = html_options.merge(class: classes)

  initials = if name.present?
    parts = name.strip.split(/\s+/)
    if parts.length == 1
      parts[0].slice(0, 2).upcase
    else
      "#{parts.first[0]}#{parts.last[0]}".upcase
    end
  end

  inner_content = if src.present?
    image_tag(src, alt: name || "Avatar", class: "w-full h-full object-cover")
  elsif initials.present?
    tag.span(initials, class: "leading-none")
  else
    tag.i(class: icon)
  end

  tag.div(inner_content, **opts)
end

#odt_badge(text = nil, variant: :subtle, color: :neutral, size: :sm, radius: :full, dot: false, icon: nil, **html_options, &block) ⇒ ActiveSupport::SafeBuffer

Parameters:

  • text (String, nil) (defaults to: nil)
  • variant (:subtle, :filled, :outline) (defaults to: :subtle)
  • color (:primary, :secondary, :success, :warning, :danger, :info, :neutral) (defaults to: :neutral)
  • size (:sm, :md) (defaults to: :sm)
  • radius (:none, :xs, :sm, :md, :lg, :xl, 2xl, 3xl, 4xl, :full) (defaults to: :full)
  • dot (Boolean) (defaults to: false)
  • icon (String, nil) (defaults to: nil)

Returns:

  • (ActiveSupport::SafeBuffer)


171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
# File 'lib/odt_ui/helper.rb', line 171

def odt_badge(text = nil, variant: :subtle, color: :neutral, size: :sm, radius: :full, dot: false, icon: nil, **html_options, &block)
  variant_key = variant.to_sym
  color_key = color.to_sym
  size_key = size.to_sym
  radius_key = radius.to_s.to_sym

  base_class = "inline-flex items-center gap-1.5 leading-none transition-colors select-none font-medium shrink-0"
  variant_map = BADGE_VARIANTS[variant_key] || BADGE_VARIANTS[:subtle]
  style_class = variant_map[color_key] || variant_map[:neutral] || variant_map.values.first
  size_class = BADGE_SIZES[size_key] || BADGE_SIZES[:sm]
  radius_class = RADIUS_CLASSES[radius_key] || RADIUS_CLASSES[:full]

  classes = [base_class, style_class, size_class, radius_class, html_options[:class]].compact.join(" ")
  opts = html_options.merge(class: classes)

  parts = []
  parts << tag.span(class: "w-1.5 h-1.5 rounded-full bg-current opacity-80 shrink-0") if dot
  parts << (icon.start_with?("<") ? icon.html_safe : tag.i(class: icon)) if icon.present?
  parts << (block_given? ? capture(&block) : text)
  inner_content = safe_join(parts.compact, " ")

  tag.span(inner_content, **opts)
end

#odt_button(text = nil, variant: :filled, color: :primary, size: :md, radius: :full, href: nil, icon: nil, right_icon: nil, full_width: false, disabled: false, type: "button", **html_options, &block) ⇒ ActiveSupport::SafeBuffer

Parameters:

  • text (String, nil) (defaults to: nil)
  • variant (:filled, :light, :outline, :subtle, :ghost) (defaults to: :filled)
  • color (:primary, :secondary, :success, :warning, :danger, :neutral) (defaults to: :primary)
  • size (:xs, :sm, :md, :lg, :xl) (defaults to: :md)
  • radius (:none, :xs, :sm, :md, :lg, :xl, :"2xl", :"3xl", :"4xl", :full) (defaults to: :full)
  • href (String, nil) (defaults to: nil)
  • icon (String, nil) (defaults to: nil)
  • right_icon (String, nil) (defaults to: nil)
  • full_width (Boolean) (defaults to: false)
  • disabled (Boolean) (defaults to: false)
  • type (String) (defaults to: "button")

Returns:

  • (ActiveSupport::SafeBuffer)


131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
# File 'lib/odt_ui/helper.rb', line 131

def odt_button(text = nil, variant: :filled, color: :primary, size: :md, radius: :full, href: nil, icon: nil, right_icon: nil, full_width: false, disabled: false, type: "button", **html_options, &block)
  variant_key = variant.to_sym
  color_key = color.to_sym
  size_key = size.to_sym
  radius_key = radius.to_s.to_sym

  base_class = "inline-flex items-center justify-center font-medium transition-all duration-200 select-none focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:opacity-50 disabled:pointer-events-none cursor-pointer"
  
  variant_map = BUTTON_VARIANTS[variant_key] || BUTTON_VARIANTS[:filled]
  style_class = variant_map[color_key] || variant_map[:primary] || variant_map.values.first
  size_class = BUTTON_SIZES[size_key] || BUTTON_SIZES[:md]
  radius_class = RADIUS_CLASSES[radius_key] || RADIUS_CLASSES[:full]
  width_class = full_width ? "w-full" : ""

  classes = [base_class, style_class, size_class, radius_class, width_class, html_options[:class]].compact.join(" ")
  opts = html_options.merge(class: classes)
  opts[:disabled] = true if disabled

  parts = []
  parts << (icon.start_with?("<") ? icon.html_safe : tag.i(class: icon)) if icon.present?
  parts << (block_given? ? capture(&block) : text)
  parts << (right_icon.start_with?("<") ? right_icon.html_safe : tag.i(class: right_icon)) if right_icon.present?
  inner_content = safe_join(parts.compact, " ")

  if href.present?
    link_to(inner_content, href, opts)
  else
    opts[:type] = type
    tag.button(inner_content, **opts)
  end
end

#odt_card(title: nil, subtitle: nil, variant: :frosted, radius: :"3xl", border: false, hover: true, **html_options, &block) ⇒ ActiveSupport::SafeBuffer

Parameters:

  • title (String, nil) (defaults to: nil)
  • subtitle (String, nil) (defaults to: nil)
  • variant (:elevated, :frosted, :subtle, :muted, :outlined) (defaults to: :frosted)
  • radius (:none, :xs, :sm, :md, :lg, :xl, 2xl, 3xl, 4xl, :full) (defaults to: :"3xl")
  • border (Boolean, :subtle, :line) (defaults to: false)
  • hover (Boolean) (defaults to: true)

Returns:

  • (ActiveSupport::SafeBuffer)


202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
# File 'lib/odt_ui/helper.rb', line 202

def odt_card(title: nil, subtitle: nil, variant: :frosted, radius: :"3xl", border: false, hover: true, **html_options, &block)
  variant_class = CARD_VARIANTS[variant.to_sym] || CARD_VARIANTS[:frosted]
  radius_class = RADIUS_CLASSES[radius.to_s.to_sym] || RADIUS_CLASSES[:"3xl"]
  hover_class = hover ? "hover:shadow-primary-200/50 hover:-translate-y-0.5 transition-all duration-300" : ""
  border_class = if border == true
    "border border-line"
  elsif border.is_a?(Symbol) || border.is_a?(String)
    "border border-#{border}"
  else
    "border-0"
  end

  classes = ["p-6 flex flex-col h-full", variant_class, border_class, radius_class, hover_class, html_options[:class]].compact.join(" ")
  opts = html_options.merge(class: classes)

  header = if title.present?
    tag.div(class: "mb-3") do
      concat tag.h3(title, class: "text-xl font-bold text-fg-strong leading-tight")
      concat tag.p(subtitle, class: "text-sm text-fg-muted mt-1") if subtitle.present?
    end
  end
  content = block_given? ? capture(&block) : nil
  inner_content = safe_join([header, content].compact)

  tag.div(inner_content, **opts)
end

#odt_heading(text = nil, as: :h2, **html_options, &block) ⇒ ActiveSupport::SafeBuffer

Parameters:

  • text (String, nil) (defaults to: nil)
  • as (:h1, :h2, :h3, :h4, :h5) (defaults to: :h2)

Returns:

  • (ActiveSupport::SafeBuffer)


258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
# File 'lib/odt_ui/helper.rb', line 258

def odt_heading(text = nil, as: :h2, **html_options, &block)
  tag_name = as.to_sym
  heading_styles = {
    h1: "text-3xl font-bold text-fg-strong tracking-tight",
    h2: "text-2xl font-bold text-fg-strong tracking-tight",
    h3: "text-xl font-bold text-fg-strong leading-tight",
    h4: "text-lg font-semibold text-fg-strong",
    h5: "text-base font-semibold text-fg-strong"
  }

  style_class = heading_styles[tag_name] || heading_styles[:h2]
  classes = [style_class, html_options[:class]].compact.join(" ")
  opts = html_options.merge(class: classes)
  inner_content = block_given? ? capture(&block) : text

  tag.public_send(tag_name, inner_content, **opts)
end

#odt_text(text = nil, color: :default, size: :base, **html_options, &block) ⇒ ActiveSupport::SafeBuffer

Parameters:

  • text (String, nil) (defaults to: nil)
  • color (:default, :strong, :muted, :subtle, :primary) (defaults to: :default)
  • size (:xs, :sm, :base, :lg) (defaults to: :base)

Returns:

  • (ActiveSupport::SafeBuffer)


280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
# File 'lib/odt_ui/helper.rb', line 280

def odt_text(text = nil, color: :default, size: :base, **html_options, &block)
  color_styles = {
    default: "text-fg",
    strong: "text-fg-strong font-medium",
    muted: "text-fg-muted",
    subtle: "text-fg-subtle",
    primary: "text-primary-600 font-medium"
  }

  size_styles = {
    xs: "text-xs",
    sm: "text-sm",
    base: "text-base",
    lg: "text-lg"
  }

  c_class = color_styles[color.to_sym] || color_styles[:default]
  s_class = size_styles[size.to_sym] || size_styles[:base]

  classes = [c_class, s_class, html_options[:class]].compact.join(" ")
  opts = html_options.merge(class: classes)
  inner_content = block_given? ? capture(&block) : text

  tag.p(inner_content, **opts)
end