Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions .claude/skills/layered-ui-rails/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,28 @@ Populate layout regions with `content_for` (always above the render call):
<% content_for :l_ui_logo_dark do %>
<%= image_tag "my_logo_dark.svg", alt: "", class: "l-ui-header__logo l-ui-header__logo--dark" %>
<% end %>

<%# Prepend or append items to the header actions group %>
<% content_for :l_ui_header_actions_start do %>
<%= link_to "Docs", docs_path, class: "l-ui-button l-ui-button--ghost l-ui-button--small" %>
<% end %>
<% content_for :l_ui_header_actions_end do %>
<%= link_to "Help", help_path, class: "l-ui-button l-ui-button--ghost l-ui-button--small" %>
<% end %>

<%# Or replace the default actions group entirely and compose with helpers %>
<% content_for :l_ui_header_actions do %>
<%= link_to "Docs", docs_path, class: "l-ui-button l-ui-button--ghost l-ui-button--small" %>
<%= l_ui_theme_toggle %>
<%= l_ui_authentication %>
<%= l_ui_navigation_toggle %>
<% end %>

<%# Inline header links (alongside the logo) %>
<% content_for :l_ui_header_links do %>
<%= link_to "Pricing", pricing_path %>
<%= link_to "About", about_path %>
<% end %>
```

Body class modifiers:
Expand Down Expand Up @@ -116,6 +138,9 @@ Quick reference:
| `l_ui_normalise_field(record, config)` | Normalise a raw field config hash into canonical form |
| `l_ui_user_signed_in?` | Check if user is authenticated |
| `l_ui_current_user` | Current user object |
| `l_ui_theme_toggle` | Default header theme toggle button |
| `l_ui_authentication` | Default header login/register buttons (Devise) |
| `l_ui_navigation_toggle` | Default header sidebar toggle button |

## CSS classes

Expand Down
19 changes: 19 additions & 0 deletions .claude/skills/layered-ui-rails/references/HELPERS.md
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,25 @@ The button does not need to be inside the helper's wrapper, and no `data-control

Calling `dialog.showModal()` directly is not supported - it bypasses the `l-ui--modal` controller and skips scroll lock, focus restoration, open-count tracking, and the screen-reader announcement.

## Header

```ruby
l_ui_theme_toggle # Renders the dark/light theme toggle button
l_ui_authentication # Renders Devise login/register buttons (no-op when signed in or when Devise routes are absent)
l_ui_navigation_toggle # Renders the hamburger that toggles the sidebar (only when navigation items exist or a user is signed in)
```

Use these when overriding the header actions group with `:l_ui_header_actions` to compose your own bar from the default building blocks. The header also exposes `:l_ui_header_actions_start` and `:l_ui_header_actions_end` yields to prepend or append items without replacing the defaults, and `:l_ui_header_links` for inline links beside the logo.

```erb
<% content_for :l_ui_header_actions do %>
<%= link_to "Docs", docs_path, class: "l-ui-button l-ui-button--ghost l-ui-button--small" %>
<%= l_ui_theme_toggle %>
<%= l_ui_authentication %>
<%= l_ui_navigation_toggle %>
<% end %>
```

## Authentication

```ruby
Expand Down
17 changes: 17 additions & 0 deletions app/helpers/layered/ui/header_helper.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
module Layered
module Ui
module HeaderHelper
def l_ui_theme_toggle
render "layouts/layered_ui/theme_toggle"
end

def l_ui_authentication
render "layouts/layered_ui/authentication"
end

def l_ui_navigation_toggle
render "layouts/layered_ui/navigation_toggle"
end
end
end
end
9 changes: 9 additions & 0 deletions app/views/layouts/layered_ui/_authentication.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<% unless l_ui_user_signed_in? %>
<% if respond_to?(:new_user_registration_path) %>
<%= link_to "Register", main_app.new_user_registration_path, class: "l-ui-button l-ui-button--outline l-ui-button--small" %>
<% end %>

<% if respond_to?(:new_user_session_path) %>
<%= link_to "Login", main_app.new_user_session_path, class: "l-ui-button l-ui-button--primary l-ui-button--small" %>
<% end %>
<% end %>
44 changes: 8 additions & 36 deletions app/views/layouts/layered_ui/_header.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -14,42 +14,14 @@
<% end %>

<nav class="l-ui-header__navigation" aria-label="Header navigation">
<button
type="button"
data-controller="l-ui--theme"
data-action="click->l-ui--theme#toggle"
data-l-ui--theme-target="button"
class="l-ui-button l-ui-button--icon l-ui-theme-toggle"
aria-label="Toggle dark mode"
aria-pressed="false"
>
<%= image_tag "layered_ui/icon_moon.svg", alt: "", class: "l-ui-icon l-ui-icon--sm l-ui-theme-toggle__icon l-ui-theme-toggle__icon--light", aria: { hidden: true } %>
<%= image_tag "layered_ui/icon_sun.svg", alt: "", class: "l-ui-icon l-ui-icon--sm l-ui-theme-toggle__icon l-ui-theme-toggle__icon--dark", aria: { hidden: true } %>
</button>

<% unless l_ui_user_signed_in? %>
<% if respond_to?(:new_user_registration_path) %>
<%= link_to "Register", main_app.new_user_registration_path, class: "l-ui-button l-ui-button--outline l-ui-button--small" %>
<% end %>

<% if respond_to?(:new_user_session_path) %>
<%= link_to "Login", main_app.new_user_session_path, class: "l-ui-button l-ui-button--primary l-ui-button--small" %>
<% end %>
<% end %>

<% if yield(:l_ui_navigation_items).present? || l_ui_user_signed_in? %>
<button
type="button"
class="l-ui-button l-ui-button--navigation-toggle"
data-action="click->l-ui--navigation#toggle"
data-l-ui--navigation-target="toggleButton"
aria-label="Toggle navigation menu"
aria-expanded="false"
aria-controls="l-ui-navigation"
>
<%= image_tag "layered_ui/icon_hamburger.svg", alt: "", class: "l-ui-icon l-ui-icon--md", data: { "l-ui--navigation-target": "openIcon" }, aria: { hidden: true } %>
<%= image_tag "layered_ui/icon_close.svg", alt: "", class: "l-ui-icon l-ui-icon--md", style: "display: none;", data: { "l-ui--navigation-target": "closeIcon" }, aria: { hidden: true } %>
</button>
<% if content_for?(:l_ui_header_actions) %>
<%= yield(:l_ui_header_actions) %>
<% else %>
<%= yield(:l_ui_header_actions_start) %>
<%= l_ui_theme_toggle %>
<%= l_ui_authentication %>
<%= l_ui_navigation_toggle %>
<%= yield(:l_ui_header_actions_end) %>
<% end %>
</nav>
</div>
Expand Down
14 changes: 14 additions & 0 deletions app/views/layouts/layered_ui/_navigation_toggle.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<% if yield(:l_ui_navigation_items).present? || l_ui_user_signed_in? %>
<button
type="button"
class="l-ui-button l-ui-button--navigation-toggle"
data-action="click->l-ui--navigation#toggle"
data-l-ui--navigation-target="toggleButton"
aria-label="Toggle navigation menu"
aria-expanded="false"
aria-controls="l-ui-navigation"
>
<%= image_tag "layered_ui/icon_hamburger.svg", alt: "", class: "l-ui-icon l-ui-icon--md", data: { "l-ui--navigation-target": "openIcon" }, aria: { hidden: true } %>
<%= image_tag "layered_ui/icon_close.svg", alt: "", class: "l-ui-icon l-ui-icon--md", style: "display: none;", data: { "l-ui--navigation-target": "closeIcon" }, aria: { hidden: true } %>
</button>
<% end %>
12 changes: 12 additions & 0 deletions app/views/layouts/layered_ui/_theme_toggle.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<button
type="button"
data-controller="l-ui--theme"
data-action="click->l-ui--theme#toggle"
data-l-ui--theme-target="button"
class="l-ui-button l-ui-button--icon l-ui-theme-toggle"
aria-label="Toggle dark mode"
aria-pressed="false"
>
<%= image_tag "layered_ui/icon_moon.svg", alt: "", class: "l-ui-icon l-ui-icon--sm l-ui-theme-toggle__icon l-ui-theme-toggle__icon--light", aria: { hidden: true } %>
<%= image_tag "layered_ui/icon_sun.svg", alt: "", class: "l-ui-icon l-ui-icon--sm l-ui-theme-toggle__icon l-ui-theme-toggle__icon--dark", aria: { hidden: true } %>
</button>
1 change: 1 addition & 0 deletions lib/layered/ui/engine.rb
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ class Engine < ::Rails::Engine
helper Layered::Ui::TableHelper
helper Layered::Ui::TitleBarHelper
helper Layered::Ui::FormHelper
helper Layered::Ui::HeaderHelper
helper Layered::Ui::ModalHelper
helper Layered::Ui::RansackHelper
end
Expand Down
9 changes: 9 additions & 0 deletions test/dummy/app/controllers/pages_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,9 @@ def layout_metadata
def layout_navigation
end

def layout_header
end

def layout_panel
end

Expand Down Expand Up @@ -132,6 +135,12 @@ def test_logo_override
def test_head_injection
end

def test_header_actions
end

def test_header_actions_replaced
end

private

def load_users
Expand Down
1 change: 1 addition & 0 deletions test/dummy/app/views/layouts/application.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
<%= l_ui_navigation_item "Head", layout_head_path %>
<%= l_ui_navigation_item "Metadata", layout_metadata_path %>
<%= l_ui_navigation_item "Navigation", layout_navigation_path %>
<%= l_ui_navigation_item "Header", layout_header_path %>
<%= l_ui_navigation_item "Breadcrumbs", layout_breadcrumbs_path %>
<%= l_ui_navigation_item "Panel", layout_panel_path %>
<% end %>
Expand Down
4 changes: 2 additions & 2 deletions test/dummy/app/views/pages/layout.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,8 @@
<td class="l-ui-table__cell">Navigation links, off-canvas by default - use <code>l-ui-body--always-show-navigation</code> to pin as a sidebar on desktop</td>
</tr>
<tr>
<th class="l-ui-table__cell l-ui-table__cell--primary" scope="row">Header</th>
<td class="l-ui-table__cell">Shown by default - use <code>l-ui-body--hide-header</code> to hide it, or <code>l-ui-body--header-contained</code> to constrain its inner row to the page content width (useful for landers). Add inline links with the <code>:l_ui_header_links</code> yield.</td>
<th class="l-ui-table__cell l-ui-table__cell--primary" scope="row"><%= link_to "Header", layout_header_path %></th>
<td class="l-ui-table__cell">Inline links, action slot composition, the default theme/auth/navigation helpers, and hiding the header</td>
</tr>
<tr>
<th class="l-ui-table__cell l-ui-table__cell--primary" scope="row"><%= link_to "Panel", layout_panel_path %></th>
Expand Down
84 changes: 84 additions & 0 deletions test/dummy/app/views/pages/layout_header.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
<%= l_ui_title_bar(
title: "Header",
breadcrumbs: [
["Home", root_path],
["Layout", layout_path]
]
) %>

<p class="mt-4">The header is a fixed 63px bar containing the logo, optional inline links, and an actions group on the right. By default the actions group shows the theme toggle, Devise login/register buttons (when signed out), and the navigation toggle (when navigation items or a signed-in user are present).</p>

<h2 class="mt-8">Inline links</h2>

<p class="mt-4">For landing pages, use the <code>:l_ui_header_links</code> yield to render plain inline links beside the logo. Combine with <code>l-ui-body--header-contained</code> to constrain the header's inner row to the same width as your page content. The links are hidden below the <code>sm</code> breakpoint; pair with <code>:l_ui_navigation_items</code> if you need a mobile menu.</p>

<pre class="l-ui-surface mt-4"><code>&lt;% content_for :l_ui_body_class, "l-ui-body--header-contained" %&gt;

&lt;% content_for :l_ui_header_links do %&gt;
&lt;%= link_to "Features", "#features" %&gt;
&lt;%= link_to "Pricing", "#pricing" %&gt;
&lt;%= link_to "Docs", docs_path %&gt;
&lt;% end %&gt;

&lt;%= render template: "layouts/layered_ui/application" %&gt;</code></pre>

<h2 class="mt-8">Prepend or append actions</h2>

<p class="mt-4">Add items either side of the default actions group with the <code>:l_ui_header_actions_start</code> and <code>:l_ui_header_actions_end</code> yields. The default theme toggle, authentication, and navigation toggle remain in place.</p>

<pre class="l-ui-surface mt-4"><code>&lt;% content_for :l_ui_header_actions_start do %&gt;
&lt;%= link_to "Docs", docs_path, class: "l-ui-button l-ui-button--ghost l-ui-button--small" %&gt;
&lt;% end %&gt;

&lt;% content_for :l_ui_header_actions_end do %&gt;
&lt;%= link_to "Help", help_path, class: "l-ui-button l-ui-button--ghost l-ui-button--small" %&gt;
&lt;% end %&gt;</code></pre>

<h2 class="mt-8">Replace the actions group</h2>

<p class="mt-4">For full control over the action area, set <code>:l_ui_header_actions</code> and compose your own bar using the default building blocks. Any helper you omit will not render, and <code>:l_ui_header_actions_start</code> and <code>:l_ui_header_actions_end</code> are skipped when this yield is set.</p>

<div class="l-ui-table-container mt-4">
<table class="l-ui-table">
<caption class="l-ui-sr-only">Header action helpers</caption>
<thead class="l-ui-table__header">
<tr>
<th class="l-ui-table__header-cell" scope="col">Helper</th>
<th class="l-ui-table__header-cell" scope="col">Renders</th>
</tr>
</thead>
<tbody class="l-ui-table__body">
<tr>
<th class="l-ui-table__cell l-ui-table__cell--primary" scope="row"><code>l_ui_theme_toggle</code></th>
<td class="l-ui-table__cell">Dark/light mode toggle button</td>
</tr>
<tr>
<th class="l-ui-table__cell l-ui-table__cell--primary" scope="row"><code>l_ui_authentication</code></th>
<td class="l-ui-table__cell">Devise login/register buttons (only when signed out and the routes exist)</td>
</tr>
<tr>
<th class="l-ui-table__cell l-ui-table__cell--primary" scope="row"><code>l_ui_navigation_toggle</code></th>
<td class="l-ui-table__cell">Hamburger toggle (only when navigation items exist or a user is signed in)</td>
</tr>
</tbody>
</table>
</div>

<pre class="l-ui-surface mt-4"><code>&lt;% content_for :l_ui_header_actions do %&gt;
&lt;%= link_to "Docs", docs_path, class: "l-ui-button l-ui-button--ghost l-ui-button--small" %&gt;
&lt;%= l_ui_theme_toggle %&gt;
&lt;%= l_ui_authentication %&gt;
&lt;%= l_ui_navigation_toggle %&gt;
&lt;% end %&gt;</code></pre>

<h2 class="mt-8">Hide the header</h2>

<p class="mt-4">Add the <code>l-ui-body--hide-header</code> modifier to the body to hide the header and reclaim its 63px of vertical space.</p>

<pre class="l-ui-surface mt-4"><code>&lt;% content_for :l_ui_body_class, "l-ui-body--hide-header" %&gt;

&lt;%= render template: "layouts/layered_ui/application" %&gt;</code></pre>

<h2 class="mt-8">Override</h2>

<p class="mt-4">For full control, override the partial by creating <code>app/views/layouts/layered_ui/_header.html.erb</code> in your application.</p>
16 changes: 2 additions & 14 deletions test/dummy/app/views/pages/layout_navigation.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

<p class="mt-4">Populate the navigation using the <code>:l_ui_navigation_items</code> yield with <code>l_ui_navigation_item</code> for clickable items and <code>l_ui_navigation_section</code> for grouping.</p>

<p class="mt-4">The navigation is off-canvas by default and toggled via the header button on all screen sizes. Add <code>l-ui-body--always-show-navigation</code> to the body to pin it as a persistent sidebar on desktop. The header is shown by default - use <code>l-ui-body--hide-header</code> to hide it and collapse its reserved space.</p>
<p class="mt-4">The navigation is off-canvas by default and toggled via the header button on all screen sizes. Add <code>l-ui-body--always-show-navigation</code> to the body to pin it as a persistent sidebar on desktop.</p>

<h2 class="mt-6">Example</h2>

Expand Down Expand Up @@ -40,19 +40,7 @@

&lt;%= render template: "layouts/layered_ui/application" %&gt;</code></pre>

<h2 class="mt-6">Header links (landing pages)</h2>

<p class="mt-4">For landing pages, use the <code>:l_ui_header_links</code> yield to render plain inline links in the header (no icons, no sidebar). Combine with <code>l-ui-body--header-contained</code> to constrain the header's inner row to the same width as your page content. The links are hidden below the <code>sm</code> breakpoint; pair with <code>:l_ui_navigation_items</code> if you need a mobile menu.</p>

<pre class="l-ui-surface mt-4"><code>&lt;% content_for :l_ui_body_class, "l-ui-body--header-contained" %&gt;

&lt;% content_for :l_ui_header_links do %&gt;
&lt;%= link_to "Features", "#features" %&gt;
&lt;%= link_to "Pricing", "#pricing" %&gt;
&lt;%= link_to "Docs", docs_path %&gt;
&lt;% end %&gt;

&lt;%= render template: "layouts/layered_ui/application" %&gt;</code></pre>
<p class="mt-4">For inline header links on landing pages, see the <%= link_to "header", layout_header_path %> page.</p>

<h2 class="mt-6">Override</h2>

Expand Down
7 changes: 7 additions & 0 deletions test/dummy/app/views/pages/test_header_actions.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<% content_for :l_ui_header_actions_start do %>
<a href="/docs" class="l-ui-button l-ui-button--ghost l-ui-button--small" data-test="actions-start">Docs</a>
<% end %>

<% content_for :l_ui_header_actions_end do %>
<a href="/help" class="l-ui-button l-ui-button--ghost l-ui-button--small" data-test="actions-end">Help</a>
<% end %>
12 changes: 12 additions & 0 deletions test/dummy/app/views/pages/test_header_actions_replaced.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<% content_for :l_ui_header_actions_start do %>
<a href="/start" data-test="actions-start-suppressed">Start</a>
<% end %>

<% content_for :l_ui_header_actions_end do %>
<a href="/end" data-test="actions-end-suppressed">End</a>
<% end %>

<% content_for :l_ui_header_actions do %>
<a href="/docs" class="l-ui-button l-ui-button--ghost l-ui-button--small" data-test="actions-replaced">Docs</a>
<%= l_ui_theme_toggle %>
<% end %>
3 changes: 3 additions & 0 deletions test/dummy/config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
get "layout", to: "pages#layout"
get "layout_metadata", to: "pages#layout_metadata"
get "layout_navigation", to: "pages#layout_navigation"
get "layout_header", to: "pages#layout_header"
get "layout_panel", to: "pages#layout_panel"
get "layout_logos", to: "pages#layout_logos"
get "layout_icons", to: "pages#layout_icons"
Expand All @@ -42,4 +43,6 @@
get "test_panel_icon_dark_only", to: "pages#test_panel_icon_dark_only"
get "test_logo_override", to: "pages#test_logo_override"
get "test_head_injection", to: "pages#test_head_injection"
get "test_header_actions", to: "pages#test_header_actions"
get "test_header_actions_replaced", to: "pages#test_header_actions_replaced"
end
12 changes: 12 additions & 0 deletions test/helpers/header_helper_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
require "test_helper"

class HeaderHelperTest < ActionView::TestCase
include Layered::Ui::HeaderHelper

test "l_ui_theme_toggle renders the toggle button" do
markup = l_ui_theme_toggle
assert_includes markup, 'data-controller="l-ui--theme"'
assert_includes markup, "l-ui-theme-toggle"
assert_includes markup, 'aria-label="Toggle dark mode"'
end
end
Loading
Loading