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
12 changes: 5 additions & 7 deletions .claude/skills/layered-ui-rails/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,10 @@ bin/rails generate layered:ui:install

The generator adds `@import "../builds/tailwind/layered_ui";` to `application.css` (the engine's CSS is served straight from the gem via tailwindcss-rails' engine support), creates a `layered_ui_overrides.css` file for theme customisations, and adds the JS import to `application.js`.

Then render the engine layout from your application layout. Place all `content_for` blocks **above** the render call - the engine layout reads them when it renders, so they must be defined first:
Then render the engine layout from your application layout. Place all `content_for` blocks and `l_ui_add_body_class` calls **above** the render call - the engine layout reads them when it renders, so they must be defined first:

```erb
<% content_for :l_ui_body_class, "l-ui-body--always-show-navigation" %>
<% l_ui_add_body_class "l-ui-body--always-show-navigation" %>

<% content_for :l_ui_navigation_items do %>
<%= l_ui_navigation_item("Dashboard", dashboard_path) %>
Expand Down Expand Up @@ -69,10 +69,8 @@ Populate layout regions with `content_for` (always above the render call):
<meta name="google-site-verification" content="...">
<% end %>

<%# Add CSS classes to <body> %>
<% content_for :l_ui_body_class do %>
l-ui-body--always-show-navigation
<% end %>
<%# Add CSS classes to <body> (call multiple times or pass several; they compose) %>
<% l_ui_add_body_class "l-ui-body--always-show-navigation" %>

<%# Override logos %>
<% content_for :l_ui_logo_light do %>
Expand Down Expand Up @@ -131,7 +129,7 @@ Populate layout regions with `content_for` (always above the render call):

#### Layout modes

The engine layout has two header/navigation modes, both selected through the body class (`content_for :l_ui_body_class`). The default - setting nothing - is a **full-width header with the sidebar shown only on toggle**. Add `l-ui-body--header-contained` for a centred, contained header (pair it with `l-ui-page__contained` to constrain the body width), or `l-ui-body--always-show-navigation` to pin the sidebar open on desktop. The modes compose, and because the layout reads the body class at render time, a page that sets nothing gets the full-width default.
The engine layout has two header/navigation modes, both selected through the body class (`l_ui_add_body_class`). The default - setting nothing - is a **full-width header with the sidebar shown only on toggle**. Add `l-ui-body--header-contained` for a centred, contained header (pair it with `l-ui-page__contained` to constrain the body width), or `l-ui-body--always-show-navigation` to pin the sidebar open on desktop. The modes compose, and because the layout reads the body class at render time, a page that sets nothing gets the full-width default.

The two intended defaults:

Expand Down
2 changes: 1 addition & 1 deletion .claude/skills/layered-ui-rails/references/CSS.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ This gives author-written links inside long-form content (Markdown, prose, ad-ho

## Body modifiers

Applied to `<body>` via the `:l_ui_body_class` yield to toggle layout-level behaviour:
Registered with the `l_ui_add_body_class` helper (call from any template or layout above the engine layout render; calls accumulate) to toggle layout-level behaviour:

```
.l-ui-body--always-show-navigation Pin sidebar navigation open on desktop
Expand Down
16 changes: 16 additions & 0 deletions .claude/skills/layered-ui-rails/references/HELPERS.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,22 @@

All helpers are prefixed `l_ui_` and are available in all views automatically.

## Body modifiers

```ruby
l_ui_add_body_class(*modifiers)
```

Registers one or more body modifier classes (see the [body modifiers](CSS.md) list). Call it from any template or layout **above** the engine layout render; calls accumulate, so a shared layout and an individual page can each contribute without clobbering one another. Pass full class names:

```erb
<% l_ui_add_body_class "l-ui-body--always-show-navigation" %>
<% l_ui_add_body_class "l-ui-body--glass-header", "l-ui-body--flush-top" %>
<% l_ui_add_body_class "l-ui-body--hide-header" if minimal_chrome? %>
```

The resulting classes are deduplicated and space-joined, so repeated modifiers are harmless.

## Navigation

```ruby
Expand Down
4 changes: 2 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ Guidance for AI agents working in this repository.
- **CSS `@apply`:** Multi-line with grouping, following the Prettier Tailwind plugin order: layout → sizing → spacing → typography → backgrounds → borders → effects → transitions → interactivity. Within each group, follow Tailwind's own ordering (not alphabetical). State variants (`hover:`, `focus:`, `active:`, `disabled:`) and responsive prefixes (`sm:`, `md:`, `lg:`) are grouped with their base utility. Single utilities may stay on one line.
- **Generators:** `bin/rails generate layered:ui:install` (import engine CSS via `@import "../builds/tailwind/layered_ui"`, create overrides file, import JS)
- **JS** `app/javascript/layered_ui/`: Stimulus controllers registered as `l-ui--theme`, `l-ui--navigation`, `l-ui--panel`, `l-ui--modal`, `l-ui--tabs`
- **Layout yields** (prefixed `l_ui_`): `:l_ui_navigation_items`, `:l_ui_panel_heading`, `:l_ui_panel_body`, `:l_ui_body_class`
- `:l_ui_body_class` modifiers: `l-ui-body--always-show-navigation` (pins nav as sidebar on desktop), `l-ui-body--hide-header` (hides header and collapses its space)
- **Layout yields** (prefixed `l_ui_`): `:l_ui_navigation_items`, `:l_ui_panel_heading`, `:l_ui_panel_body`
- **Body modifiers:** register via the `l_ui_add_body_class(*modifiers)` helper (calls accumulate, deduped + space-joined). Modifiers: `l-ui-body--always-show-navigation` (pins nav as sidebar on desktop), `l-ui-body--hide-header` (hides header and collapses its space). Legacy `content_for :l_ui_body_class` is still honoured as a temporary bridge but is deprecated.

## Testing

Expand Down
23 changes: 23 additions & 0 deletions app/helpers/layered/ui/body_helper.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
module Layered
module Ui
module BodyHelper
# Register one or more body modifier classes from any template or layout.
# Calls accumulate, so a shared layout and an individual page can each
# contribute without clobbering one another. Pass full class names, e.g.
# l_ui_add_body_class "l-ui-body--hide-header", "l-ui-body--glass-header".
def l_ui_add_body_class(*modifiers)
(@_l_ui_body_classes ||= []).concat(modifiers.flatten.compact)
nil
end

# Render the full <body> class string, including the base l-ui-body class.
# Deduplicates and space-joins, so repeated or multi-token modifiers are safe.
#
# The legacy `content_for :l_ui_body_class` is still honoured as a temporary
# bridge so existing host apps keep working; prefer l_ui_add_body_class.
def l_ui_body_classes
token_list("l-ui-body", *(@_l_ui_body_classes || []), content_for(:l_ui_body_class))
end
end
end
end
2 changes: 1 addition & 1 deletion app/views/layouts/layered_ui/application.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
<%= javascript_importmap_tags %>
</head>

<body class="l-ui-body <%= yield :l_ui_body_class %>" data-controller="l-ui--navigation" data-action="click@window->l-ui--navigation#close keydown.esc@window->l-ui--navigation#close">
<body class="<%= l_ui_body_classes %>" data-controller="l-ui--navigation" data-action="click@window->l-ui--navigation#close keydown.esc@window->l-ui--navigation#close">
<a href="#main-content" class="l-ui-skip-link">Skip to main content</a>
<div id="l-ui-live-region" class="l-ui-sr-only" aria-live="polite" aria-atomic="true"></div>
<%= render "layouts/layered_ui/header" %>
Expand Down
1 change: 1 addition & 0 deletions lib/layered/ui/engine.rb
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ class Engine < ::Rails::Engine
initializer "layered-ui-rails.helpers" do
ActiveSupport.on_load(:action_controller) do
helper Layered::Ui::AuthenticationHelper
helper Layered::Ui::BodyHelper
helper Layered::Ui::BreadcrumbsHelper
helper Layered::Ui::NavigationHelper
helper Layered::Ui::PagyHelper
Expand Down
2 changes: 1 addition & 1 deletion test/dummy/app/views/layouts/application.html.erb
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<% content_for :l_ui_body_class, "l-ui-body--always-show-navigation" %>
<% l_ui_add_body_class "l-ui-body--always-show-navigation" %>

<% unless devise_controller? %>
<% content_for :l_ui_navigation_items do %>
Expand Down
2 changes: 1 addition & 1 deletion test/dummy/app/views/pages/layout.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
<p class="mt-4">Edit your <code>app/views/layouts/application.html.erb</code> with the following content:</p>

<pre class="l-ui-surface mt-4"><code>&lt;!-- Optional: body modifiers --&gt;
&lt;% content_for :l_ui_body_class, "l-ui-body--always-show-navigation l-ui-body--hide-header" %&gt;
&lt;% l_ui_add_body_class "l-ui-body--always-show-navigation", "l-ui-body--hide-header" %&gt;

&lt;!-- Optional: sidebar navigation --&gt;
&lt;% content_for :l_ui_navigation_items do %&gt;
Expand Down
6 changes: 3 additions & 3 deletions test/dummy/app/views/pages/layout_header.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

<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;
<pre class="l-ui-surface mt-4"><code>&lt;% l_ui_add_body_class "l-ui-body--header-contained" %&gt;

&lt;% content_for :l_ui_header_links do %&gt;
&lt;%= link_to "Features", "#features" %&gt;
Expand Down Expand Up @@ -75,7 +75,7 @@

<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;
<pre class="l-ui-surface mt-4"><code>&lt;% l_ui_add_body_class "l-ui-body--hide-header" %&gt;

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

Expand All @@ -85,7 +85,7 @@

<p class="mt-4">To start the page content at very top of the viewport, behind the header, also add <code>l-ui-body--flush-top</code> - it zeroes the page's top gutter so the first section reaches the top edge and shows through a glass header. Give the hero its own internal top padding (at least <code>--header-height</code>) so its content clears the bar.</p>

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

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

Expand Down
4 changes: 2 additions & 2 deletions test/dummy/app/views/pages/layout_navigation.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,9 @@

<h2 class="mt-6">Always-show navigation</h2>

<p class="mt-4">Pin the navigation open on desktop by passing the <code>l-ui-body--always-show-navigation</code> modifier via the <code>:l_ui_body_class</code> yield.</p>
<p class="mt-4">Pin the navigation open on desktop by registering the <code>l-ui-body--always-show-navigation</code> modifier with <code>l_ui_add_body_class</code>.</p>

<pre class="l-ui-surface mt-4"><code>&lt;% content_for :l_ui_body_class, "l-ui-body--always-show-navigation" %&gt;
<pre class="l-ui-surface mt-4"><code>&lt;% l_ui_add_body_class "l-ui-body--always-show-navigation" %&gt;

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

Expand Down
40 changes: 40 additions & 0 deletions test/helpers/body_helper_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
require "test_helper"

class BodyHelperTest < ActionView::TestCase
include Layered::Ui::BodyHelper

test "renders just the base class when nothing is registered" do
assert_equal "l-ui-body", l_ui_body_classes
end

test "registers a single modifier" do
l_ui_add_body_class "l-ui-body--hide-header"
assert_equal "l-ui-body l-ui-body--hide-header", l_ui_body_classes
end

test "accumulates across multiple calls and multiple arguments" do
l_ui_add_body_class "l-ui-body--glass-header", "l-ui-body--flush-top"
l_ui_add_body_class "l-ui-body--always-show-navigation"
assert_equal(
"l-ui-body l-ui-body--glass-header l-ui-body--flush-top l-ui-body--always-show-navigation",
l_ui_body_classes
)
end

test "deduplicates repeated modifiers" do
l_ui_add_body_class "l-ui-body--hide-header"
l_ui_add_body_class "l-ui-body--hide-header"
assert_equal "l-ui-body l-ui-body--hide-header", l_ui_body_classes
end

test "ignores nil arguments" do
l_ui_add_body_class "l-ui-body--hide-header", nil
assert_equal "l-ui-body l-ui-body--hide-header", l_ui_body_classes
end

test "still honours the legacy content_for bridge" do
content_for :l_ui_body_class, "l-ui-body--header-contained"
l_ui_add_body_class "l-ui-body--hide-header"
assert_equal "l-ui-body l-ui-body--hide-header l-ui-body--header-contained", l_ui_body_classes
end
end
Loading