layered-ui-rails 0.16.1 → 0.18.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.
Files changed (31) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/skills/layered-ui-rails/SKILL.md +62 -9
  3. data/.claude/skills/layered-ui-rails/references/CSS.md +8 -1
  4. data/.claude/skills/layered-ui-rails/references/HELPERS.md +26 -2
  5. data/AGENTS.md +2 -2
  6. data/CHANGELOG.md +36 -0
  7. data/README.md +16 -18
  8. data/Rakefile +6 -0
  9. data/app/assets/tailwind/{layered/ui/styles.css → layered_ui/engine.css} +24 -5
  10. data/app/helpers/layered/ui/form_helper.rb +12 -4
  11. data/app/helpers/layered/ui/header_helper.rb +17 -0
  12. data/app/views/devise/confirmations/new.html.erb +1 -1
  13. data/app/views/devise/passwords/edit.html.erb +1 -1
  14. data/app/views/devise/passwords/new.html.erb +1 -1
  15. data/app/views/devise/registrations/new.html.erb +1 -1
  16. data/app/views/devise/sessions/new.html.erb +1 -1
  17. data/app/views/devise/unlocks/new.html.erb +1 -1
  18. data/app/views/layered/ui/managed_resource/_field_input.html.erb +14 -4
  19. data/app/views/layered/ui/managed_resource/_form.html.erb +2 -0
  20. data/app/views/layouts/layered_ui/_authentication.html.erb +9 -0
  21. data/app/views/layouts/layered_ui/_header.html.erb +8 -36
  22. data/app/views/layouts/layered_ui/_navigation_toggle.html.erb +14 -0
  23. data/app/views/layouts/layered_ui/_panel.html.erb +1 -1
  24. data/app/views/layouts/layered_ui/_theme_toggle.html.erb +12 -0
  25. data/app/views/layouts/layered_ui/application.html.erb +1 -1
  26. data/lib/generators/layered/ui/import_css_generator.rb +1 -1
  27. data/lib/generators/layered/ui/install_generator.rb +0 -4
  28. data/lib/layered/ui/engine.rb +1 -0
  29. data/lib/layered/ui/version.rb +1 -1
  30. metadata +16 -6
  31. data/lib/generators/layered/ui/copy_assets_generator.rb +0 -30
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8c6db00c8644f6d77f57044ddb722d85679aa1a212dcf97e343235dedc059aa6
4
- data.tar.gz: 427a18c3eddf0676e5c9c6b92b13ac651c01a8d73b964a4d5c4ea5c683102cdc
3
+ metadata.gz: 45a2361df4b5409267f1619b3ba0de294c2e90a56e610492e9587dc40c9a79be
4
+ data.tar.gz: e812a5155ed72c2d0c9fe6668a74215fd4600decf3326d08190a76936b43de2f
5
5
  SHA512:
6
- metadata.gz: c543aa25cc392198de7350249cc6e448d84513af39b3ca5e42f0e94058a94aa014fd05ceb8eaed7dc3427c88c791f704b86101bc7636c23ac9fdf42d5ad5c142
7
- data.tar.gz: '003035092eee2d1a1aa9994829da39e532cf067f6a8248478d09a3ec5167cb09835fae97c7c808bc00087da9f4a2a643bdd8d7c600e4afe374ed3195be3f8ec3'
6
+ metadata.gz: ef9f4e14b361fcc2a6d3d4db9e4629a97533d24d479f3fba123daa58278d0a53be18c1cd3526ed4a70cf4ef9fd7cf65a243a303249bb8eb81bfa2c7c55efbb1e
7
+ data.tar.gz: d1f369d793b03173d8ba0c44616b4452841f07391c846744738b426f5c6f7af87ffd85250ffabf1a1248f59da74947a4df32288c5c63c63232ea97f8efb63885
@@ -20,7 +20,7 @@ bundle add layered-ui-rails
20
20
  bin/rails generate layered:ui:install
21
21
  ```
22
22
 
23
- The generator copies `layered_ui.css` into `app/assets/tailwind/`, adds the CSS import to `application.css`, and adds the JS import to `application.js`.
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
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:
26
26
 
@@ -39,6 +39,8 @@ Then render the engine layout from your application layout. Place all `content_f
39
39
 
40
40
  The engine layout provides a fixed header (63px), optional sidebar navigation (256px wide), optional resizable panel (320px default), and a main content area. Dark mode is built in with a toggle and localStorage persistence.
41
41
 
42
+ **Your view's `yield` content is already wrapped in `.l-ui-page`** by the engine layout (which applies the responsive padding and `--with-navigation` margin). Do not add your own `.l-ui-page` wrapper around your view content - it duplicates the container and nests two `.l-ui-page` elements. Start your view with its actual content (headings, sections, components) directly.
43
+
42
44
  ### Content blocks
43
45
 
44
46
  Populate layout regions with `content_for` (always above the render call):
@@ -58,11 +60,13 @@ Populate layout regions with `content_for` (always above the render call):
58
60
  <p>Panel content here.</p>
59
61
  <% end %>
60
62
 
61
- <%# Inject into <head> (e.g. per-tenant theming) %>
63
+ <%# Inject arbitrary content into <head>: third-party scripts (analytics,
64
+ chat widgets), a page-specific inline <script>, meta/verification tags,
65
+ preload hints, or a per-request stylesheet link. For styling, the overrides
66
+ file is usually a better fit - see note below. %>
62
67
  <% content_for :l_ui_head do %>
63
- <style nonce="<%= content_security_policy_nonce %>">
64
- :root { --accent: oklch(0.58 0.19 255); }
65
- </style>
68
+ <%= javascript_include_tag "https://cdn.example.com/widget.js", defer: true %>
69
+ <meta name="google-site-verification" content="...">
66
70
  <% end %>
67
71
 
68
72
  <%# Add CSS classes to <body> %>
@@ -77,8 +81,54 @@ Populate layout regions with `content_for` (always above the render call):
77
81
  <% content_for :l_ui_logo_dark do %>
78
82
  <%= image_tag "my_logo_dark.svg", alt: "", class: "l-ui-header__logo l-ui-header__logo--dark" %>
79
83
  <% end %>
84
+
85
+ <%# Prepend or append items to the header actions group %>
86
+ <% content_for :l_ui_header_actions_start do %>
87
+ <%= link_to "Docs", docs_path, class: "l-ui-button l-ui-button--ghost l-ui-button--small" %>
88
+ <% end %>
89
+ <% content_for :l_ui_header_actions_end do %>
90
+ <%= link_to "Help", help_path, class: "l-ui-button l-ui-button--ghost l-ui-button--small" %>
91
+ <% end %>
92
+
93
+ <%# Or replace the default actions group entirely and compose with helpers %>
94
+ <% content_for :l_ui_header_actions do %>
95
+ <%= link_to "Docs", docs_path, class: "l-ui-button l-ui-button--ghost l-ui-button--small" %>
96
+ <%= l_ui_theme_toggle %>
97
+ <%= l_ui_authentication %>
98
+ <%= l_ui_navigation_toggle %>
99
+ <% end %>
100
+
101
+ <%# Inline header links (alongside the logo) %>
102
+ <% content_for :l_ui_header_links do %>
103
+ <%= link_to "Pricing", pricing_path %>
104
+ <%= link_to "About", about_path %>
105
+ <% end %>
80
106
  ```
81
107
 
108
+ > `:l_ui_head` injects whatever you like into `<head>`. As a rule of thumb,
109
+ > reach for it for head content rather than styling, because styles are easier
110
+ > to maintain when they live with the rest of your CSS:
111
+ >
112
+ > - **layered-ui token or component overrides** (e.g. `--accent`, restyling a
113
+ > `.l-ui-*` class) fit best in `app/assets/tailwind/layered_ui_overrides.css`
114
+ > - see [Theming](#theming) - so they are part of the Tailwind build and can
115
+ > use `@apply` and the design tokens.
116
+ > - **Other custom styling** fits in the host app's own application stylesheet,
117
+ > like any normal Rails app.
118
+ >
119
+ > For *per-request* values that cannot be known at build time (e.g. per-tenant
120
+ > brand tokens), a good option is to serve them as a stylesheet from a Rails
121
+ > controller and link it via `:l_ui_head`:
122
+ >
123
+ > ```erb
124
+ > <% content_for :l_ui_head do %>
125
+ > <%= stylesheet_link_tag tenant_theme_path(current_tenant) %>
126
+ > <% end %>
127
+ > ```
128
+ >
129
+ > The controller renders CSS that overrides the design tokens (`--accent`,
130
+ > etc.) - Turbo- and CSP-friendly, and it keeps styling out of the markup.
131
+
82
132
  Body class modifiers:
83
133
  - `l-ui-body--always-show-navigation` - pins navigation as a sidebar on desktop
84
134
  - `l-ui-body--hide-header` - hides the header and collapses its space
@@ -116,6 +166,9 @@ Quick reference:
116
166
  | `l_ui_normalise_field(record, config)` | Normalise a raw field config hash into canonical form |
117
167
  | `l_ui_user_signed_in?` | Check if user is authenticated |
118
168
  | `l_ui_current_user` | Current user object |
169
+ | `l_ui_theme_toggle` | Default header theme toggle button |
170
+ | `l_ui_authentication` | Default header login/register buttons (Devise) |
171
+ | `l_ui_navigation_toggle` | Default header sidebar toggle button |
119
172
 
120
173
  ## CSS classes
121
174
 
@@ -125,7 +178,7 @@ Key components:
125
178
 
126
179
  | Component | Key classes |
127
180
  |---|---|
128
- | Page layout | `.l-ui-page`, `--with-navigation`, `__vertically-centered`, `__width-constrained` |
181
+ | Page layout | `.l-ui-page`, `--with-navigation`, `__vertically-centered`, `__narrow` (narrow ~384px column, md:max-w-sm) |
129
182
  | Buttons | `.l-ui-button`, `--primary`, `--outline`, `--outline-danger`, `--full`, `--icon` |
130
183
  | Surfaces | `.l-ui-surface`, `--highlighted`, `--sm`, `--collapsible`, `--collapsible-highlighted` |
131
184
  | Forms | `.l-ui-form`, `.l-ui-form__group`, `.l-ui-form__field`, `.l-ui-label`, `.l-ui-select` |
@@ -156,7 +209,7 @@ All controllers use the `l-ui--` namespace and are auto-registered via importmap
156
209
  Override CSS custom properties after the engine import. Values are full CSS colors - `oklch()` is recommended for perceptually uniform mixing and consistent contrast, but `#hex`, `rgb()`, and keywords also work. A converter such as https://oklch.com/ can help translate from hex/rgb.
157
210
 
158
211
  ```css
159
- @import "./layered_ui";
212
+ @import "../builds/tailwind/layered_ui";
160
213
 
161
214
  :root {
162
215
  --accent: oklch(0.58 0.19 255);
@@ -192,8 +245,8 @@ Layered::Ui.current_user_method = :current_member # default: :current_user
192
245
 
193
246
  ## Common issues
194
247
 
195
- - **Tailwind classes not generated** - The host app's Tailwind build only sees classes in the host app's templates. Use `l-ui-` classes (which are in the copied CSS) rather than raw Tailwind utilities when styling engine-provided patterns.
196
- - **Missing styles** - Ensure `@import "./layered_ui";` is in `app/assets/tailwind/application.css`.
248
+ - **Tailwind classes not generated** - The host app's Tailwind build only sees classes in the host app's templates. Use `l-ui-` classes (which are defined in the engine CSS) rather than raw Tailwind utilities when styling engine-provided patterns.
249
+ - **Missing styles** - Ensure `@import "../builds/tailwind/layered_ui";` is in `app/assets/tailwind/application.css`. The import resolves to a file generated by tailwindcss-rails' engine support; it is created automatically by `tailwindcss:build`/`watch` (and `assets:precompile`), so run a build (e.g. `bin/dev`) if the file is missing.
197
250
  - **Missing JS controllers** - Ensure `import "layered_ui"` is in `app/javascript/application.js`.
198
251
 
199
252
  ## Further reference
@@ -33,9 +33,16 @@ Applied to `<body>` via the `:l_ui_body_class` yield to toggle layout-level beha
33
33
  .l-ui-page Main content wrapper with responsive padding
34
34
  .l-ui-page--with-navigation Left margin for sidebar on desktop
35
35
  .l-ui-page__vertically-centered Centred layout element (e.g. login pages)
36
- .l-ui-page__width-constrained Max-width container element
36
+ .l-ui-page__narrow Narrow column (md:max-w-sm, ~384px) for any compact, centred content
37
+ .l-ui-bleed Breaks a child out to the page edge (full-bleed hero etc.)
37
38
  ```
38
39
 
40
+ `.l-ui-page` (and `--with-navigation`) is applied by the engine layout around your view's `yield` - you do not add it yourself. Wrapping your view content in another `.l-ui-page` nests two containers. The `__vertically-centered` and `__narrow` elements are used *inside* views for centred, compact content (the Devise auth pages are one example, but they are general-purpose). `__narrow` caps width at `md:max-w-sm` (~384px). For a wider content area, add your own max-width wrapper in the host app - `.l-ui-page` already holds content to the available width.
41
+
42
+ The page gutter (horizontal and bottom padding) is the `--l-ui-gutter` custom property (default `1rem`), shared by `.l-ui-page` and `.l-ui-header` so they always align. Override it on a container to change the gutter in one place rather than reverse-engineering padding values. To take a single child edge-to-edge (a full-bleed hero, a banner), add `.l-ui-bleed` to it - it cancels exactly the current gutter, so no negative-margin guesswork. Note `.l-ui-bleed` only handles the horizontal edges; content still sits below the page's top padding (header offset + gutter).
43
+
44
+ `.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.
45
+
39
46
  ## Buttons
40
47
 
41
48
  Always combine the `l-ui-button` base class with a colour modifier (e.g. `l-ui-button l-ui-button--primary`):
@@ -208,7 +208,7 @@ Formats a date/time value as `"%-d %b %Y, %H:%M"` (e.g. "15 Apr 2026, 10:30"). R
208
208
  ## Form
209
209
 
210
210
  ```ruby
211
- l_ui_form(record, fields:, url:, method: nil, submit: nil)
211
+ l_ui_form(record, fields:, url:, method: nil, submit: nil, multipart: nil)
212
212
  ```
213
213
 
214
214
  - `record` (ActiveRecord) - the model instance
@@ -216,16 +216,21 @@ l_ui_form(record, fields:, url:, method: nil, submit: nil)
216
216
  - `url` (String) - form action URL
217
217
  - `method` (Symbol, optional) - HTTP method override
218
218
  - `submit` (String, optional) - submit button text; defaults to "Create" for new records and "Save" for persisted records
219
+ - `multipart` (Boolean, optional) - override multipart encoding. Defaults to auto-detection: `true` when any field is `:file`, otherwise `false`. Pass `true`/`false` to force.
220
+
221
+ If you need control beyond these options, override the partial in your host app by creating `app/views/layered/ui/managed_resource/_form.html.erb` (Rails view lookup prefers the host's copy), or write the form directly with `form_with` and reuse `layered_ui/shared/form_errors`, `layered_ui/shared/label`, and `layered_ui/shared/field_error`.
219
222
 
220
223
  Renders a complete form with all fields, error summary, and submit button via the `layered/ui/managed_resource/form` partial.
221
224
 
222
225
  Field options:
223
226
  - `attribute` (Symbol) - model attribute
224
- - `as` (Symbol, optional) - field type; auto-detected from column type. Supported: `:string`, `:text`, `:email`, `:password`, `:number`, `:date`, `:datetime`, `:select`, `:checkbox`, `:hidden`
227
+ - `as` (Symbol, optional) - field type; auto-detected from column type. Supported: `:string`, `:text`, `:email`, `:password`, `:number`, `:tel`, `:url`, `:search`, `:date`, `:datetime`, `:time`, `:month`, `:week`, `:color`, `:range`, `:file`, `:select`, `:checkbox`, `:hidden`. Forms automatically become `multipart` when any field is `:file`.
225
228
  - `label` (String, optional) - custom label text; defaults to humanised attribute
226
229
  - `required` (Boolean, optional) - marks field as required; default false
227
230
  - `hint` (String, optional) - help text below the field
228
231
  - `collection` (Array, optional) - required for `:select` type; e.g. `[['Label', value], ...]`
232
+ - `include_blank` (Boolean or String, optional) - for `:select` fields; defaults to `true`. Pass a string to use as the blank option's label, or `false` to omit it. Suppressed when `prompt:` is set
233
+ - `prompt` (String, optional) - for `:select` fields; prompt text shown as the first option, only selectable when no value is set
229
234
  - `placeholder` (String, optional) - input placeholder text
230
235
 
231
236
  ```erb
@@ -287,6 +292,25 @@ The button does not need to be inside the helper's wrapper, and no `data-control
287
292
 
288
293
  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.
289
294
 
295
+ ## Header
296
+
297
+ ```ruby
298
+ l_ui_theme_toggle # Renders the dark/light theme toggle button
299
+ l_ui_authentication # Renders Devise login/register buttons (no-op when signed in or when Devise routes are absent)
300
+ l_ui_navigation_toggle # Renders the hamburger that toggles the sidebar (only when navigation items exist or a user is signed in)
301
+ ```
302
+
303
+ 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.
304
+
305
+ ```erb
306
+ <% content_for :l_ui_header_actions do %>
307
+ <%= link_to "Docs", docs_path, class: "l-ui-button l-ui-button--ghost l-ui-button--small" %>
308
+ <%= l_ui_theme_toggle %>
309
+ <%= l_ui_authentication %>
310
+ <%= l_ui_navigation_toggle %>
311
+ <% end %>
312
+ ```
313
+
290
314
  ## Authentication
291
315
 
292
316
  ```ruby
data/AGENTS.md CHANGED
@@ -6,9 +6,9 @@ Guidance for AI agents working in this repository.
6
6
 
7
7
  - **Entry:** `require "layered-ui-rails"` → `lib/layered/ui.rb` → `lib/layered/ui/engine.rb`
8
8
  - **Engine:** importmap, assets, Pagy helpers when present; helpers: `AuthenticationHelper`, `NavigationHelper`, `PagyHelper`
9
- - **CSS** `app/assets/tailwind/layered/ui/styles.css`: OKLCH tokens, `.dark` on `<html>`, `@theme` utilities (`bg-background`, etc.), BEM components (`.l-ui-button--primary`, etc.). Layout: 63px header, 256px sidebar, 320px panel. WCAG 2.2 AA.
9
+ - **CSS** `app/assets/tailwind/layered_ui/engine.css` (the tailwindcss-rails engine entry point): OKLCH tokens, `.dark` on `<html>`, `@theme` utilities (`bg-background`, etc.), BEM components (`.l-ui-button--primary`, etc.). Layout: 63px header, 256px sidebar, 320px panel. WCAG 2.2 AA.
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
- - **Generators:** `bin/rails generate layered:ui:install` (copy CSS, import CSS, import JS)
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
13
  - **Layout yields** (prefixed `l_ui_`): `:l_ui_navigation_items`, `:l_ui_panel_heading`, `:l_ui_panel_body`, `:l_ui_body_class`
14
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)
data/CHANGELOG.md CHANGED
@@ -2,6 +2,42 @@
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.18.0] - 2026-06-07
6
+
7
+ ### Fixed
8
+
9
+ - Primary-button icons (e.g. the panel hide button) are now painted from the `--button-primary-icon` token via a CSS mask instead of a binary `invert` filter. `invert` could only ever produce black or white, so accents with a non-white foreground rendered the wrong colour and had to be patched out downstream with `filter: none`. Any accent foreground now works in both themes; the downstream override can be removed.
10
+
11
+ ### Added
12
+
13
+ - `--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.
14
+ - `.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.
15
+
16
+ ### Changed
17
+
18
+ - **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`.
19
+ - **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.
20
+ - 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.
21
+ - 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).
22
+
23
+ ### Removed
24
+
25
+ - `CopyAssetsGenerator` (`layered:ui:copy_assets`), which copied the engine CSS into the host app. It is replaced by the engine-support import above.
26
+
27
+ ### Migration
28
+
29
+ Re-run `bin/rails generate layered:ui:install` (it adds the new import without duplicating existing ones), then delete the now-unused copied file at `app/assets/tailwind/layered_ui.css` and remove its `@import "./layered_ui";` line from `application.css`. Your `layered_ui_overrides.css` and any customisations are unaffected.
30
+
31
+ ## [0.17.0] - 2026-05-19
32
+
33
+ ### Added
34
+
35
+ - `header_actions` helper for inserting custom action buttons into the layout header, alongside the existing authentication/theme/navigation toggles
36
+ - Header partial split into `_authentication`, `_navigation_toggle`, and `_theme_toggle` sub-partials
37
+ - Expanded `l_ui_form` field types: `tel`, `url`, `search`, `time`, `month`, `week`, `color`, `range`, and `file`
38
+ - `multipart:` option on `l_ui_form` to override the form's multipart encoding (auto-detected from `:file` fields otherwise)
39
+ - `prompt:` field option on `:select` fields in `l_ui_form`, shown as the first option and only selectable when no value is set; suppresses the default blank option
40
+
5
41
  ## [0.16.1] - 2026-05-16
6
42
 
7
43
  ### Fixed
data/README.md CHANGED
@@ -76,10 +76,16 @@ bin/rails generate layered:ui:install
76
76
  ```
77
77
 
78
78
  The install generator will:
79
- - Copy `layered_ui.css` to `app/assets/tailwind/`
80
- - Add `@import "./layered_ui";` to your `application.css`
79
+ - Add `@import "../builds/tailwind/layered_ui";` to your `application.css` (the engine's CSS is served straight from the gem via [tailwindcss-rails' engine support](https://github.com/rails/tailwindcss-rails#rails-engines-support-experimental), so it stays in sync when you upgrade)
80
+ - Create `app/assets/tailwind/layered_ui_overrides.css` for your theme customisations (never overwritten on re-install)
81
81
  - Add `import "layered_ui"` to your `application.js`
82
82
 
83
+ To let AI coding agents work with `layered-ui-rails` in your project, install the included [agent skill](#agent-skill):
84
+
85
+ ```bash
86
+ bin/rails generate layered:ui:install_agent_skill
87
+ ```
88
+
83
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:
84
90
 
85
91
  ```erb
@@ -101,7 +107,7 @@ All colors are CSS custom properties on `:root` using a two-tier system:
101
107
 
102
108
  ```css
103
109
  /* app/assets/tailwind/application.css */
104
- @import "./layered_ui";
110
+ @import "../builds/tailwind/layered_ui";
105
111
 
106
112
  :root {
107
113
  --accent: oklch(0.58 0.19 255);
@@ -115,27 +121,19 @@ All colors are CSS custom properties on `:root` using a two-tier system:
115
121
  }
116
122
  ```
117
123
 
118
- For dynamic theming (e.g. per-tenant branding), use `content_for :l_ui_head` to inject content into the layout `<head>`:
124
+ `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.
125
+
126
+ 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:
119
127
 
120
128
  ```erb
121
129
  <% content_for :l_ui_head do %>
122
- <style>
123
- :root { --accent: <%= @tenant.accent_color %>; --accent-foreground: oklch(1 0 0); }
124
- </style>
130
+ <%= stylesheet_link_tag tenant_theme_path(current_tenant) %>
125
131
  <% end %>
126
132
  ```
127
133
 
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
- > ```
134
+ > **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).
135
+
136
+ > **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.
139
137
 
140
138
  See the [Colors documentation](https://layered-ui-rails.layered.ai/layout_colors) for the full list of tokens.
141
139
 
data/Rakefile CHANGED
@@ -13,4 +13,10 @@ Rake::TestTask.new(:test) do |t|
13
13
  t.verbose = false
14
14
  end
15
15
 
16
+ # The engine layout links the compiled Tailwind build (stylesheet_link_tag
17
+ # "tailwind"), so the dummy app's CSS must be built before integration tests
18
+ # run - otherwise propshaft raises "asset 'tailwind.css' was not found". This
19
+ # also generates the engine entry point under app/assets/builds/tailwind/.
20
+ task test: "app:tailwindcss:build"
21
+
16
22
  task default: :test
@@ -80,6 +80,7 @@
80
80
  --error-bg: oklch(0.748 0.1306 20.64);
81
81
  --error-text: oklch(0.2248 0.0874 28.11);
82
82
  --header-height: 63px;
83
+ --l-ui-gutter: 1rem;
83
84
  }
84
85
 
85
86
  .dark {
@@ -444,11 +445,16 @@
444
445
 
445
446
  .l-ui-page {
446
447
  @apply flex flex-1 flex-col overflow-x-clip
447
- min-h-full max-w-full
448
- px-4 pb-4 pt-[calc(var(--header-height)+1rem)]
448
+ min-h-full
449
+ px-[var(--l-ui-gutter)] pb-[var(--l-ui-gutter)] pt-[calc(var(--header-height)+var(--l-ui-gutter))]
449
450
  transition-[margin] duration-300;
450
451
  }
451
452
 
453
+ /* Break a child out to the page edge by cancelling the page gutter (e.g. a full-bleed hero). */
454
+ .l-ui-bleed {
455
+ @apply mx-[calc(var(--l-ui-gutter)*-1)];
456
+ }
457
+
452
458
  .l-ui-page a:not([class*="l-ui-"]),
453
459
  .l-ui-panel__body a:not([class*="l-ui-"]) {
454
460
  @apply text-foreground underline underline-offset-4 decoration-foreground-muted/60
@@ -464,7 +470,8 @@
464
470
  @apply flex flex-1 flex-col items-center justify-center;
465
471
  }
466
472
 
467
- .l-ui-page__width-constrained {
473
+ /* Narrow ~384px column (md:max-w-sm) for any compact, centred content. */
474
+ .l-ui-page__narrow {
468
475
  @apply flex flex-col
469
476
  w-full md:max-w-sm
470
477
  my-auto;
@@ -486,7 +493,7 @@
486
493
  .l-ui-header {
487
494
  @apply flex items-center justify-between
488
495
  h-[var(--header-height)]
489
- px-4 py-3;
496
+ px-[var(--l-ui-gutter)] py-3;
490
497
  }
491
498
 
492
499
  .l-ui-body--header-contained .l-ui-header {
@@ -581,8 +588,20 @@
581
588
  @apply dark:invert;
582
589
  }
583
590
 
591
+ /* Painted from the --button-primary-icon token via a mask, so any accent
592
+ foreground works in both themes (not just black/white as `invert` allowed).
593
+ The icon shape comes from --l-ui-icon-src, set inline at the call site. */
584
594
  .l-ui-button--primary .l-ui-icon {
585
- @apply invert dark:invert-0;
595
+ @apply inline-block shrink-0;
596
+ background-color: var(--button-primary-icon);
597
+ -webkit-mask: var(--l-ui-icon-src) no-repeat center / contain;
598
+ mask: var(--l-ui-icon-src) no-repeat center / contain;
599
+ }
600
+
601
+ /* Higher specificity than `.dark .l-ui-icon` so the base dark-mode invert
602
+ does not flip the token colour above. */
603
+ .dark .l-ui-button--primary .l-ui-icon {
604
+ filter: none;
586
605
  }
587
606
 
588
607
  .l-ui-icon--xs {
@@ -1,7 +1,12 @@
1
1
  module Layered
2
2
  module Ui
3
3
  module FormHelper
4
- FIELD_TYPES = %i[string text email password number date datetime select checkbox hidden].freeze
4
+ FIELD_TYPES = %i[
5
+ string text email password number tel url search
6
+ date datetime time month week
7
+ color range file
8
+ select checkbox hidden
9
+ ].freeze
5
10
 
6
11
  # Renders a complete form with all fields, error summary,
7
12
  # and submit button.
@@ -10,9 +15,10 @@ module Layered
10
15
  # fields: Post.l_managed_resource_fields,
11
16
  # url: managed_posts_path)
12
17
  #
13
- def l_ui_form(record, fields:, url:, method: nil, submit: nil)
18
+ def l_ui_form(record, fields:, url:, method: nil, submit: nil, multipart: nil)
14
19
  render partial: "layered/ui/managed_resource/form",
15
- locals: { record: record, fields: fields, url: url, method: method, submit: submit }
20
+ locals: { record: record, fields: fields, url: url, method: method,
21
+ submit: submit, multipart: multipart }
16
22
  end
17
23
 
18
24
  # Normalises a raw field config hash into a canonical form.
@@ -35,7 +41,7 @@ module Layered
35
41
  label = config[:label] || attribute.to_s.humanize
36
42
 
37
43
  extras = config.except(:attribute, :as, :label, :required, :hint,
38
- :collection, :placeholder)
44
+ :collection, :placeholder, :prompt, :include_blank)
39
45
 
40
46
  {
41
47
  attribute: attribute,
@@ -45,6 +51,8 @@ module Layered
45
51
  hint: config[:hint],
46
52
  collection: config[:collection],
47
53
  placeholder: config[:placeholder],
54
+ prompt: config[:prompt],
55
+ include_blank: config[:include_blank],
48
56
  extras: extras
49
57
  }
50
58
  end
@@ -0,0 +1,17 @@
1
+ module Layered
2
+ module Ui
3
+ module HeaderHelper
4
+ def l_ui_theme_toggle
5
+ render "layouts/layered_ui/theme_toggle"
6
+ end
7
+
8
+ def l_ui_authentication
9
+ render "layouts/layered_ui/authentication"
10
+ end
11
+
12
+ def l_ui_navigation_toggle
13
+ render "layouts/layered_ui/navigation_toggle"
14
+ end
15
+ end
16
+ end
17
+ end
@@ -1,5 +1,5 @@
1
1
  <div class="l-ui-page__vertically-centered">
2
- <div class="l-ui-page__width-constrained">
2
+ <div class="l-ui-page__narrow">
3
3
  <h1>Resend confirmation instructions</h1>
4
4
 
5
5
  <%= form_with(model: resource, as: resource_name, url: confirmation_path(resource_name), method: :post, class: 'l-ui-form l-ui-mt-3') do |f| %>
@@ -1,5 +1,5 @@
1
1
  <div class="l-ui-page__vertically-centered">
2
- <div class="l-ui-page__width-constrained">
2
+ <div class="l-ui-page__narrow">
3
3
  <h1>Change your password</h1>
4
4
 
5
5
  <%= form_with(model: resource, as: resource_name, url: password_path(resource_name), method: :put, class: 'l-ui-form l-ui-mt-3') do |f| %>
@@ -1,5 +1,5 @@
1
1
  <div class="l-ui-page__vertically-centered">
2
- <div class="l-ui-page__width-constrained">
2
+ <div class="l-ui-page__narrow">
3
3
  <h1>Forgot your password?</h1>
4
4
 
5
5
  <%= form_with(model: resource, as: resource_name, url: password_path(resource_name), method: :post, class: 'l-ui-form l-ui-mt-3') do |f| %>
@@ -1,5 +1,5 @@
1
1
  <div class="l-ui-page__vertically-centered">
2
- <div class="l-ui-page__width-constrained">
2
+ <div class="l-ui-page__narrow">
3
3
  <h1>Register</h1>
4
4
 
5
5
  <%= form_with(model: resource, as: resource_name, url: registration_path(resource_name), class: 'l-ui-form l-ui-mt-3') do |f| %>
@@ -1,5 +1,5 @@
1
1
  <div class="l-ui-page__vertically-centered">
2
- <div class="l-ui-page__width-constrained">
2
+ <div class="l-ui-page__narrow">
3
3
  <h1>Login</h1>
4
4
 
5
5
  <%= form_with(model: resource, as: resource_name, url: session_path(resource_name), class: 'l-ui-form l-ui-mt-3') do |f| %>
@@ -1,5 +1,5 @@
1
1
  <div class="l-ui-page__vertically-centered">
2
- <div class="l-ui-page__width-constrained">
2
+ <div class="l-ui-page__narrow">
3
3
  <h1>Resend unlock instructions</h1>
4
4
 
5
5
  <%= form_with(model: resource, as: resource_name, url: unlock_path(resource_name), method: :post, class: 'l-ui-form l-ui-mt-3') do |f| %>
@@ -25,11 +25,15 @@
25
25
  <%
26
26
  collection = config[:collection]
27
27
  choices = collection.respond_to?(:call) ? collection.call : collection
28
- include_blank = extras.delete(:include_blank)
29
- include_blank = true if include_blank.nil?
28
+ include_blank = config[:include_blank]
29
+ prompt = config[:prompt]
30
+ include_blank = true if include_blank.nil? && prompt.nil?
31
+ select_options = {}
32
+ select_options[:include_blank] = include_blank unless include_blank.nil?
33
+ select_options[:prompt] = prompt unless prompt.nil?
30
34
  %>
31
35
  <div class="l-ui-select-container">
32
- <%= form.select(attribute, choices, { include_blank: include_blank },
36
+ <%= form.select(attribute, choices, select_options,
33
37
  class: field_class.call("l-ui-select"), **base_opts, **extras) %>
34
38
  </div>
35
39
  <% elsif config[:as] == :checkbox %>
@@ -38,7 +42,13 @@
38
42
  <%= form.label(attribute, config[:label], class: "l-ui-checkbox-container__label") %>
39
43
  </div>
40
44
  <% else %>
41
- <% method_name = config[:as] == :string ? :text_field : :"#{config[:as]}_field" %>
45
+ <%
46
+ method_name = case config[:as]
47
+ when :string then :text_field
48
+ when :tel then :telephone_field
49
+ else :"#{config[:as]}_field"
50
+ end
51
+ %>
42
52
  <%= form.public_send(method_name, attribute,
43
53
  class: field_class.call("l-ui-form__field"), **base_opts, **extras) %>
44
54
  <% end %>
@@ -1,5 +1,7 @@
1
+ <% multipart = local_assigns.fetch(:multipart, nil) %>
1
2
  <% form_opts = { url: url, class: "l-ui-form" } %>
2
3
  <% form_opts[:method] = method if method %>
4
+ <% form_opts[:multipart] = multipart.nil? ? fields.any? { |f| f[:as] == :file } : multipart %>
3
5
 
4
6
  <%= form_with(model: record, **form_opts) do |f| %>
5
7
  <%= render "layered_ui/shared/form_errors", item: record %>
@@ -0,0 +1,9 @@
1
+ <% unless l_ui_user_signed_in? %>
2
+ <% if respond_to?(:new_user_registration_path) %>
3
+ <%= link_to "Register", main_app.new_user_registration_path, class: "l-ui-button l-ui-button--outline l-ui-button--small" %>
4
+ <% end %>
5
+
6
+ <% if respond_to?(:new_user_session_path) %>
7
+ <%= link_to "Login", main_app.new_user_session_path, class: "l-ui-button l-ui-button--primary l-ui-button--small" %>
8
+ <% end %>
9
+ <% end %>
@@ -14,42 +14,14 @@
14
14
  <% end %>
15
15
 
16
16
  <nav class="l-ui-header__navigation" aria-label="Header navigation">
17
- <button
18
- type="button"
19
- data-controller="l-ui--theme"
20
- data-action="click->l-ui--theme#toggle"
21
- data-l-ui--theme-target="button"
22
- class="l-ui-button l-ui-button--icon l-ui-theme-toggle"
23
- aria-label="Toggle dark mode"
24
- aria-pressed="false"
25
- >
26
- <%= 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 } %>
27
- <%= 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 } %>
28
- </button>
29
-
30
- <% unless l_ui_user_signed_in? %>
31
- <% if respond_to?(:new_user_registration_path) %>
32
- <%= link_to "Register", main_app.new_user_registration_path, class: "l-ui-button l-ui-button--outline l-ui-button--small" %>
33
- <% end %>
34
-
35
- <% if respond_to?(:new_user_session_path) %>
36
- <%= link_to "Login", main_app.new_user_session_path, class: "l-ui-button l-ui-button--primary l-ui-button--small" %>
37
- <% end %>
38
- <% end %>
39
-
40
- <% if yield(:l_ui_navigation_items).present? || l_ui_user_signed_in? %>
41
- <button
42
- type="button"
43
- class="l-ui-button l-ui-button--navigation-toggle"
44
- data-action="click->l-ui--navigation#toggle"
45
- data-l-ui--navigation-target="toggleButton"
46
- aria-label="Toggle navigation menu"
47
- aria-expanded="false"
48
- aria-controls="l-ui-navigation"
49
- >
50
- <%= 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 } %>
51
- <%= 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 } %>
52
- </button>
17
+ <% if content_for?(:l_ui_header_actions) %>
18
+ <%= yield(:l_ui_header_actions) %>
19
+ <% else %>
20
+ <%= yield(:l_ui_header_actions_start) %>
21
+ <%= l_ui_theme_toggle %>
22
+ <%= l_ui_authentication %>
23
+ <%= l_ui_navigation_toggle %>
24
+ <%= yield(:l_ui_header_actions_end) %>
53
25
  <% end %>
54
26
  </nav>
55
27
  </div>
@@ -0,0 +1,14 @@
1
+ <% if yield(:l_ui_navigation_items).present? || l_ui_user_signed_in? %>
2
+ <button
3
+ type="button"
4
+ class="l-ui-button l-ui-button--navigation-toggle"
5
+ data-action="click->l-ui--navigation#toggle"
6
+ data-l-ui--navigation-target="toggleButton"
7
+ aria-label="Toggle navigation menu"
8
+ aria-expanded="false"
9
+ aria-controls="l-ui-navigation"
10
+ >
11
+ <%= 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 } %>
12
+ <%= 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 } %>
13
+ </button>
14
+ <% end %>
@@ -41,7 +41,7 @@
41
41
  title="Toggle panel (Ctrl+i / ⌘i)"
42
42
  data-action="click->l-ui--panel#toggle"
43
43
  data-l-ui--panel-target="hideButton">
44
- <%= image_tag "layered_ui/icon_panel_close.svg", alt: "", class: "l-ui-icon l-ui-icon--sm", aria: { hidden: true } %>
44
+ <span class="l-ui-icon l-ui-icon--sm" style="--l-ui-icon-src: url('<%= asset_path "layered_ui/icon_panel_close.svg" %>')" aria-hidden="true"></span>
45
45
  </button>
46
46
  </div>
47
47
  </div>
@@ -0,0 +1,12 @@
1
+ <button
2
+ type="button"
3
+ data-controller="l-ui--theme"
4
+ data-action="click->l-ui--theme#toggle"
5
+ data-l-ui--theme-target="button"
6
+ class="l-ui-button l-ui-button--icon l-ui-theme-toggle"
7
+ aria-label="Toggle dark mode"
8
+ aria-pressed="false"
9
+ >
10
+ <%= 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 } %>
11
+ <%= 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 } %>
12
+ </button>
@@ -20,7 +20,7 @@
20
20
 
21
21
  <%# Apply dark class before paint to prevent white flash on reload %>
22
22
  <script>try{var s=localStorage.getItem("theme");(s==="dark"||(!s&&matchMedia("(prefers-color-scheme:dark)").matches))&&document.documentElement.classList.add("dark")}catch(e){}</script>
23
- <%= stylesheet_link_tag :app, "data-turbo-track": "reload" %>
23
+ <%= stylesheet_link_tag "tailwind", "data-turbo-track": "reload" %>
24
24
  <%= yield(:l_ui_head) %>
25
25
  <%= javascript_importmap_tags %>
26
26
  </head>
@@ -10,7 +10,7 @@ module Layered
10
10
  return unless File.exist?(application_css)
11
11
 
12
12
  content = File.read(application_css)
13
- import_line = '@import "./layered_ui";'
13
+ import_line = '@import "../builds/tailwind/layered_ui";'
14
14
  overrides_line = '@import "./layered_ui_overrides";'
15
15
 
16
16
  unless content.include?(import_line)
@@ -15,10 +15,6 @@ module Layered
15
15
  end
16
16
  end
17
17
 
18
- def copy_assets
19
- invoke "layered:ui:copy_assets"
20
- end
21
-
22
18
  def create_overrides
23
19
  invoke "layered:ui:create_overrides"
24
20
  end
@@ -33,6 +33,7 @@ module Layered
33
33
  helper Layered::Ui::TableHelper
34
34
  helper Layered::Ui::TitleBarHelper
35
35
  helper Layered::Ui::FormHelper
36
+ helper Layered::Ui::HeaderHelper
36
37
  helper Layered::Ui::ModalHelper
37
38
  helper Layered::Ui::RansackHelper
38
39
  end
@@ -1,5 +1,5 @@
1
1
  module Layered
2
2
  module Ui
3
- VERSION = "0.16.1"
3
+ VERSION = "0.18.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.16.1
4
+ version: 0.18.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - layered.ai
@@ -209,10 +209,11 @@ files:
209
209
  - app/assets/images/layered_ui/logo_light.svg
210
210
  - app/assets/images/layered_ui/panel_icon_dark.svg
211
211
  - app/assets/images/layered_ui/panel_icon_light.svg
212
- - app/assets/tailwind/layered/ui/styles.css
212
+ - app/assets/tailwind/layered_ui/engine.css
213
213
  - app/helpers/layered/ui/authentication_helper.rb
214
214
  - app/helpers/layered/ui/breadcrumbs_helper.rb
215
215
  - app/helpers/layered/ui/form_helper.rb
216
+ - app/helpers/layered/ui/header_helper.rb
216
217
  - app/helpers/layered/ui/modal_helper.rb
217
218
  - app/helpers/layered/ui/navigation_helper.rb
218
219
  - app/helpers/layered/ui/pagy_helper.rb
@@ -253,13 +254,15 @@ files:
253
254
  - app/views/layered_ui/shared/_label.html.erb
254
255
  - app/views/layered_ui/shared/_search_field.html.erb
255
256
  - app/views/layered_ui/shared/_search_select.html.erb
257
+ - app/views/layouts/layered_ui/_authentication.html.erb
256
258
  - app/views/layouts/layered_ui/_header.html.erb
257
259
  - app/views/layouts/layered_ui/_navigation.html.erb
260
+ - app/views/layouts/layered_ui/_navigation_toggle.html.erb
258
261
  - app/views/layouts/layered_ui/_notice.html.erb
259
262
  - app/views/layouts/layered_ui/_panel.html.erb
263
+ - app/views/layouts/layered_ui/_theme_toggle.html.erb
260
264
  - app/views/layouts/layered_ui/application.html.erb
261
265
  - config/importmap.rb
262
- - lib/generators/layered/ui/copy_assets_generator.rb
263
266
  - lib/generators/layered/ui/create_overrides_generator.rb
264
267
  - lib/generators/layered/ui/import_css_generator.rb
265
268
  - lib/generators/layered/ui/import_js_generator.rb
@@ -286,12 +289,19 @@ post_install_message: |
286
289
  bin/rails generate layered:ui:install
287
290
 
288
291
  This command will:
289
- Copy the layered UI CSS to your host app at app/assets/tailwind/layered_ui.css
290
- This approach ensures the CSS is processed with your host app's Tailwind configuration
291
- Add an import statement to your app/assets/tailwind/application.css
292
+ Add `@import "../builds/tailwind/layered_ui";` to your app/assets/tailwind/application.css
293
+ The engine's CSS is served directly from the gem via tailwindcss-rails' engine
294
+ support, so it is compiled with your host app's Tailwind configuration and stays
295
+ in sync automatically when you upgrade
296
+ • Create app/assets/tailwind/layered_ui_overrides.css for your theme customisations
292
297
  • Add `import "layered_ui"` to your app/javascript/application.js (just after `import "@hotwired/turbo-rails"`, if present)
293
298
 
294
299
  If these imports already exist, they will not be duplicated.
300
+
301
+ To let AI coding agents work with layered-ui-rails in your project, install
302
+ the included agent skill:
303
+
304
+ bin/rails generate layered:ui:install_agent_skill
295
305
  rdoc_options: []
296
306
  require_paths:
297
307
  - lib
@@ -1,30 +0,0 @@
1
- module Layered
2
- module Ui
3
- module Generators
4
- class CopyAssetsGenerator < Rails::Generators::Base
5
- desc "Copy layered-ui-rails CSS assets into the host application"
6
-
7
- def self.source_root
8
- Layered::Ui::Engine.root
9
- end
10
-
11
- def copy_css
12
- source_path = File.join(self.class.source_root, "app/assets/tailwind/layered/ui/styles.css")
13
- source_content = File.read(source_path)
14
-
15
- header = <<~CSS
16
- /*
17
- * layered-ui-rails v#{Layered::Ui::VERSION}
18
- *
19
- * This file was automatically generated by the layered:ui:install generator.
20
- * Do not modify directly. To update, re-run: bin/rails generate layered:ui:install
21
- */
22
-
23
- CSS
24
-
25
- create_file "app/assets/tailwind/layered_ui.css", header + source_content
26
- end
27
- end
28
- end
29
- end
30
- end