Skip to content

Commit 3234fa0

Browse files
Clarify l_ui_head guidance: steer styling to overrides/app stylesheet (#89)
* Clarify l_ui_head guidance: steer styling to overrides/app stylesheet l_ui_head's inline <style> example led host-app LLMs to dump all custom CSS into the head as raw CSS. Reframe the advice (skill, README, dummy docs) so l_ui_head reads as arbitrary head content - scripts, inline page-specific <script>, meta tags, preload hints, stylesheet links - with styling advised toward layered_ui_overrides.css (layered-ui token/ component overrides) or the host app stylesheet. Per-request theming is shown as a Rails-served stylesheet link. Advisory, not prohibitive. * Reframe CSP/Turbo head notes around linked stylesheets The CSP and Turbo sections taught how to make an inline <style nonce> block work, which contradicts steering CSS out of the head. Flip the framing: a linked stylesheet from your own origin is already CSP-clean (style-src 'self', no nonce) and Turbo-cached by URL, so these are now reasons the linked-stylesheet pattern is preferable. Inline-block caveats demoted to an 'if you go inline anyway' footnote. * Move per-tenant CSS annotation out of code block The example used # as a comment inside CSS-looking content, which is not valid CSS comment syntax and would break on copy-paste. Move the token- override note to a prose paragraph so the <pre> holds only valid ERB.
1 parent 3ee706f commit 3234fa0

3 files changed

Lines changed: 54 additions & 41 deletions

File tree

.claude/skills/layered-ui-rails/SKILL.md

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -58,11 +58,13 @@ Populate layout regions with `content_for` (always above the render call):
5858
<p>Panel content here.</p>
5959
<% end %>
6060
61-
<%# Inject into <head> (e.g. per-tenant theming) %>
61+
<%# Inject arbitrary content into <head>: third-party scripts (analytics,
62+
chat widgets), a page-specific inline <script>, meta/verification tags,
63+
preload hints, or a per-request stylesheet link. For styling, the overrides
64+
file is usually a better fit - see note below. %>
6265
<% content_for :l_ui_head do %>
63-
<style nonce="<%= content_security_policy_nonce %>">
64-
:root { --accent: oklch(0.58 0.19 255); }
65-
</style>
66+
<%= javascript_include_tag "https://cdn.example.com/widget.js", defer: true %>
67+
<meta name="google-site-verification" content="...">
6668
<% end %>
6769
6870
<%# Add CSS classes to <body> %>
@@ -101,6 +103,30 @@ Populate layout regions with `content_for` (always above the render call):
101103
<% end %>
102104
```
103105

106+
> `:l_ui_head` injects whatever you like into `<head>`. As a rule of thumb,
107+
> reach for it for head content rather than styling, because styles are easier
108+
> to maintain when they live with the rest of your CSS:
109+
>
110+
> - **layered-ui token or component overrides** (e.g. `--accent`, restyling a
111+
> `.l-ui-*` class) fit best in `app/assets/tailwind/layered_ui_overrides.css`
112+
> - see [Theming](#theming) - so they are part of the Tailwind build and can
113+
> use `@apply` and the design tokens.
114+
> - **Other custom styling** fits in the host app's own application stylesheet,
115+
> like any normal Rails app.
116+
>
117+
> For *per-request* values that cannot be known at build time (e.g. per-tenant
118+
> brand tokens), a good option is to serve them as a stylesheet from a Rails
119+
> controller and link it via `:l_ui_head`:
120+
>
121+
> ```erb
122+
> <% content_for :l_ui_head do %>
123+
> <%= stylesheet_link_tag tenant_theme_path(current_tenant) %>
124+
> <% end %>
125+
> ```
126+
>
127+
> The controller renders CSS that overrides the design tokens (`--accent`,
128+
> etc.) - Turbo- and CSP-friendly, and it keeps styling out of the markup.
129+
104130
Body class modifiers:
105131
- `l-ui-body--always-show-navigation` - pins navigation as a sidebar on desktop
106132
- `l-ui-body--hide-header` - hides the header and collapses its space

README.md

Lines changed: 7 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -115,27 +115,19 @@ All colors are CSS custom properties on `:root` using a two-tier system:
115115
}
116116
```
117117

118-
For dynamic theming (e.g. per-tenant branding), use `content_for :l_ui_head` to inject content into the layout `<head>`:
118+
`content_for :l_ui_head` injects arbitrary content into `<head>` (third-party scripts, a page-specific inline `<script>`, meta tags, preload hints). As a rule of thumb, styles are easier to maintain elsewhere: layered-ui token and component overrides fit in the overrides file above, and other custom styling fits in your app's own application stylesheet, like any normal Rails app.
119+
120+
For *dynamic* theming whose values are only known per request (e.g. per-tenant branding), a good option is to serve the tokens as a stylesheet from a Rails controller and link it via `:l_ui_head` - Turbo- and CSP-friendly, and it keeps styling out of the markup:
119121

120122
```erb
121123
<% content_for :l_ui_head do %>
122-
<style>
123-
:root { --accent: <%= @tenant.accent_color %>; --accent-foreground: oklch(1 0 0); }
124-
</style>
124+
<%= stylesheet_link_tag tenant_theme_path(current_tenant) %>
125125
<% end %>
126126
```
127127

128-
> **Security:** never interpolate user-supplied strings directly into a `<style>` tag - this allows CSS injection (Important: Validate or sanitise any user-derived values before interpolation).
129-
130-
> **CSP compatibility:** inline `<style>` blocks are blocked by a strict `Content-Security-Policy: style-src 'self'` header. If your app enforces a strict CSP, add a nonce to the style tag using Rails' `content_security_policy_nonce` helper - Rails automatically includes the matching nonce in the CSP header:
131-
>
132-
> ```erb
133-
> <% content_for :l_ui_head do %>
134-
> <style nonce="<%= content_security_policy_nonce %>">
135-
> :root { --accent: <%= @tenant.accent_color %>; --accent-foreground: oklch(1 0 0); }
136-
> </style>
137-
> <% end %>
138-
> ```
128+
> **Security:** never interpolate user-supplied strings directly into the served CSS - this allows CSS injection (Important: Validate or sanitise any user-derived values before interpolation).
129+
130+
> **CSP and Turbo:** a stylesheet served from your own origin satisfies a strict `Content-Security-Policy: style-src 'self'` with no nonce, and Turbo caches it by URL - both reasons the linked-stylesheet pattern above is preferable. If you do inject an inline `<style>` block instead, add a nonce with Rails' `content_security_policy_nonce` helper (Rails includes the matching nonce in the CSP header automatically), and note Turbo's preview pass may briefly show stale tokens from a cached snapshot.
139131
140132
See the [Colors documentation](https://layered-ui-rails.layered.ai/layout_colors) for the full list of tokens.
141133

test/dummy/app/views/pages/layout_head.html.erb

Lines changed: 17 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -6,42 +6,37 @@
66
]
77
) %>
88

9-
<p class="mt-4">Use <code>content_for :l_ui_head</code> to inject content into the layout <code>&lt;head&gt;</code>, after the stylesheet. This is the right place for dynamic <code>&lt;style&gt;</code> blocks, preload hints, or other per-request head content.</p>
10-
11-
<h2 class="mt-8">Dynamic theme tokens</h2>
12-
13-
<p class="mt-4">Override CSS custom properties per request (e.g. per-tenant branding) by injecting a <code>&lt;style&gt;</code> block. Placing it after the stylesheet ensures the cascade override takes effect correctly.</p>
9+
<p class="mt-4">Use <code>content_for :l_ui_head</code> to inject arbitrary content into the layout <code>&lt;head&gt;</code>, after the stylesheet. This is the place for third-party scripts (analytics, chat widgets), a page-specific inline <code>&lt;script&gt;</code>, meta and verification tags, preload hints, or a per-request stylesheet link - anything you would normally add to the head of a page. For styling, the overrides file is usually a better fit (see below).</p>
1410

1511
<pre class="l-ui-surface mt-4"><code>&lt;% content_for :l_ui_head do %&gt;
16-
&lt;style&gt;
17-
:root { --accent: &lt;%= @tenant.accent_color %&gt;; --accent-foreground: oklch(1 0 0); }
18-
&lt;/style&gt;
12+
&lt;%= javascript_include_tag "https://cdn.example.com/widget.js", defer: true %&gt;
13+
&lt;meta name="google-site-verification" content="..."&gt;
1914
&lt;% end %&gt;</code></pre>
2015

21-
<p class="l-ui-notice l-ui-notice--warning mt-4"><strong>Security:</strong> never interpolate user-supplied strings directly into a <code>&lt;style&gt;</code> tag - this allows CSS injection. Validate or sanitise any user-derived values before interpolation.</p>
22-
23-
<p class="mt-4">See the <%= link_to "colors", layout_colors_path %> page for the full list of design tokens.</p>
16+
<p class="l-ui-notice mt-4"><strong>Where styles fit best.</strong> layered-ui token and component overrides (e.g. <code>--accent</code>, restyling a <code>.l-ui-*</code> class) fit in <code>app/assets/tailwind/layered_ui_overrides.css</code> - see the <%= link_to "colors", layout_colors_path %> page. Other custom styling fits in your app's own application stylesheet, like any normal Rails app. Both keep styles with the rest of your CSS, where they are easier to maintain than inline in the head.</p>
2417

25-
<h2 class="mt-8">CSP compatibility</h2>
18+
<h2 class="mt-8">Per-tenant theming</h2>
2619

27-
<p class="mt-4">Inline <code>&lt;style&gt;</code> blocks are blocked by a strict <code>Content-Security-Policy: style-src 'self'</code> header. If your app enforces a strict CSP, add a nonce using Rails' <code>content_security_policy_nonce</code> helper - Rails includes the matching nonce in the CSP header automatically:</p>
20+
<p class="mt-4">When brand colors vary per request and cannot be known at build time, a good option is to serve them as a dedicated stylesheet from a Rails controller and link it here. The stylesheet overrides the design tokens, and this approach is Turbo- and CSP-friendly while keeping styling out of the markup.</p>
2821

2922
<pre class="l-ui-surface mt-4"><code>&lt;% content_for :l_ui_head do %&gt;
30-
&lt;style nonce="&lt;%= content_security_policy_nonce %&gt;"&gt;
31-
:root { --accent: &lt;%= @tenant.accent_color %&gt;; --accent-foreground: oklch(1 0 0); }
32-
&lt;/style&gt;
23+
&lt;%= stylesheet_link_tag tenant_theme_path(current_tenant) %&gt;
3324
&lt;% end %&gt;</code></pre>
3425

35-
<h2 class="mt-8">Turbo Drive compatibility</h2>
26+
<p class="mt-4">The controller behind <code>tenant_theme_path</code> renders CSS that overrides the design tokens, for example <code>:root { --accent: &lt;%= @tenant.accent_color %&gt;; --accent-foreground: oklch(1 0 0); }</code>.</p>
27+
28+
<p class="l-ui-notice l-ui-notice--warning mt-4"><strong>Security:</strong> never interpolate user-supplied strings directly into the served CSS - this allows CSS injection. Validate or sanitise any user-derived values before interpolation.</p>
3629

37-
<p class="mt-4">Turbo Drive merges <code>&lt;head&gt;</code> content on each navigation - changed <code>&lt;style&gt;</code> blocks are replaced correctly. The caveat is the preview pass: when Turbo restores a snapshot from its cache before the network response arrives, a cached snapshot may briefly show stale tokens.</p>
30+
<h2 class="mt-8">CSP and Turbo compatibility</h2>
3831

39-
<p class="mt-4">If tokens are stable for a session (e.g. a single-tenant session where branding only changes on login), add <code>data-turbo-track="reload"</code> to the <code>&lt;style&gt;</code> tag. Turbo will trigger a full page reload only when the tag content changes between visits.</p>
32+
<p class="mt-4">The linked-stylesheet pattern above sidesteps two issues that an inline <code>&lt;style&gt;</code> block runs into. A stylesheet served from your own origin satisfies a strict <code>Content-Security-Policy: style-src 'self'</code> with no nonce, and Turbo caches and reuses it by URL - both reasons to prefer it.</p>
4033

41-
<p class="mt-4">If tokens vary per navigation (e.g. a user switches between tenants mid-session), <code>data-turbo-track="reload"</code> will reload on every visit, effectively disabling Turbo. The preferred pattern in that case is to serve the theme as a dedicated stylesheet URL:</p>
34+
<p class="mt-4">If you do inject an inline <code>&lt;style&gt;</code> block instead, two caveats apply. Under a strict CSP, add a nonce with Rails' <code>content_security_policy_nonce</code> helper - Rails includes the matching nonce in the CSP header automatically:</p>
4235

4336
<pre class="l-ui-surface mt-4"><code>&lt;% content_for :l_ui_head do %&gt;
44-
&lt;link rel="stylesheet" href="&lt;%= @tenant.theme_stylesheet_url %&gt;"&gt;
37+
&lt;style nonce="&lt;%= content_security_policy_nonce %&gt;"&gt;
38+
:root { --accent: &lt;%= @tenant.accent_color %&gt;; --accent-foreground: oklch(1 0 0); }
39+
&lt;/style&gt;
4540
&lt;% end %&gt;</code></pre>
4641

47-
<p class="mt-4">Turbo caches and reuses stylesheet responses by URL, so this approach is both Turbo-friendly and avoids inline style blocks entirely.</p>
42+
<p class="mt-4">And on Turbo's preview pass - when it restores a cached snapshot before the network response arrives - an inline block may briefly show stale tokens. If tokens are stable for a session, add <code>data-turbo-track="reload"</code> so Turbo reloads only when the block changes between visits; if they vary per navigation this disables Turbo, so the linked stylesheet is the better fit.</p>

0 commit comments

Comments
 (0)