Module: Protege::Components::SidebarHelper

Included in:
ComponentsHelper
Defined in:
app/helpers/protege/components/sidebar_helper.rb

Overview

The console sidebar surface — the docked <aside> shell and the header/list/footer elements that fill it. Split out of LayoutHelper so the sidebar is a single focused concern: sidebar_container is the resizable outer shell, sidebar_content the flex column inside it, and sidebar_header / sidebar_search_link / sidebar_body / sidebar_item / sidebar_stream compose the list.

Colors use the CSS custom properties (--surface, --border, --text, --text-muted, --text-faint) defined in tailwind/application.css so dark mode works via a single .dark class toggle.

Instance Method Summary collapse

Instance Method Details

Render a fixed-height sidebar bar — the standard horizontal strip (bordered, faint, padded) used for a labeled control row such as the trace filter. Its contents are yielded and laid out with space-between, matching the +sidebar_header+/+sidebar_search_link+ bar shape.

Yields:

  • the bar contents (typically a label span and a control)

Returns:

  • (ActiveSupport::SafeBuffer)

    the bar markup



91
92
93
94
# File 'app/helpers/protege/components/sidebar_helper.rb', line 91

def sidebar_bar(&)
  tag.div(class: 'flex h-10 flex-shrink-0 items-center justify-between px-4 text-xs',
          style: 'border-bottom: 1px solid var(--border); color: var(--text-faint)', &)
end

Render the scrollable list area of a sidebar — the flexible middle that grows between the fixed header and pagination footer.

Two forms:

  • sidebar_body(collection, empty: '…') { |item| … } renders each item via the block, or a muted empty-state hint when the collection is empty — the common list sidebar.
  • sidebar_body { … } (no collection) just wraps arbitrary content, for sidebars that own their own iteration/empty handling (e.g. the Inbox's Turbo Frame, the search results' two empty states).

Parameters:

  • collection (Enumerable, nil) (defaults to: nil)

    the items to render, or nil for the block-only form

  • empty (String, nil) (defaults to: nil)

    the empty-state message for the collection form

  • id (String, nil) (defaults to: nil)

    DOM id for the scroll container — the live-append broadcast target

Yields:

  • (item)

    each item (collection form), or the raw body (block-only form)

Returns:

  • (ActiveSupport::SafeBuffer)

    the scrollable body markup



57
58
59
60
61
62
63
64
65
66
67
# File 'app/helpers/protege/components/sidebar_helper.rb', line 57

def sidebar_body(collection = nil, empty: nil, id: nil, &block)
  tag.div(class: 'flex-1 overflow-y-auto', id:) do
    if collection.nil?
      capture(&block)
    elsif collection.any?
      safe_join(collection.map { |item| capture(item, &block) })
    else
      hint(empty, classes: 'py-8 text-center')
    end
  end
end

Render the sidebar shell — the resizable, full-height <aside> that docks the navigation sidebar on the left of the console. The user can drag its right edge to resize it (the general resize_handle paired with resize.js), bounded by min/max and remembered across page loads. The yielded block is the sidebar's content (typically a sidebar_content wrapper).

Yields:

  • the sidebar contents

Returns:

  • (ActiveSupport::SafeBuffer)

    the aside markup



20
21
22
23
24
25
26
# File 'app/helpers/protege/components/sidebar_helper.rb', line 20

def sidebar_container(&block)
  tag.aside(class: 'relative flex w-72 flex-shrink-0 flex-col overflow-hidden',
            data:  { resize: '', resize_edge: 'right', resize_min: 200,
                     resize_max: 480, resize_key: 'protege:sidebar-width' }) do
    resize_handle(edge: :right) + capture(&block)
  end
end

Wrap a sidebar's contents (header, scrollable list, pagination footer) in the flex column that fills the sidebar shell — header/footer stay fixed while the list scrolls between them.

Pass frame: to render the column as a Turbo Frame (an update target a form/stream can replace) instead of a plain div; the same layout classes apply either way. A bare <turbo-frame> is display: inline, so the classes (which set it flex) are what make the frame variant lay out at all.

Parameters:

  • frame (String, nil) (defaults to: nil)

    a Turbo Frame id to render as, or nil for a plain div

Yields:

  • the sidebar contents

Returns:

  • (ActiveSupport::SafeBuffer)

    the column markup



38
39
40
41
# File 'app/helpers/protege/components/sidebar_helper.rb', line 38

def sidebar_content(frame: nil, &)
  classes = 'flex min-h-0 flex-1 flex-col'
  frame ? turbo_frame_tag(frame, class: classes, &) : tag.div(class: classes, &)
end

Render a sidebar section header with an optional "new record" link.

Parameters:

  • title (String)

    the section title

  • new_path (String, nil) (defaults to: nil)

    optional target for the new-record link

  • new_label (String) (defaults to: 'New')

    the new-record link label

Returns:

  • (ActiveSupport::SafeBuffer)

    the sidebar header markup



102
103
104
105
106
107
108
# File 'app/helpers/protege/components/sidebar_helper.rb', line 102

def sidebar_header(title, new_path: nil, new_label: 'New')
  tag.div(class: 'flex h-10 flex-shrink-0 items-center justify-between px-4',
          style: 'border-bottom: 1px solid var(--border)') do
    tag.span(title, class: 'text-xs font-semibold uppercase tracking-wider', style: 'color: var(--text-faint)') +
      (new_path ? ui_link(new_label, new_path) : ''.html_safe)
  end
end

Render a sidebar list entry — title, optional detail/subtitle, and an optional extra block — highlighted when active.

Parameters:

  • title (String)

    the entry title

  • href (String)

    the entry target

  • detail (String, ActiveSupport::SafeBuffer, nil) (defaults to: nil)

    right-aligned detail text or markup

  • subtitle (String, nil) (defaults to: nil)

    optional subtitle line

  • active (Boolean) (defaults to: false)

    whether to highlight the entry as current

  • attrs (Hash)

    extra HTML attributes forwarded to the link

Yields:

  • optional extra content rendered beneath the subtitle

Returns:

  • (ActiveSupport::SafeBuffer)

    the sidebar entry markup



136
137
138
139
140
141
142
# File 'app/helpers/protege/components/sidebar_helper.rb', line 136

def sidebar_item(title, href, detail: nil, subtitle: nil, active: false, **attrs, &block)
  link_to(href, class: 'block px-4 py-3 transition-colors', style: sidebar_item_style(active), **attrs) do
    sidebar_item_header(title, detail) +
      sidebar_item_subtitle(subtitle) +
      (block ? tag.div(class: 'mt-1', &block) : ''.html_safe)
  end
end

Render a sidebar search affordance — a faux search field that links to a search page.

Styled like an input (magnifier + muted placeholder) but it is a plain link: clicking it navigates to href. Sits between the sidebar header and its list.

Parameters:

  • href (String)

    the search page to open

  • placeholder (String) (defaults to: 'Search…')

    the muted prompt text

Returns:

  • (ActiveSupport::SafeBuffer)

    the search-bar markup



118
119
120
121
122
123
# File 'app/helpers/protege/components/sidebar_helper.rb', line 118

def sidebar_search_link(href, placeholder: 'Search…')
  classes = 'flex h-10 flex-shrink-0 items-center gap-2 px-4 text-xs transition-colors hover:opacity-80'
  link_to(href, class: classes, style: 'border-bottom: 1px solid var(--border); color: var(--text-faint)') do
    search_icon + tag.span(placeholder)
  end
end

Subscribe the sidebar to a Turbo Stream for live-appended rows — but only on the first page.

New rows are broadcast (prepend/append) into +sidebar_body+'s id target; gating the subscription to page 1 is how we keep live rows from landing on the wrong page — later pages simply aren't subscribed and reconcile on their next load. Row-replace updates are id-targeted, so they're unaffected by whether this subscription is present.

Parameters:

  • streamables (Array)

    the Turbo stream identifier(s) (e.g. a string, or [record, :scope])

  • pagy (Pagy)

    the list's pagination — the subscription renders only when pagy.page == 1

Returns:

  • (ActiveSupport::SafeBuffer)

    the stream-source tag on page 1, else an empty buffer



79
80
81
82
83
# File 'app/helpers/protege/components/sidebar_helper.rb', line 79

def sidebar_stream(*streamables, pagy:)
  return ''.html_safe unless pagy.page == 1

  turbo_stream_from(*streamables)
end