layered-ui-rails 0.17.0 → 0.18.1
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 +4 -4
- data/.claude/skills/layered-ui-rails/SKILL.md +38 -10
- data/.claude/skills/layered-ui-rails/references/CSS.md +10 -2
- data/.claude/skills/layered-ui-rails/references/HELPERS.md +16 -2
- data/AGENTS.md +3 -2
- data/CHANGELOG.md +41 -0
- data/README.md +16 -18
- data/Rakefile +6 -0
- data/app/assets/tailwind/{layered/ui/styles.css → layered_ui/engine.css} +51 -6
- data/app/javascript/layered_ui/controllers/l_ui/navigation_section_controller.js +32 -1
- data/app/views/devise/confirmations/new.html.erb +1 -1
- data/app/views/devise/passwords/edit.html.erb +1 -1
- data/app/views/devise/passwords/new.html.erb +1 -1
- data/app/views/devise/registrations/new.html.erb +1 -1
- data/app/views/devise/sessions/new.html.erb +1 -1
- data/app/views/devise/unlocks/new.html.erb +1 -1
- data/app/views/layouts/layered_ui/_panel.html.erb +2 -2
- data/app/views/layouts/layered_ui/application.html.erb +1 -1
- data/lib/generators/layered/ui/import_css_generator.rb +1 -1
- data/lib/generators/layered/ui/install_generator.rb +0 -4
- data/lib/layered/ui/version.rb +1 -1
- metadata +12 -6
- 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:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2acf474942e7d6fa669959fcf0c78675d57e9ae065f18347ddbb3a9f1a48b7fc
|
|
4
|
+
data.tar.gz: a9a2c5123723a239582523090095f2539c01ba7439335bcffc464d51bd9ffabe
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 805599d0bc604bdde66d6092b283ae078024b5b0a1761b2dcd8795926f240439861fe63c591dbd8c06357cf16d39c3b46111e4356f2051b3b662e9a664334821
|
|
7
|
+
data.tar.gz: d84bad400743d3e09478ba82d1e735037051758d6b5e1a65dcd93613c6a24569823c1d578b8fddcb5b88b53fab2225a5c0c2abc4e42afb17099920099c031822
|
|
@@ -20,7 +20,7 @@ bundle add layered-ui-rails
|
|
|
20
20
|
bin/rails generate layered:ui:install
|
|
21
21
|
```
|
|
22
22
|
|
|
23
|
-
The generator
|
|
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
|
|
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
|
-
|
|
64
|
-
|
|
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> %>
|
|
@@ -101,6 +105,30 @@ Populate layout regions with `content_for` (always above the render call):
|
|
|
101
105
|
<% end %>
|
|
102
106
|
```
|
|
103
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
|
+
|
|
104
132
|
Body class modifiers:
|
|
105
133
|
- `l-ui-body--always-show-navigation` - pins navigation as a sidebar on desktop
|
|
106
134
|
- `l-ui-body--hide-header` - hides the header and collapses its space
|
|
@@ -125,7 +153,7 @@ Quick reference:
|
|
|
125
153
|
|
|
126
154
|
| Helper | Purpose |
|
|
127
155
|
|---|---|
|
|
128
|
-
| `l_ui_navigation_item(label, path, ...)` | Sidebar nav link (supports `icon:`, `match: :starts_with`, `expandable:`) |
|
|
156
|
+
| `l_ui_navigation_item(label, path, ...)` | Sidebar nav link (supports `icon:`, `match: :starts_with`, `expandable:`). For valid `icon:` names and the missing-asset gotcha, see the "Icons" section in `references/HELPERS.md` |
|
|
129
157
|
| `l_ui_navigation_section(heading = nil, ...)` | Group nav items; supports `collapsible:`, `storage_key:`, `separated:` |
|
|
130
158
|
| `l_ui_breadcrumbs(&block)` | Breadcrumb nav wrapper |
|
|
131
159
|
| `l_ui_breadcrumb_item(label, path = nil)` | Individual breadcrumb |
|
|
@@ -150,7 +178,7 @@ Key components:
|
|
|
150
178
|
|
|
151
179
|
| Component | Key classes |
|
|
152
180
|
|---|---|
|
|
153
|
-
| Page layout | `.l-ui-page`, `--with-navigation`, `__vertically-centered`, `
|
|
181
|
+
| Page layout | `.l-ui-page`, `--with-navigation`, `__vertically-centered`, `__narrow` (narrow ~384px column, md:max-w-sm) |
|
|
154
182
|
| Buttons | `.l-ui-button`, `--primary`, `--outline`, `--outline-danger`, `--full`, `--icon` |
|
|
155
183
|
| Surfaces | `.l-ui-surface`, `--highlighted`, `--sm`, `--collapsible`, `--collapsible-highlighted` |
|
|
156
184
|
| Forms | `.l-ui-form`, `.l-ui-form__group`, `.l-ui-form__field`, `.l-ui-label`, `.l-ui-select` |
|
|
@@ -181,7 +209,7 @@ All controllers use the `l-ui--` namespace and are auto-registered via importmap
|
|
|
181
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.
|
|
182
210
|
|
|
183
211
|
```css
|
|
184
|
-
@import "
|
|
212
|
+
@import "../builds/tailwind/layered_ui";
|
|
185
213
|
|
|
186
214
|
:root {
|
|
187
215
|
--accent: oklch(0.58 0.19 255);
|
|
@@ -217,8 +245,8 @@ Layered::Ui.current_user_method = :current_member # default: :current_user
|
|
|
217
245
|
|
|
218
246
|
## Common issues
|
|
219
247
|
|
|
220
|
-
- **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
|
|
221
|
-
- **Missing styles** - Ensure `@import "
|
|
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.
|
|
222
250
|
- **Missing JS controllers** - Ensure `import "layered_ui"` is in `app/javascript/application.js`.
|
|
223
251
|
|
|
224
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-
|
|
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`):
|
|
@@ -53,7 +60,8 @@ Always combine the `l-ui-button` base class with a colour modifier (e.g. `l-ui-b
|
|
|
53
60
|
Icon variants (combine with the `l-ui-button` base):
|
|
54
61
|
|
|
55
62
|
```
|
|
56
|
-
.l-ui-button--icon Icon-only button (
|
|
63
|
+
.l-ui-button--icon Icon-only button (44px square, no text)
|
|
64
|
+
Add --small (l-ui-button--icon l-ui-button--small) for a 32px square icon button
|
|
57
65
|
.l-ui-button--navigation-toggle Mobile navigation toggle
|
|
58
66
|
```
|
|
59
67
|
|
|
@@ -14,13 +14,13 @@ l_ui_navigation_section(heading = nil, icon: nil, icon_path: nil, icon_html: nil
|
|
|
14
14
|
- `path` (String) - URL
|
|
15
15
|
- `active` (Boolean, optional) - force the active style; defaults to a match against the current request path. Does not affect `aria-current`, which is set only when `current_page?(path)` is true
|
|
16
16
|
- `match` (Symbol) - `:exact` (default) uses `current_page?`; `:starts_with` activates when the request path begins with `path` (use for parents whose sub-routes should keep the parent highlighted)
|
|
17
|
-
- `icon` (String, optional) - icon name
|
|
17
|
+
- `icon` (String, optional) - icon name. Resolves to the asset `layered_ui/icon_NAME.svg`. See "Icons" below for the names that ship with the gem and how to add your own. A name with no matching asset raises `Propshaft::MissingAssetError` (a 500 in dev) when the page renders, so only pass a name you know resolves
|
|
18
18
|
- `icon_path` (String, optional) - explicit asset path; takes precedence over `icon:`
|
|
19
19
|
- `icon_html` (ActiveSupport::SafeBuffer, optional) - pre-rendered icon markup for icon-font libraries (e.g. `tag.i(class: "fa-solid fa-house")`); takes precedence over `icon:` and `icon_path:`. Must be already html-safe - plain strings will be escaped. Never pass user-controlled input
|
|
20
20
|
|
|
21
21
|
`l_ui_navigation_section`:
|
|
22
22
|
- `heading` (String, optional) - non-clickable section heading; omit for an unlabelled group. To expose the parent route of a section, add an "Overview" item inside the block
|
|
23
|
-
- `icon` (String, optional) -
|
|
23
|
+
- `icon` (String, optional) - icon name next to the heading; resolves the same way as `l_ui_navigation_item`'s `icon:` (see "Icons" below)
|
|
24
24
|
- `icon_path` (String, optional) - explicit asset path next to the heading; takes precedence over `icon:`
|
|
25
25
|
- `icon_html` (String, optional) - pre-rendered icon markup; takes precedence over `icon:` and `icon_path:`
|
|
26
26
|
- `collapsible` (Boolean) - when `true` and a heading is given, renders a toggle button with a chevron and wires `aria-controls` to the panel
|
|
@@ -43,6 +43,20 @@ l_ui_navigation_section(heading = nil, icon: nil, icon_path: nil, icon_html: nil
|
|
|
43
43
|
<% end %>
|
|
44
44
|
```
|
|
45
45
|
|
|
46
|
+
### Icons
|
|
47
|
+
|
|
48
|
+
The `icon:` argument (on `l_ui_navigation_item` and `l_ui_navigation_section`) resolves to the asset `layered_ui/icon_NAME.svg`. There is no fallback: a name with no matching asset raises `Propshaft::MissingAssetError` and renders a 500. Only pass a name you know resolves.
|
|
49
|
+
|
|
50
|
+
Names that ship with the gem (use the bare name, e.g. `icon: "globe"`):
|
|
51
|
+
|
|
52
|
+
- General-purpose (black `currentColor` sources, recoloured for dark mode by `dark:invert` - safe to use via `icon:`): `home`, `globe`, `mail`, `github`, `discord`, `linkedin`, `x`, `youtube`
|
|
53
|
+
- Internal UI (engine chrome - theme toggle, nav chevrons, close buttons): `close`, `chevron_down`, `chevron_right`, `hamburger`, `sun`, `moon`, `light`, `dark`, `panel_close`. Several are CSS mask shapes or carry baked theme colours (e.g. `chevron_down` is white, `dark` fills white), so they will **not** render correctly through `icon:`. Don't use these for nav icons
|
|
54
|
+
|
|
55
|
+
To add your own, drop an SVG in the host app at `app/assets/images/layered_ui/icon_NAME.svg` and pass `icon: "NAME"`. Alternatively:
|
|
56
|
+
|
|
57
|
+
- `icon_path:` - point at any asset path directly (e.g. `icon_path: "my_icons/star.svg"`), bypassing the `layered_ui/icon_` convention
|
|
58
|
+
- `icon_html:` - pass pre-rendered, html-safe markup for icon-font libraries (e.g. `tag.i(class: "fa-solid fa-house")`); never pass user-controlled input
|
|
59
|
+
|
|
46
60
|
## Breadcrumbs
|
|
47
61
|
|
|
48
62
|
```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/
|
|
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` (
|
|
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)
|
|
@@ -27,6 +27,7 @@ Guidance for AI agents working in this repository.
|
|
|
27
27
|
- Use l-ui classes only in engine views, with no additional Tailwind utilities, as Tailwind classes referenced only inside the engine will not be generated by the host app’s build
|
|
28
28
|
- Dummy app documentation pages may use additional Tailwind utilities, but should favour l-ui classes where possible
|
|
29
29
|
- Importmap for JS (no bundler)
|
|
30
|
+
- Put new changes on a branch with a meaningful name, but do not commit; the user will ask when ready to commit
|
|
30
31
|
- We are currently in pre-release, so breaking changes may be introduced. Flag any expected breaking changes clearly before making them, but do not avoid necessary improvements solely to preserve backwards compatibility
|
|
31
32
|
- CRITICAL: Retain WCAG 2.2 AA compliance. Do not be too pedantic though as this introduces audit loops which are undesirable.
|
|
32
33
|
- A WCAG 2.2 AA table looks like this:
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,47 @@
|
|
|
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.1] - 2026-06-07
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Small icon buttons: combining `l-ui-button--icon` with `l-ui-button--small` now renders a 32px square (still above the WCAG 2.2 AA 24px target-size minimum) instead of stretching to 44px wide, since `--small` previously only shrank the height. The panel close button now uses this variant.
|
|
10
|
+
- Documented the icons bundled with the gem and clarified `icon:` usage: the name resolves to `layered_ui/icon_NAME.svg`, and an unknown name raises `Propshaft::MissingAssetError`. Added an "Available icons" section to the dummy app showing the general-purpose set (internal-UI chrome icons are not usable via `icon:`).
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
- Navigation sub-sections now animate open by transitioning height from 0 to their natural height, replacing the per-item slide keyframe animation. Respects `prefers-reduced-motion`.
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
|
|
18
|
+
- Navigation section chevron no longer animates when restoring its saved open/closed state on page load; it now renders in position.
|
|
19
|
+
|
|
20
|
+
## [0.18.0] - 2026-06-07
|
|
21
|
+
|
|
22
|
+
### Fixed
|
|
23
|
+
|
|
24
|
+
- 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.
|
|
25
|
+
|
|
26
|
+
### Added
|
|
27
|
+
|
|
28
|
+
- `--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.
|
|
29
|
+
- `.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.
|
|
30
|
+
|
|
31
|
+
### Changed
|
|
32
|
+
|
|
33
|
+
- **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`.
|
|
34
|
+
- **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.
|
|
35
|
+
- 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.
|
|
36
|
+
- 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).
|
|
37
|
+
|
|
38
|
+
### Removed
|
|
39
|
+
|
|
40
|
+
- `CopyAssetsGenerator` (`layered:ui:copy_assets`), which copied the engine CSS into the host app. It is replaced by the engine-support import above.
|
|
41
|
+
|
|
42
|
+
### Migration
|
|
43
|
+
|
|
44
|
+
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.
|
|
45
|
+
|
|
5
46
|
## [0.17.0] - 2026-05-19
|
|
6
47
|
|
|
7
48
|
### Added
|
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
|
-
-
|
|
80
|
-
-
|
|
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 "
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
129
|
-
|
|
130
|
-
> **CSP
|
|
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
|
|
448
|
-
px-
|
|
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
|
-
|
|
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-
|
|
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
|
|
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 {
|
|
@@ -728,8 +747,16 @@
|
|
|
728
747
|
mask-size: contain;
|
|
729
748
|
}
|
|
730
749
|
|
|
750
|
+
/* transition-none is intentional: the after-change style determines the
|
|
751
|
+
transition, so this gives "animate open, snap shut" - opening matches the
|
|
752
|
+
base rule (animates), closing matches this rule (snaps instantly). */
|
|
731
753
|
.l-ui-navigation__section-toggle[aria-expanded="false"] .l-ui-navigation__section-chevron {
|
|
732
|
-
@apply -rotate-90
|
|
754
|
+
@apply -rotate-90
|
|
755
|
+
transition-none;
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
.l-ui-navigation__section-toggle--no-transition .l-ui-navigation__section-chevron {
|
|
759
|
+
@apply transition-none;
|
|
733
760
|
}
|
|
734
761
|
|
|
735
762
|
.l-ui-navigation__section-items {
|
|
@@ -752,6 +779,19 @@
|
|
|
752
779
|
gap-0.5 px-3 py-3;
|
|
753
780
|
}
|
|
754
781
|
|
|
782
|
+
/* Height-expand slide applied while the section is opening; the controller
|
|
783
|
+
measures the natural height and transitions from 0 to reveal the items. */
|
|
784
|
+
.l-ui-navigation__section-items--opening {
|
|
785
|
+
@apply overflow-hidden
|
|
786
|
+
transition-[height] duration-200 ease-out;
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
@media (prefers-reduced-motion: reduce) {
|
|
790
|
+
.l-ui-navigation__section-items--opening {
|
|
791
|
+
@apply transition-none;
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
|
|
755
795
|
.l-ui-navigation__user {
|
|
756
796
|
@apply shrink-0
|
|
757
797
|
p-4;
|
|
@@ -878,6 +918,11 @@
|
|
|
878
918
|
text-xs;
|
|
879
919
|
}
|
|
880
920
|
|
|
921
|
+
.l-ui-button--icon.l-ui-button--small {
|
|
922
|
+
@apply min-w-[32px]
|
|
923
|
+
p-1.5;
|
|
924
|
+
}
|
|
925
|
+
|
|
881
926
|
.l-ui-button:disabled {
|
|
882
927
|
@apply opacity-50
|
|
883
928
|
cursor-not-allowed;
|
|
@@ -21,12 +21,21 @@ export default class extends Controller {
|
|
|
21
21
|
if (stored === null) return
|
|
22
22
|
|
|
23
23
|
const isOpen = stored === "true"
|
|
24
|
-
|
|
24
|
+
// Apply the restored state without animating the chevron on load; it should
|
|
25
|
+
// simply render in the correct position.
|
|
26
|
+
const toggle = this.toggleTarget
|
|
27
|
+
toggle.classList.add("l-ui-navigation__section-toggle--no-transition")
|
|
28
|
+
toggle.setAttribute("aria-expanded", isOpen ? "true" : "false")
|
|
25
29
|
if (isOpen) {
|
|
26
30
|
this.panelTarget.removeAttribute("hidden")
|
|
27
31
|
} else {
|
|
28
32
|
this.panelTarget.setAttribute("hidden", "")
|
|
29
33
|
}
|
|
34
|
+
requestAnimationFrame(() => {
|
|
35
|
+
requestAnimationFrame(() => {
|
|
36
|
+
toggle.classList.remove("l-ui-navigation__section-toggle--no-transition")
|
|
37
|
+
})
|
|
38
|
+
})
|
|
30
39
|
}
|
|
31
40
|
|
|
32
41
|
toggle() {
|
|
@@ -34,6 +43,7 @@ export default class extends Controller {
|
|
|
34
43
|
this.toggleTarget.setAttribute("aria-expanded", isOpen ? "true" : "false")
|
|
35
44
|
if (isOpen) {
|
|
36
45
|
this.panelTarget.removeAttribute("hidden")
|
|
46
|
+
this.#animateOpen()
|
|
37
47
|
} else {
|
|
38
48
|
this.panelTarget.setAttribute("hidden", "")
|
|
39
49
|
}
|
|
@@ -45,4 +55,25 @@ export default class extends Controller {
|
|
|
45
55
|
// ignore
|
|
46
56
|
}
|
|
47
57
|
}
|
|
58
|
+
|
|
59
|
+
// Slide the panel open by transitioning its height from 0 to its natural
|
|
60
|
+
// height, only on user-initiated open, not on initial page load.
|
|
61
|
+
#animateOpen() {
|
|
62
|
+
const panel = this.panelTarget
|
|
63
|
+
const CLASS = "l-ui-navigation__section-items--opening"
|
|
64
|
+
|
|
65
|
+
if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) return
|
|
66
|
+
|
|
67
|
+
const endHeight = panel.scrollHeight
|
|
68
|
+
panel.classList.add(CLASS)
|
|
69
|
+
panel.style.height = "0px"
|
|
70
|
+
panel.offsetHeight // Force a reflow so the starting height is applied.
|
|
71
|
+
panel.style.height = `${endHeight}px`
|
|
72
|
+
|
|
73
|
+
panel.addEventListener("transitionend", (event) => {
|
|
74
|
+
if (event.propertyName !== "height") return
|
|
75
|
+
panel.classList.remove(CLASS)
|
|
76
|
+
panel.style.height = ""
|
|
77
|
+
}, { once: true })
|
|
78
|
+
}
|
|
48
79
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<div class="l-ui-page__vertically-centered">
|
|
2
|
-
<div class="l-ui-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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| %>
|
|
@@ -35,13 +35,13 @@
|
|
|
35
35
|
|
|
36
36
|
<div class="l-ui-panel__header-actions">
|
|
37
37
|
<button type="button"
|
|
38
|
-
class="l-ui-button l-ui-button--primary l-ui-button--icon"
|
|
38
|
+
class="l-ui-button l-ui-button--primary l-ui-button--icon l-ui-button--small"
|
|
39
39
|
aria-label="Hide panel"
|
|
40
40
|
aria-controls="panel"
|
|
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
|
-
|
|
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>
|
|
@@ -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
|
|
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 "
|
|
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)
|
data/lib/layered/ui/version.rb
CHANGED
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.
|
|
4
|
+
version: 0.18.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- layered.ai
|
|
@@ -209,7 +209,7 @@ 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/
|
|
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
|
|
@@ -263,7 +263,6 @@ files:
|
|
|
263
263
|
- app/views/layouts/layered_ui/_theme_toggle.html.erb
|
|
264
264
|
- app/views/layouts/layered_ui/application.html.erb
|
|
265
265
|
- config/importmap.rb
|
|
266
|
-
- lib/generators/layered/ui/copy_assets_generator.rb
|
|
267
266
|
- lib/generators/layered/ui/create_overrides_generator.rb
|
|
268
267
|
- lib/generators/layered/ui/import_css_generator.rb
|
|
269
268
|
- lib/generators/layered/ui/import_js_generator.rb
|
|
@@ -290,12 +289,19 @@ post_install_message: |
|
|
|
290
289
|
bin/rails generate layered:ui:install
|
|
291
290
|
|
|
292
291
|
This command will:
|
|
293
|
-
•
|
|
294
|
-
•
|
|
295
|
-
|
|
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
|
|
296
297
|
• Add `import "layered_ui"` to your app/javascript/application.js (just after `import "@hotwired/turbo-rails"`, if present)
|
|
297
298
|
|
|
298
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
|
|
299
305
|
rdoc_options: []
|
|
300
306
|
require_paths:
|
|
301
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
|