layered-ui-rails 0.18.3 → 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2fd8c671e3944ef7729ed3b06323c413e8e6b514bb41399063abab390216b874
4
- data.tar.gz: fed13e167f82d2765c5152cd86bed58eb424c4e95205111b74fe21cfd5e2dfb0
3
+ metadata.gz: 3300c4fe2f7e3d1fc8175af4985078888212de7a797482ffc12b5d60074b4b09
4
+ data.tar.gz: a56fed6fd7b9013d7c0a2ca988194d5071713e137f1c4022b58e86e205992bf1
5
5
  SHA512:
6
- metadata.gz: 6e78e1b237b6f7319e1de46c4fe569578b1f29c0d271b5d149d6578280991dd5912508c46cbb9c4de8ce2a96886e6494f2b0abcbc1421b1343680cee16770ee2
7
- data.tar.gz: c781405282f383c5f6c4921521e745ce10af3746e6eace6f68ae85b1053faf0ec78d2d5c3e40ca1f483e6fa04cf8d3683346af269c9c919e5fbc3977f74dc8ce
6
+ metadata.gz: 717f5fef1822f58376c54673e9c536be2d1509ee185d152f82e0bcce75e91930b2312d21d106b076ae58a0aff5c7c0f239fe7cff5147987b725dcaa433d773d2
7
+ data.tar.gz: e99903b76852ebf675b7583a71572b0bcd0cad27eb12a9231fc4a9b7ad2b9a4e9d5a3e48970e71713fe780307f6e63fee398a9c25c1c37d94c7d1439abf42f5a
@@ -22,10 +22,10 @@ bin/rails generate layered:ui:install
22
22
 
23
23
  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`.
24
24
 
25
- 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:
25
+ 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:
26
26
 
27
27
  ```erb
28
- <% content_for :l_ui_body_class, "l-ui-body--always-show-navigation" %>
28
+ <% l_ui_add_body_class "l-ui-body--always-show-navigation" %>
29
29
 
30
30
  <% content_for :l_ui_navigation_items do %>
31
31
  <%= l_ui_navigation_item("Dashboard", dashboard_path) %>
@@ -69,10 +69,8 @@ Populate layout regions with `content_for` (always above the render call):
69
69
  <meta name="google-site-verification" content="...">
70
70
  <% end %>
71
71
 
72
- <%# Add CSS classes to <body> %>
73
- <% content_for :l_ui_body_class do %>
74
- l-ui-body--always-show-navigation
75
- <% end %>
72
+ <%# Add CSS classes to <body> (call multiple times or pass several; they compose) %>
73
+ <% l_ui_add_body_class "l-ui-body--always-show-navigation" %>
76
74
 
77
75
  <%# Override logos %>
78
76
  <% content_for :l_ui_logo_light do %>
@@ -129,7 +127,19 @@ Populate layout regions with `content_for` (always above the render call):
129
127
  > The controller renders CSS that overrides the design tokens (`--accent`,
130
128
  > etc.) - Turbo- and CSP-friendly, and it keeps styling out of the markup.
131
129
 
130
+ #### Layout modes
131
+
132
+ 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.
133
+
134
+ The two intended defaults:
135
+
136
+ - **Landing / marketing pages** - contained header and contained page width: `l-ui-body--header-contained` plus `l-ui-page__contained` on the body.
137
+ - **Admin / back-office pages** - full-width with the menu pinned open: `l-ui-body--always-show-navigation`, and no `--header-contained` (full-width is the default).
138
+
139
+ Which pages get which mode (per-section layouts, controllers, etc.) is an application concern, not something the gem dictates.
140
+
132
141
  Body class modifiers:
142
+ - `l-ui-body--header-contained` - centres the header's inner row at `--l-ui-contained-width` (pair with `l-ui-page__contained`); landing pages
133
143
  - `l-ui-body--always-show-navigation` - pins navigation as a sidebar on desktop
134
144
  - `l-ui-body--hide-header` - hides the header and collapses its space
135
145
  - `l-ui-body--glass-header` - glass header (translucent + blur); content scrolls under it
@@ -182,7 +192,10 @@ Key components:
182
192
  |---|---|
183
193
  | Page layout | `.l-ui-page`, `--with-navigation`, `__vertically-centered`, `__narrow` (narrow ~384px column, md:max-w-sm), `__contained` (wide column capped at `--l-ui-contained-width`) |
184
194
  | Buttons | `.l-ui-button`, `--primary`, `--outline`, `--outline-danger`, `--full`, `--icon` |
195
+ | Hero | `.l-ui-hero` (pair with `.l-ui-bleed`), `__inner`, `__title`, `__title-accent`, `__subtitle`, `__actions`, `__media`, `__media-img` |
185
196
  | Surfaces | `.l-ui-surface`, `--highlighted`, `--sm`, `--collapsible`, `--collapsible-highlighted` |
197
+ | Cards | `.l-ui-card`, `--gradient`, `__eyebrow`, `__title`, `__icon`, `__body` |
198
+ | Tints | `.l-ui-tint-1` … `.l-ui-tint-5` (shared decorative palette; sets the `--l-ui-tint-*` role variables) |
186
199
  | Forms | `.l-ui-form`, `.l-ui-form__group`, `.l-ui-form__field`, `.l-ui-label`, `.l-ui-select` |
187
200
  | Tables | `.l-ui-table`, `.l-ui-table__header`, `.l-ui-table__cell`, `--primary`, `--action`, `.l-ui-table__action`, `--danger` |
188
201
  | Badges | `.l-ui-badge`, `--rounded`, `--default`, `--success`, `--warning`, `--danger` |
@@ -19,7 +19,7 @@ This gives author-written links inside long-form content (Markdown, prose, ad-ho
19
19
 
20
20
  ## Body modifiers
21
21
 
22
- Applied to `<body>` via the `:l_ui_body_class` yield to toggle layout-level behaviour:
22
+ 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:
23
23
 
24
24
  ```
25
25
  .l-ui-body--always-show-navigation Pin sidebar navigation open on desktop
@@ -47,6 +47,16 @@ The page gutter (horizontal and bottom padding) is the `--l-ui-gutter` custom pr
47
47
 
48
48
  `.l-ui-page` is a flex column (`flex flex-1 flex-col`) and uses `overflow-x-clip` to hold its content to the available width: intrinsically wide content (tables, code blocks, long unbroken strings) is clipped rather than expanding the page or adding a horizontal page scrollbar. So make such content scroll internally (e.g. wrap a table in an `overflow-x-auto` element) instead of expecting the page to grow.
49
49
 
50
+ ## Typography
51
+
52
+ Base elements (`h1`-`h4`, `p`, `ul`/`ol`, `code`, etc.) are styled via `@layer base` so the host app can override them. Headings set type scale and weight only - they carry **no** divider by default, so the heading level can be chosen for document structure alone (e.g. an `h2` inside a panel header or hero, without an unwanted rule).
53
+
54
+ ```
55
+ .l-ui-heading--section Opt-in bottom divider; add to any heading to separate a section from the content above it
56
+ ```
57
+
58
+ Add `l-ui-heading--section` wherever a visual section rule is wanted (the section headings on the documentation pages use it). It applies to whichever heading element you already have, so it does not change the heading level.
59
+
50
60
  ## Buttons
51
61
 
52
62
  Always combine the `l-ui-button` base class with a colour modifier (e.g. `l-ui-button l-ui-button--primary`):
@@ -73,6 +83,73 @@ Any button variant is automatically styled as disabled when the `disabled` HTML
73
83
 
74
84
  For destructive actions use `l-ui-button--danger` (solid) or `l-ui-button--outline-danger` (bordered).
75
85
 
86
+ ## Hero
87
+
88
+ Full-bleed marketing hero for the top of a landing page. Add `l-ui-bleed` to the section to break out of the page gutter, and pair the body with `l-ui-body--glass-header` + `l-ui-body--flush-top` so it sits flush to the viewport top with optional media showing through the glass header (see Page layout above):
89
+
90
+ ```
91
+ .l-ui-hero Full-bleed hero section (pair with l-ui-bleed); ships with a default background
92
+ .l-ui-hero__inner Content column, capped at the contained width with the page gutter
93
+ .l-ui-hero__title Large, tight-tracked hero heading
94
+ .l-ui-hero__title-accent Gradient-clipped accent span for part of the title
95
+ .l-ui-hero__subtitle Supporting subtitle paragraph below the title
96
+ .l-ui-hero__actions Row of call-to-action buttons or links
97
+ .l-ui-hero__media Optional decorative background media layer
98
+ .l-ui-hero__media--light/--dark Theme-specific media; only the active theme's variant shows
99
+ .l-ui-hero__media-img Image inside a media layer (covers, right-aligned)
100
+ ```
101
+
102
+ The `__inner` picks up the fixed-header offset automatically under `l-ui-body--flush-top`. A token-driven overlay over the media keeps text legible and fades the hero into the page background below.
103
+
104
+ A good-looking default background ships with the gem and is on by default, driven by the `--l-ui-hero-image` token (light/dark). Re-skin it by overriding `--l-ui-hero-image` in the overrides file (global) or inline on one section (per-hero); set it to `none` to remove it. For per-page art with proper light/dark `<img>` loading, add a `l-ui-hero__media` picture instead - it paints above the token background.
105
+
106
+ ## Tint palette
107
+
108
+ A decorative, **control-agnostic** colour palette, separate from the Tier 1 brand `--accent`. `--accent` is the single interactive/emphasis colour (primary buttons, active tab/nav); the tints are a set of categorical decorative colours (cards, tags, sections). Both use `-foreground` for "the readable text/icon colour on this colour", so they read as siblings: `--accent` / `--accent-foreground` vs `--l-ui-tint-1-…` / `--l-ui-tint-1-foreground`.
109
+
110
+ ```
111
+ .l-ui-tint-1 … .l-ui-tint-5 Select a palette slot (defaults: teal, green, amber, rose, purple)
112
+ ```
113
+
114
+ Each `l-ui-tint-N` class only sets four "current tint" role variables, which any control reads:
115
+
116
+ ```
117
+ --l-ui-tint-border Border / outline tone
118
+ --l-ui-tint-foreground Strong text / icon tone
119
+ --l-ui-tint-from Gradient fill start
120
+ --l-ui-tint-to Gradient fill end
121
+ ```
122
+
123
+ The defaults live in `@layer base` as `--l-ui-tint-N-border` / `-foreground` / `-from` / `-to` (light and dark). **Slot numbers are palette positions, not fixed hues**, so re-skinning a slot (override the `--l-ui-tint-N-*` tokens in the overrides file) never makes a class name misleading. The tints are independent of `--accent`; to colour a control with the brand accent instead, point the role variables at `var(--accent)` inline - no special class needed. Apply `l-ui-tint-N` to any control that reads the role variables (the card below is one consumer).
124
+
125
+ ## Card
126
+
127
+ A content card in two composable styles. The base `l-ui-card` is bordered; add `l-ui-card--gradient` for a soft tint fill behind the same border. Colour comes from a separate `l-ui-tint-N` slot (see Tint palette above), so style and colour compose independently (`l-ui-card l-ui-card--gradient l-ui-tint-1`):
128
+
129
+ ```
130
+ .l-ui-card Base card (bordered, on the surface background)
131
+ .l-ui-card--gradient Adds the 135° tint fill behind the border
132
+ .l-ui-card__eyebrow Small uppercase label above the title
133
+ .l-ui-card__title Card heading, coloured by the active tint
134
+ .l-ui-card__icon Optional title icon, painted in the tint colour (set --l-ui-icon-src inline)
135
+ .l-ui-card__body Body copy in the muted foreground colour
136
+ ```
137
+
138
+ The card reads the shared `--l-ui-tint-*` role variables, falling back to the neutral `--border`/`--foreground` when no tint slot is present (so a plain `l-ui-card` is a neutral bordered card). The gradient modifier reads `--l-ui-tint-from`/`-to`. Set the role variables inline (or point them at `var(--accent)`, which is already theme-aware) for a one-off colour - but note that fixed inline values are theme-fixed: for a custom colour that should adapt to dark mode, supply light and dark values via a `.dark` rule (or `prefers-color-scheme`) rather than inline. `__icon` uses the same mask technique as the primary-button icon, so set `--l-ui-icon-src` at the call site.
139
+
140
+ ## Logo block
141
+
142
+ A responsive strip of brand or partner logos (a "trusted by" / "works with" row). Logos are normalised to a common height and flattened to one tone (black in light, white in dark) so a mismatched set reads as a tidy row. Wraps three-up on small screens and spreads to a single row from `md` up:
143
+
144
+ ```
145
+ .l-ui-logo-block Strip container; wraps three-up on small screens, single row from md
146
+ .l-ui-logo-block__item Wrapper for one logo (give it role="listitem")
147
+ .l-ui-logo-block__logo The logo image; normalised height, flattened to the foreground tone
148
+ .l-ui-logo-block__logo--wordmark Taller variant for wide name lockups
149
+ ```
150
+
151
+ Mark the container `role="list"` with an `aria-label`, each item `role="listitem"`, and keep meaningful `alt` text on every logo. Supply normal full-colour logos - the tone is applied with a CSS filter, so source colours are ignored.
152
+
76
153
  ## Surfaces
77
154
 
78
155
  Always combine the `l-ui-surface` base class with any modifiers (e.g. `l-ui-surface l-ui-surface--highlighted`):
@@ -2,6 +2,22 @@
2
2
 
3
3
  All helpers are prefixed `l_ui_` and are available in all views automatically.
4
4
 
5
+ ## Body modifiers
6
+
7
+ ```ruby
8
+ l_ui_add_body_class(*modifiers)
9
+ ```
10
+
11
+ 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:
12
+
13
+ ```erb
14
+ <% l_ui_add_body_class "l-ui-body--always-show-navigation" %>
15
+ <% l_ui_add_body_class "l-ui-body--glass-header", "l-ui-body--flush-top" %>
16
+ <% l_ui_add_body_class "l-ui-body--hide-header" if minimal_chrome? %>
17
+ ```
18
+
19
+ The resulting classes are deduplicated and space-joined, so repeated modifiers are harmless.
20
+
5
21
  ## Navigation
6
22
 
7
23
  ```ruby
data/AGENTS.md CHANGED
@@ -10,8 +10,8 @@ Guidance for AI agents working in this repository.
10
10
  - **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.
11
11
  - **Generators:** `bin/rails generate layered:ui:install` (import engine CSS via `@import "../builds/tailwind/layered_ui"`, create overrides file, import JS)
12
12
  - **JS** `app/javascript/layered_ui/`: Stimulus controllers registered as `l-ui--theme`, `l-ui--navigation`, `l-ui--panel`, `l-ui--modal`, `l-ui--tabs`
13
- - **Layout yields** (prefixed `l_ui_`): `:l_ui_navigation_items`, `:l_ui_panel_heading`, `:l_ui_panel_body`, `:l_ui_body_class`
14
- - `: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)
13
+ - **Layout yields** (prefixed `l_ui_`): `:l_ui_navigation_items`, `:l_ui_panel_heading`, `:l_ui_panel_body`
14
+ - **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.
15
15
 
16
16
  ## Testing
17
17
 
data/CHANGELOG.md CHANGED
@@ -2,6 +2,31 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file. This project follows [Semantic Versioning](https://semver.org/).
4
4
 
5
+ ## [0.19.0] - 2026-06-15
6
+
7
+ ### Added
8
+
9
+ - `l-ui-hero`: a full-bleed marketing hero for the top of a landing page, with `__inner`, `__title`, `__title-accent` (gradient-clipped emphasis span), `__subtitle`, and `__actions`. Pair the section with `l-ui-bleed` and the body with `l-ui-body--glass-header` + `l-ui-body--flush-top` to sit flush to the viewport top behind the glass header. Ships with a default light/dark background driven by the `--l-ui-hero-image` token (override globally, per-hero inline, or set to `none`); supply per-page art with the optional `l-ui-hero__media`/`__media-img` layer (`--light`/`--dark` variants for theme-aware `<img>` loading).
10
+ - `l-ui-card`: a content card in two composable styles - the base bordered card plus `l-ui-card--gradient` for a soft tint fill behind the same border - with `__eyebrow`, `__title`, `__icon` (masked, set `--l-ui-icon-src` inline), and `__body`. Colour comes from a separate tint slot, so style and colour compose independently; a plain `l-ui-card` falls back to neutral `--border`/`--foreground`.
11
+ - Decorative tint palette: `l-ui-tint-1` … `l-ui-tint-5` set the shared `--l-ui-tint-border`/`-foreground`/`-from`/`-to` role variables that tint-aware controls (such as the card) read. The palette is categorical and independent of the brand `--accent`; slot numbers are palette positions, not fixed hues, and each slot's `--l-ui-tint-N-*` tokens can be re-skinned in the overrides file (light and dark).
12
+ - `l-ui-logo-block`: a responsive "trusted by" / "works with" strip with `__item`, `__logo`, and `__logo--wordmark`. Logos are normalised to a common height and flattened to a single tone (black in light, white in dark); wraps three-up on small screens and spreads to one row from `md` up.
13
+ - `l-ui-heading--section`: an opt-in bottom divider that can be added to any heading to separate a section from the content above it, without changing the heading level.
14
+ - The overrides generator now scaffolds commented-out `--l-ui-hero-image` and `--l-ui-tint-*` token blocks (light and dark) so hosts can re-skin the hero background and tint palette. Pass `--force` to `layered:ui:create_overrides` to regenerate an existing overrides file and pick up the new scaffolding (this overwrites the file, so back up your customisations first).
15
+
16
+ ### Breaking
17
+
18
+ - Headings (`h1`-`h4`) now set type scale and weight only and carry no divider by default. Previously `h2` rendered a bottom border; the divider is now opt-in via `l-ui-heading--section`. This lets the heading level be chosen for document structure alone (e.g. an `h2` inside a panel header or hero) without an unwanted rule. Add `l-ui-heading--section` to any `h2` that should keep its previous divider. See `UPGRADING.md`.
19
+
20
+ ## [0.18.4] - 2026-06-14
21
+
22
+ ### Added
23
+
24
+ - `l_ui_add_body_class(*modifiers)` helper for registering `<body>` modifier classes. Calls accumulate across templates and layouts, so a shared layout and an individual page can each contribute modifiers without clobbering one another; the result is deduplicated and space-joined.
25
+
26
+ ### Changed
27
+
28
+ - The engine layout now builds its `<body>` class via `l_ui_add_body_class` instead of `content_for :l_ui_body_class`. The legacy `content_for :l_ui_body_class` is still honoured as a temporary bridge but is deprecated; migrate to `l_ui_add_body_class`.
29
+
5
30
  ## [0.18.3] - 2026-06-08
6
31
 
7
32
  ### Added
@@ -46,10 +71,13 @@ All notable changes to this project will be documented in this file. This projec
46
71
  - `--l-ui-gutter` custom property (default `1rem`) now drives the page's horizontal and bottom padding, shared with `.l-ui-header` so the two always align. Override it on a container to retune the gutter in one place.
47
72
  - `.l-ui-bleed` utility to take a child element edge-to-edge by cancelling the current page gutter (e.g. a full-bleed hero), with no negative-margin guesswork.
48
73
 
74
+ ### Breaking
75
+
76
+ - `l-ui-page__width-constrained` renamed to `l-ui-page__narrow`. The old name read as a general page-width container, but it is a narrow ~384px column for any compact, centred content (the auth pages are one use). See `UPGRADING.md`.
77
+ - Install flow: the engine's CSS is now served directly from the gem using [tailwindcss-rails' engine support](https://github.com/rails/tailwindcss-rails#rails-engines-support-experimental) instead of being copied into the host app. The install generator no longer creates `app/assets/tailwind/layered_ui.css`; instead it adds `@import "../builds/tailwind/layered_ui";` to your `application.css`. The CSS now stays in sync with the installed gem version automatically - no need to re-run the generator after upgrading.
78
+
49
79
  ### Changed
50
80
 
51
- - **Breaking:** `l-ui-page__width-constrained` renamed to `l-ui-page__narrow`. The old name read as a general page-width container, but it is a narrow ~384px column for any compact, centred content (the auth pages are one use). See `UPGRADING.md`.
52
- - **Breaking (install flow):** the engine's CSS is now served directly from the gem using [tailwindcss-rails' engine support](https://github.com/rails/tailwindcss-rails#rails-engines-support-experimental) instead of being copied into the host app. The install generator no longer creates `app/assets/tailwind/layered_ui.css`; instead it adds `@import "../builds/tailwind/layered_ui";` to your `application.css`. The CSS now stays in sync with the installed gem version automatically - no need to re-run the generator after upgrading.
53
81
  - The engine layout now links the compiled Tailwind build explicitly (`stylesheet_link_tag "tailwind"`) rather than the `:app` bundle, matching tailwindcss-rails' own convention and avoiding a stray link to the engine's intermediate build file.
54
82
  - Moved the engine's source stylesheet from `app/assets/tailwind/layered/ui/styles.css` to `app/assets/tailwind/layered_ui/engine.css` (the path tailwindcss-rails' engine support expects).
55
83
 
data/README.md CHANGED
@@ -86,10 +86,10 @@ To let AI coding agents work with `layered-ui-rails` in your project, install th
86
86
  bin/rails generate layered:ui:install_agent_skill
87
87
  ```
88
88
 
89
- Then update your application layout to render the engine layout. Place any `content_for` blocks **above** the render call - the engine layout reads them when it renders, so they must be defined first:
89
+ Then update your application layout to render the engine layout. Place any `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:
90
90
 
91
91
  ```erb
92
- <% content_for :l_ui_body_class, "l-ui-body--always-show-navigation" %>
92
+ <% l_ui_add_body_class "l-ui-body--always-show-navigation" %>
93
93
 
94
94
  <% content_for :l_ui_navigation_items do %>
95
95
  <%= l_ui_navigation_item "Home", root_path %>
@@ -79,6 +79,30 @@
79
79
  --warning-text: oklch(0.3625 0.0732 93.12);
80
80
  --error-bg: oklch(0.748 0.1306 20.64);
81
81
  --error-text: oklch(0.2248 0.0874 28.11);
82
+ /* Decorative tint palette (categorical colours, independent of --accent; consumed via the
83
+ .l-ui-tint-* role variables). Slot numbers are palette positions, not fixed hues. */
84
+ --l-ui-tint-1-border: oklch(0.6 0.1 195);
85
+ --l-ui-tint-1-foreground: oklch(0.45 0.1 195);
86
+ --l-ui-tint-1-from: oklch(0.95 0.04 195);
87
+ --l-ui-tint-1-to: oklch(0.88 0.07 195);
88
+ --l-ui-tint-2-border: oklch(0.55 0.14 145);
89
+ --l-ui-tint-2-foreground: oklch(0.45 0.14 145);
90
+ --l-ui-tint-2-from: oklch(0.95 0.05 145);
91
+ --l-ui-tint-2-to: oklch(0.88 0.09 145);
92
+ --l-ui-tint-3-border: oklch(0.65 0.14 65);
93
+ --l-ui-tint-3-foreground: oklch(0.5 0.14 60);
94
+ --l-ui-tint-3-from: oklch(0.95 0.05 65);
95
+ --l-ui-tint-3-to: oklch(0.88 0.09 65);
96
+ --l-ui-tint-4-border: oklch(0.6 0.16 25);
97
+ --l-ui-tint-4-foreground: oklch(0.5 0.18 25);
98
+ --l-ui-tint-4-from: oklch(0.95 0.05 25);
99
+ --l-ui-tint-4-to: oklch(0.88 0.09 25);
100
+ --l-ui-tint-5-border: oklch(0.6 0.15 290);
101
+ --l-ui-tint-5-foreground: oklch(0.45 0.17 290);
102
+ --l-ui-tint-5-from: oklch(0.95 0.05 290);
103
+ --l-ui-tint-5-to: oklch(0.88 0.09 290);
104
+ /* Default hero background; override here, per-hero inline, or set to `none`. */
105
+ --l-ui-hero-image: url('layered_ui/hero_background_light.webp');
82
106
  --header-height: 63px;
83
107
  --l-ui-gutter: 1rem;
84
108
  --l-ui-contained-width: 80rem;
@@ -111,6 +135,28 @@
111
135
  --warning-text: oklch(0.9336 0.1 95.79);
112
136
  --error-bg: oklch(0.2248 0.0874 28.11);
113
137
  --error-text: oklch(0.748 0.1306 20.64);
138
+ /* Decorative tint palette (dark) */
139
+ --l-ui-tint-1-border: oklch(0.5 0.1 195);
140
+ --l-ui-tint-1-foreground: oklch(0.78 0.11 195);
141
+ --l-ui-tint-1-from: oklch(0.28 0.05 195);
142
+ --l-ui-tint-1-to: oklch(0.22 0.06 195);
143
+ --l-ui-tint-2-border: oklch(0.5 0.13 145);
144
+ --l-ui-tint-2-foreground: oklch(0.78 0.13 145);
145
+ --l-ui-tint-2-from: oklch(0.28 0.06 145);
146
+ --l-ui-tint-2-to: oklch(0.22 0.07 145);
147
+ --l-ui-tint-3-border: oklch(0.55 0.13 60);
148
+ --l-ui-tint-3-foreground: oklch(0.78 0.14 60);
149
+ --l-ui-tint-3-from: oklch(0.28 0.06 60);
150
+ --l-ui-tint-3-to: oklch(0.22 0.07 60);
151
+ --l-ui-tint-4-border: oklch(0.5 0.16 25);
152
+ --l-ui-tint-4-foreground: oklch(0.74 0.17 25);
153
+ --l-ui-tint-4-from: oklch(0.28 0.07 25);
154
+ --l-ui-tint-4-to: oklch(0.22 0.08 25);
155
+ --l-ui-tint-5-border: oklch(0.55 0.15 290);
156
+ --l-ui-tint-5-foreground: oklch(0.78 0.15 290);
157
+ --l-ui-tint-5-from: oklch(0.28 0.06 290);
158
+ --l-ui-tint-5-to: oklch(0.22 0.07 290);
159
+ --l-ui-hero-image: url('layered_ui/hero_background_dark.webp');
114
160
  }
115
161
 
116
162
  /* Typography */
@@ -120,8 +166,7 @@
120
166
  }
121
167
 
122
168
  h2 {
123
- @apply heading text-lg
124
- border-b border-border pb-2;
169
+ @apply heading text-lg;
125
170
  }
126
171
 
127
172
  h3 {
@@ -163,6 +208,15 @@
163
208
  }
164
209
  }
165
210
 
211
+ /* Headings */
212
+
213
+ /* Opt-in section divider for any heading. Headings carry no border by default so the level can be
214
+ chosen for document structure alone (e.g. an h2 inside a panel header or hero); add this where a
215
+ visual section rule is wanted. */
216
+ .l-ui-heading--section {
217
+ @apply border-b border-border pb-2;
218
+ }
219
+
166
220
  /* Theme */
167
221
 
168
222
  @theme {
@@ -505,6 +559,272 @@
505
559
  padding-inline: max(0px, calc((100% - var(--l-ui-contained-width)) / 2 + var(--l-ui-gutter)));
506
560
  }
507
561
 
562
+ /* Hero */
563
+
564
+ /* Full-bleed marketing hero. Pair the section with .l-ui-bleed, and (for a top-of-page hero)
565
+ the body with .l-ui-body--glass-header + .l-ui-body--flush-top so it sits flush to the
566
+ viewport top and the background shows through the glass header.
567
+
568
+ Ships with a default background via the --l-ui-hero-image token (light/dark, set in @layer
569
+ base). Painted on the element itself so it sits below the overlays and needs no markup. Override
570
+ it globally in the overrides file, per-hero with an inline `--l-ui-hero-image`, or set it to
571
+ `none` to drop the background. For per-page art with proper light/dark <img> loading, add a
572
+ .l-ui-hero__media element instead (it paints above this background). */
573
+ .l-ui-hero {
574
+ @apply relative isolate overflow-hidden
575
+ bg-background bg-cover bg-right bg-no-repeat;
576
+ background-image: var(--l-ui-hero-image);
577
+ }
578
+
579
+ /* Optional per-hero media layer; overrides the default background. Provide a --light and --dark
580
+ variant and the active theme's is shown. */
581
+ .l-ui-hero__media {
582
+ @apply absolute inset-0 z-[-3]
583
+ block w-full h-full
584
+ pointer-events-none;
585
+ }
586
+
587
+ .l-ui-hero__media-img {
588
+ @apply w-full h-full
589
+ object-cover object-right;
590
+ }
591
+
592
+ .l-ui-hero__media--dark {
593
+ @apply hidden;
594
+ }
595
+
596
+ .dark .l-ui-hero__media--light {
597
+ @apply hidden;
598
+ }
599
+
600
+ .dark .l-ui-hero__media--dark {
601
+ @apply block;
602
+ }
603
+
604
+ /* Left-to-right overlay keeps text legible over media; fades out so the media stays visible
605
+ on the right. Token-driven, so it adapts to the active theme. */
606
+ .l-ui-hero::before {
607
+ content: "";
608
+ @apply absolute inset-0 z-[-2]
609
+ pointer-events-none;
610
+ background: linear-gradient(
611
+ 100deg,
612
+ oklch(from var(--background) l c h / 0.85) 0%,
613
+ oklch(from var(--background) l c h / 0.6) 45%,
614
+ oklch(from var(--background) l c h / 0.15) 80%,
615
+ oklch(from var(--background) l c h / 0) 100%
616
+ );
617
+ }
618
+
619
+ /* Bottom fade blends the hero into the page background below it. */
620
+ .l-ui-hero::after {
621
+ content: "";
622
+ @apply absolute inset-0 z-[-1]
623
+ pointer-events-none;
624
+ background: linear-gradient(
625
+ to bottom,
626
+ transparent 0%,
627
+ oklch(from var(--background) l c h / 0.15) 40%,
628
+ oklch(from var(--background) l c h / 0.5) 75%,
629
+ var(--background) 100%
630
+ );
631
+ }
632
+
633
+ /* Content column, mirroring the contained page width and gutter. */
634
+ .l-ui-hero__inner {
635
+ @apply w-full max-w-[var(--l-ui-contained-width)]
636
+ mx-auto
637
+ px-[var(--l-ui-gutter)] py-[clamp(2rem,4vw,3.5rem)]
638
+ text-foreground;
639
+ }
640
+
641
+ /* When the hero sits flush at the viewport top, push the content clear of the fixed header. */
642
+ .l-ui-body--flush-top .l-ui-hero__inner {
643
+ @apply pt-[calc(var(--header-height)+clamp(2rem,4vw,3.5rem))];
644
+ }
645
+
646
+ .l-ui-hero__title {
647
+ @apply max-w-2xl
648
+ my-5
649
+ font-bold;
650
+ font-size: clamp(1.9rem, 6vw, 3.5rem);
651
+ line-height: 1.15;
652
+ letter-spacing: -0.02em;
653
+ }
654
+
655
+ /* Gradient-clipped accent for emphasising part of the title. */
656
+ .l-ui-hero__title-accent {
657
+ @apply inline-block
658
+ pb-[0.15em];
659
+ background: linear-gradient(
660
+ 95deg,
661
+ oklch(0.55 0.14 195) 0%,
662
+ oklch(0.6 0.16 60) 50%,
663
+ oklch(0.55 0.2 25) 100%
664
+ );
665
+ -webkit-background-clip: text;
666
+ background-clip: text;
667
+ color: transparent;
668
+ }
669
+
670
+ .dark .l-ui-hero__title-accent {
671
+ background: linear-gradient(
672
+ 95deg,
673
+ oklch(0.82 0.11 195) 0%,
674
+ oklch(0.78 0.14 60) 50%,
675
+ oklch(0.68 0.18 25) 100%
676
+ );
677
+ -webkit-background-clip: text;
678
+ background-clip: text;
679
+ color: transparent;
680
+ }
681
+
682
+ .l-ui-hero__subtitle {
683
+ @apply max-w-2xl
684
+ text-foreground-muted;
685
+ font-size: clamp(1rem, 1.6vw, 1.2rem);
686
+ line-height: 1.55;
687
+ }
688
+
689
+ .l-ui-hero__actions {
690
+ @apply flex flex-wrap
691
+ max-w-2xl
692
+ gap-4
693
+ mt-7;
694
+ }
695
+
696
+ /* Tint palette */
697
+
698
+ /* Decorative, control-agnostic colour slots. Each .l-ui-tint-N class selects one palette entry
699
+ (defined in @layer base, light + dark) and exposes it through four "current tint" role variables
700
+ that any control reads: --l-ui-tint-border, --l-ui-tint-foreground, --l-ui-tint-from,
701
+ --l-ui-tint-to. These are decorative and independent of the Tier-1 brand --accent; to tint a
702
+ control with the brand accent instead, point these variables at var(--accent) inline. Slot numbers
703
+ are palette positions, not fixed hues, so re-skinning a slot (via the --l-ui-tint-N-* tokens in
704
+ the overrides file) never makes a class name misleading. */
705
+ .l-ui-tint-1 {
706
+ --l-ui-tint-border: var(--l-ui-tint-1-border);
707
+ --l-ui-tint-foreground: var(--l-ui-tint-1-foreground);
708
+ --l-ui-tint-from: var(--l-ui-tint-1-from);
709
+ --l-ui-tint-to: var(--l-ui-tint-1-to);
710
+ }
711
+
712
+ .l-ui-tint-2 {
713
+ --l-ui-tint-border: var(--l-ui-tint-2-border);
714
+ --l-ui-tint-foreground: var(--l-ui-tint-2-foreground);
715
+ --l-ui-tint-from: var(--l-ui-tint-2-from);
716
+ --l-ui-tint-to: var(--l-ui-tint-2-to);
717
+ }
718
+
719
+ .l-ui-tint-3 {
720
+ --l-ui-tint-border: var(--l-ui-tint-3-border);
721
+ --l-ui-tint-foreground: var(--l-ui-tint-3-foreground);
722
+ --l-ui-tint-from: var(--l-ui-tint-3-from);
723
+ --l-ui-tint-to: var(--l-ui-tint-3-to);
724
+ }
725
+
726
+ .l-ui-tint-4 {
727
+ --l-ui-tint-border: var(--l-ui-tint-4-border);
728
+ --l-ui-tint-foreground: var(--l-ui-tint-4-foreground);
729
+ --l-ui-tint-from: var(--l-ui-tint-4-from);
730
+ --l-ui-tint-to: var(--l-ui-tint-4-to);
731
+ }
732
+
733
+ .l-ui-tint-5 {
734
+ --l-ui-tint-border: var(--l-ui-tint-5-border);
735
+ --l-ui-tint-foreground: var(--l-ui-tint-5-foreground);
736
+ --l-ui-tint-from: var(--l-ui-tint-5-from);
737
+ --l-ui-tint-to: var(--l-ui-tint-5-to);
738
+ }
739
+
740
+ /* Card */
741
+
742
+ /* A bordered or gradient-filled content card. The base card is bordered; add --gradient for the
743
+ tint fill. Colour comes from a separate, control-agnostic tint slot (l-ui-tint-1..5), so style and
744
+ colour compose independently:
745
+
746
+ <div class="l-ui-card l-ui-tint-1"> bordered, tint 1
747
+ <div class="l-ui-card l-ui-card--gradient l-ui-tint-1"> gradient, tint 1
748
+
749
+ The card reads the shared --l-ui-tint-* role variables, falling back to the neutral --border and
750
+ --foreground tokens when no tint slot is present. Set the role variables (or point them at
751
+ var(--accent)) inline for a one-off colour. */
752
+ .l-ui-card {
753
+ @apply p-4
754
+ rounded-sm;
755
+ background-color: var(--surface);
756
+ border: 1px solid var(--l-ui-tint-border, var(--border));
757
+ }
758
+
759
+ /* Tint fill; from/to come from the active tint slot (fall back to the plain surface with no slot). */
760
+ .l-ui-card--gradient {
761
+ background: linear-gradient(
762
+ 135deg,
763
+ var(--l-ui-tint-from, var(--surface)) 0%,
764
+ var(--l-ui-tint-to, var(--surface)) 100%
765
+ );
766
+ }
767
+
768
+ .l-ui-card__eyebrow {
769
+ @apply mt-0 mb-1.5
770
+ text-xs font-semibold uppercase tracking-[0.12em] text-foreground-muted;
771
+ }
772
+
773
+ .l-ui-card__title {
774
+ @apply inline-flex items-center gap-2
775
+ mt-0 mb-1.5
776
+ text-lg font-semibold tracking-[-0.01em];
777
+ color: var(--l-ui-tint-foreground, var(--foreground));
778
+ }
779
+
780
+ /* Painted from the tint foreground colour via a mask (matching the primary-button icon), so it
781
+ adapts per theme without an invert. The icon shape comes from --l-ui-icon-src, set at the call
782
+ site. */
783
+ .l-ui-card__icon {
784
+ @apply inline-block shrink-0
785
+ w-5 h-5;
786
+ background-color: var(--l-ui-tint-foreground, var(--foreground));
787
+ -webkit-mask: var(--l-ui-icon-src) no-repeat center / contain;
788
+ mask: var(--l-ui-icon-src) no-repeat center / contain;
789
+ }
790
+
791
+ .l-ui-card__body {
792
+ @apply mt-0
793
+ text-sm leading-relaxed text-foreground-muted;
794
+ }
795
+
796
+ /* Logo block */
797
+
798
+ /* A responsive strip of brand or partner logos, normalised to a common height and flattened to a
799
+ single tone (black in light, white in dark) so a mismatched set of source logos reads as one
800
+ tidy row. Wraps three-up on small screens and spreads to a single row from md up. Use the
801
+ --wordmark modifier on wide name lockups that need more height than icon-style marks. */
802
+ .l-ui-logo-block {
803
+ @apply flex flex-wrap items-center justify-center
804
+ gap-x-5 gap-y-6
805
+ md:flex-nowrap md:justify-between md:gap-4;
806
+ }
807
+
808
+ .l-ui-logo-block__item {
809
+ @apply flex shrink-0 grow-0 justify-center
810
+ min-w-0 basis-[calc(33.333%-1rem)]
811
+ md:block md:shrink md:basis-auto;
812
+ }
813
+
814
+ .l-ui-logo-block__logo {
815
+ @apply w-auto max-w-full;
816
+ height: clamp(1.1rem, 2vw, 2rem);
817
+ filter: brightness(0);
818
+ }
819
+
820
+ .l-ui-logo-block__logo--wordmark {
821
+ height: clamp(1.85rem, 3.3vw, 3.3rem);
822
+ }
823
+
824
+ .dark .l-ui-logo-block__logo {
825
+ filter: brightness(0) invert(1);
826
+ }
827
+
508
828
  /* Header */
509
829
 
510
830
  /* The container owns the gutter (mirroring .l-ui-page), so .l-ui-header fills the gutter-inset
@@ -0,0 +1,23 @@
1
+ module Layered
2
+ module Ui
3
+ module BodyHelper
4
+ # Register one or more body modifier classes from any template or layout.
5
+ # Calls accumulate, so a shared layout and an individual page can each
6
+ # contribute without clobbering one another. Pass full class names, e.g.
7
+ # l_ui_add_body_class "l-ui-body--hide-header", "l-ui-body--glass-header".
8
+ def l_ui_add_body_class(*modifiers)
9
+ (@_l_ui_body_classes ||= []).concat(modifiers.flatten.compact)
10
+ nil
11
+ end
12
+
13
+ # Render the full <body> class string, including the base l-ui-body class.
14
+ # Deduplicates and space-joins, so repeated or multi-token modifiers are safe.
15
+ #
16
+ # The legacy `content_for :l_ui_body_class` is still honoured as a temporary
17
+ # bridge so existing host apps keep working; prefer l_ui_add_body_class.
18
+ def l_ui_body_classes
19
+ token_list("l-ui-body", *(@_l_ui_body_classes || []), content_for(:l_ui_body_class))
20
+ end
21
+ end
22
+ end
23
+ end
@@ -11,7 +11,7 @@
11
11
  <% end %>
12
12
 
13
13
  <%- if devise_mapping.recoverable? || devise_mapping.confirmable? || (devise_mapping.lockable? && resource_class.unlock_strategy_enabled?(:email)) %>
14
- <h2 class="l-ui-mt-6">
14
+ <h2 class="l-ui-mt-6 l-ui-heading--section">
15
15
  Need assistance?
16
16
  </h2>
17
17
  <% end %>
@@ -1,6 +1,6 @@
1
1
  <% if item.errors.any? %>
2
2
  <div id="error_explanation" class="l-ui-form__errors" role="alert">
3
- <h2>
3
+ <h2 class="l-ui-heading--section">
4
4
  <%= pluralize(item.errors.count, "error") %> found:
5
5
  </h2>
6
6
 
@@ -25,7 +25,7 @@
25
25
  <%= javascript_importmap_tags %>
26
26
  </head>
27
27
 
28
- <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">
28
+ <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">
29
29
  <a href="#main-content" class="l-ui-skip-link">Skip to main content</a>
30
30
  <div id="l-ui-live-region" class="l-ui-sr-only" aria-live="polite" aria-atomic="true"></div>
31
31
  <%= render "layouts/layered_ui/header" %>
@@ -6,13 +6,16 @@ module Layered
6
6
 
7
7
  OVERRIDES_PATH = "app/assets/tailwind/layered_ui_overrides.css"
8
8
 
9
+ class_option :force, type: :boolean, default: false,
10
+ desc: "Regenerate the overrides file even if it exists (overwrites your customisations - back it up first)"
11
+
9
12
  def create_overrides_file
10
- if File.exist?(OVERRIDES_PATH)
11
- say "Overrides file already exists at #{OVERRIDES_PATH}, skipping", :yellow
13
+ if File.exist?(OVERRIDES_PATH) && !options[:force]
14
+ say "Overrides file already exists at #{OVERRIDES_PATH}, skipping (pass --force to regenerate)", :yellow
12
15
  return
13
16
  end
14
17
 
15
- create_file OVERRIDES_PATH, overrides_content
18
+ create_file OVERRIDES_PATH, overrides_content, force: options[:force]
16
19
  end
17
20
 
18
21
  private
@@ -126,6 +129,50 @@ module Layered
126
129
  @apply h-7.5 w-auto;
127
130
  }
128
131
  */
132
+
133
+ /*
134
+ * Hero background. A default ships with the gem; re-skin it here
135
+ * (or set it to `none` to remove it). Set --l-ui-hero-image inline
136
+ * on a single hero to give different heroes different art.
137
+ */
138
+
139
+ /*
140
+ :root {
141
+ --l-ui-hero-image: url('my_hero_light.webp');
142
+ }
143
+
144
+ .dark {
145
+ --l-ui-hero-image: url('my_hero_dark.webp');
146
+ }
147
+ */
148
+
149
+ /*
150
+ * Decorative tint palette. The tint slots (l-ui-tint-1..5) are a
151
+ * shared, control-agnostic palette - used by cards and any other
152
+ * control that reads the --l-ui-tint-* role variables. They are
153
+ * independent of the Tier 1 brand --accent above. Slot numbers are
154
+ * palette positions, not fixed hues, so re-skinning a slot never
155
+ * makes a class name misleading. Each slot has four tokens (border,
156
+ * foreground, gradient from/to) for light and dark; override only
157
+ * the ones you need. (You can also set the role variables inline on
158
+ * a single element, or point them at var(--accent), for a one-off.)
159
+ */
160
+
161
+ /*
162
+ :root {
163
+ --l-ui-tint-1-border: oklch(0.6 0.1 195);
164
+ --l-ui-tint-1-foreground: oklch(0.45 0.1 195);
165
+ --l-ui-tint-1-from: oklch(0.95 0.04 195);
166
+ --l-ui-tint-1-to: oklch(0.88 0.07 195);
167
+ }
168
+
169
+ .dark {
170
+ --l-ui-tint-1-border: oklch(0.5 0.1 195);
171
+ --l-ui-tint-1-foreground: oklch(0.78 0.11 195);
172
+ --l-ui-tint-1-from: oklch(0.28 0.05 195);
173
+ --l-ui-tint-1-to: oklch(0.22 0.06 195);
174
+ }
175
+ */
129
176
  CSS
130
177
  end
131
178
  end
@@ -27,6 +27,7 @@ module Layered
27
27
  initializer "layered-ui-rails.helpers" do
28
28
  ActiveSupport.on_load(:action_controller) do
29
29
  helper Layered::Ui::AuthenticationHelper
30
+ helper Layered::Ui::BodyHelper
30
31
  helper Layered::Ui::BreadcrumbsHelper
31
32
  helper Layered::Ui::NavigationHelper
32
33
  helper Layered::Ui::PagyHelper
@@ -1,5 +1,5 @@
1
1
  module Layered
2
2
  module Ui
3
- VERSION = "0.18.3"
3
+ VERSION = "0.19.0"
4
4
  end
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: layered-ui-rails
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.18.3
4
+ version: 0.19.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - layered.ai
@@ -188,6 +188,8 @@ files:
188
188
  - app/assets/fonts/layered_ui/inter.woff2
189
189
  - app/assets/fonts/layered_ui/manrope.woff2
190
190
  - app/assets/images/layered_ui/apple_touch_icon.png
191
+ - app/assets/images/layered_ui/hero_background_dark.webp
192
+ - app/assets/images/layered_ui/hero_background_light.webp
191
193
  - app/assets/images/layered_ui/icon_chevron_down.svg
192
194
  - app/assets/images/layered_ui/icon_chevron_right.svg
193
195
  - app/assets/images/layered_ui/icon_close.svg
@@ -211,6 +213,7 @@ files:
211
213
  - app/assets/images/layered_ui/panel_icon_light.svg
212
214
  - app/assets/tailwind/layered_ui/engine.css
213
215
  - app/helpers/layered/ui/authentication_helper.rb
216
+ - app/helpers/layered/ui/body_helper.rb
214
217
  - app/helpers/layered/ui/breadcrumbs_helper.rb
215
218
  - app/helpers/layered/ui/form_helper.rb
216
219
  - app/helpers/layered/ui/header_helper.rb