module Kiso::ComponentHelper

def kiso_merge_ui_layers(component, instance_ui)

Returns:
  • (Hash{Symbol => String}) - merged ui hash with all slot

Parameters:
  • instance_ui (Hash{Symbol => String}, nil) -- per-instance ui
  • component (Symbol) -- the component name (e.g. +:card+)
def kiso_merge_ui_layers(component, instance_ui)
  global_ui = Kiso.config.theme.dig(component, :ui)
  return instance_ui || {} if global_ui.nil? || global_ui.empty?
  return global_ui if instance_ui.nil? || instance_ui.empty?
  # Instance wins. For slots in both, concatenate — tailwind_merge
  # resolves conflicts when .render(class:) is called.
  global_ui.merge(instance_ui) do |_slot, global_classes, instance_classes|
    "#{global_classes} #{instance_classes}"
  end
end

def kiso_prepare_options(component_options, slot:, **data_attrs)

Other tags:
    Example: Caller passes data attributes through -
    Example: With a Stimulus controller -
    Example: Basic usage in a component partial -

Raises:
  • (ArgumentError) - if +component_options+ contains a +class:+ key.

Returns:
  • (Hash) - merged data attributes hash suitable for the +data:+

Parameters:
  • data_attrs (Hash) -- additional data attributes merged into the
  • slot (String) -- the +data-slot+ value in kebab-case
  • component_options (Hash) -- the +**component_options+ splat from
def kiso_prepare_options(component_options, slot:, **data_attrs)
  if component_options.key?(:class)
    raise ArgumentError, "Use css_classes: instead of class: for Kiso components"
  end
  user_data = component_options.delete(:data) || {}
  component_data = {slot: slot, **data_attrs}
  # Stimulus data-action and data-controller are space-separated and
  # additive — concatenate rather than overwrite so both the component's
  # and user's bindings apply.
  CONCATENABLE_DATA_KEYS.each do |key|
    if user_data.key?(key) && component_data.key?(key)
      component_data[key] = "#{user_data.delete(key)} #{component_data[key]}"
    end
  end
  user_data.merge(component_data)
end

def kiso_render_component(component, part, path_prefix:, collection:, ui:, scope:, merge_global_ui: true, **kwargs, &block)

Returns:
  • (ActiveSupport::SafeBuffer) - rendered HTML

Parameters:
  • block (Proc) -- optional block for component content
  • kwargs (Hash) -- locals passed to the partial
  • merge_global_ui (Boolean) -- whether to merge global config ui
  • scope (Hash, nil) -- domain locals shared from parent to sub-parts
  • ui (Hash{Symbol => String}, nil) -- per-slot class overrides
  • collection (Array, nil) -- renders the partial once per item
  • path_prefix (String) -- partial path prefix
  • part (Symbol, nil) -- optional sub-part name
  • component (Symbol) -- the component name
def kiso_render_component(component, part, path_prefix:, collection:, ui:, scope:, merge_global_ui: true, **kwargs, &block)
  path = if part
    "#{path_prefix}/#{component}/#{part}"
  else
    "#{path_prefix}/#{component}"
  end
  # Prevent yield from bubbling up the ERB rendering chain when no block
  # is passed. Without this, partials that use `capture { yield }.presence`
  # to support optional block overrides (e.g., toggle/collapse/separator)
  # would have their `yield` bubble through nested content_tag blocks all
  # the way to the layout's `<%= yield %>`, capturing the entire page
  # template content. An explicit empty proc gives `yield` something to
  # call, returning empty string → `.presence` returns nil → default
  # content renders correctly.
  block ||= proc {}
  if part
    # Sub-part: merge scope from parent context (scope values first, explicit kwargs win)
    parent_scope = kiso_current_scope(component)
    kwargs = parent_scope.merge(kwargs) if parent_scope.present?
    # Sub-part: merge slot override from parent's ui context
    parent_ui = kiso_current_ui(component)
    if (slot_classes = parent_ui[part].presence)
      existing = kwargs[:css_classes] || ""
      kwargs[:css_classes] = existing.blank? ? slot_classes : "#{existing} #{slot_classes}"
    end
    # Forward ui: to sub-part partial when explicitly provided
    kwargs[:ui] = ui if ui
    if collection
      render partial: path, collection: collection, locals: kwargs, &block
    else
      render path, **kwargs, &block
    end
  else
    # Parent component: merge global ui config with instance ui (or use instance ui directly)
    merged_ui = if merge_global_ui
      kiso_merge_ui_layers(component, ui)
    else
      ui || {}
    end
    has_ui = merged_ui.present?
    has_scope = scope.present?
    # Push context for composed sub-parts to read (skip when empty)
    kiso_push_ui_context(component, merged_ui) if has_ui
    kiso_push_scope(component, scope) if has_scope
    # Merge scope into parent kwargs so the parent partial receives
    # scope values as strict locals (scope as defaults, explicit kwargs win)
    kwargs = scope.merge(kwargs) if has_scope
    begin
      locals = has_ui ? kwargs.merge(ui: merged_ui) : kwargs
      if collection
        render partial: path, collection: collection, locals: locals, &block
      else
        render path, **locals, &block
      end
    ensure
      kiso_pop_ui_context(component) if has_ui
      kiso_pop_scope(component) if has_scope
    end
  end
end

def kui(component, part = nil, collection: nil, ui: nil, scope: nil, **kwargs, &block)

Other tags:
    Example: Render a collection -
    Example: Share domain locals with sub-parts via scope -
    Example: Pass HTML attributes through to the root element -
    Example: Override root element classes -
    Example: Render an alert with inner element overrides -
    Example: Render a card with per-slot overrides -
    Example: Render a card with composed sub-parts -
    Example: Render a simple badge -

Returns:
  • (ActiveSupport::SafeBuffer) - rendered HTML string

Other tags:
    Yield: - optional block for component content. When omitted, an empty

Parameters:
  • kwargs (Hash) -- locals forwarded to the partial (e.g. +color:+,
  • scope (Hash, nil) -- domain locals shared from parent to sub-parts
  • ui (Hash{Symbol => String}, nil) -- per-slot class overrides keyed
  • collection (Array, nil) -- when present, renders the partial once
  • part (Symbol, nil) -- optional sub-part name (e.g. +:header+,
  • component (Symbol) -- the component name (e.g. +:badge+, +:card+,
def kui(component, part = nil, collection: nil, ui: nil, scope: nil, **kwargs, &block)
  kiso_render_component(
    component, part,
    path_prefix: "kiso/components",
    collection: collection, ui: ui, scope: scope, merge_global_ui: true,
    **kwargs, &block
  )
end

def kui_tag(tag, theme:, slot:, css_classes: "", variants: {}, **component_options, &block)

Other tags:
    Example: Button with type attribute -
    Example: In a component partial -

Returns:
  • (ActiveSupport::SafeBuffer) - rendered HTML

Other tags:
    Yield: - optional block for element content

Parameters:
  • component_options (Hash) -- HTML attributes forwarded to
  • variants (Hash) -- variant key-value pairs forwarded to
  • css_classes (String) -- caller's class overrides, merged via
  • slot (String) -- the +data-slot+ value in kebab-case
  • theme (ClassVariants::Instance) -- the theme module to render
  • tag (Symbol) -- HTML element name (e.g. +:div+, +:span+, +:button+)
def kui_tag(tag, theme:, slot:, css_classes: "", variants: {}, **component_options, &block)
  html_options = {
    class: theme.render(**variants, class: css_classes),
    data: kiso_prepare_options(component_options, slot: slot),
    **component_options
  }
  if block
    content_tag(tag, html_options, &block)
  else
    content_tag(tag, nil, html_options)
  end
end