layered-ui-rails 0.18.4 → 0.20.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 +4 -4
- data/.claude/skills/layered-ui-rails/SKILL.md +6 -0
- data/.claude/skills/layered-ui-rails/references/CONTROLLERS.md +25 -0
- data/.claude/skills/layered-ui-rails/references/CSS.md +87 -0
- data/.claude/skills/layered-ui-rails/references/HELPERS.md +45 -0
- data/CHANGELOG.md +33 -2
- data/README.md +1 -1
- data/app/assets/images/layered_ui/hero_background_dark.webp +0 -0
- data/app/assets/images/layered_ui/hero_background_light.webp +0 -0
- data/app/assets/images/layered_ui/icon_more.svg +5 -0
- data/app/assets/tailwind/layered_ui/engine.css +366 -3
- data/app/helpers/layered/ui/popover_helper.rb +81 -0
- data/app/javascript/layered_ui/controllers/l_ui/popover_controller.js +112 -0
- data/app/javascript/layered_ui/index.js +2 -0
- data/app/views/devise/shared/_links.html.erb +1 -1
- data/app/views/layered_ui/shared/_form_errors.html.erb +1 -1
- data/config/importmap.rb +1 -0
- data/lib/generators/layered/ui/create_overrides_generator.rb +51 -4
- data/lib/layered/ui/engine.rb +1 -0
- data/lib/layered/ui/version.rb +1 -1
- metadata +6 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f93bf15a80c67a460c8eada9407beb94507d3dce7fe511e935355abcd329a4d8
|
|
4
|
+
data.tar.gz: 06d49cc35c54fa21fddb6ced245abfa9ad4783003a03074beb9467e03f46cbb1
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f4435d521aa96f5e74b0d1183ae5d486b795d28eb6d6dc16728b8147599715a5e49b64c0a226a78b5aed479c6586dd73f02962321d2e976e693453f79d022fe6
|
|
7
|
+
data.tar.gz: 21030bae58aefc605b07370d6dcc615a1f1441566a72b28859068151109d41005b1fc333234878293a4c02d424d5896f9b3230886f1efcfc071ce3cbb493a7d4
|
|
@@ -181,6 +181,7 @@ Quick reference:
|
|
|
181
181
|
| `l_ui_theme_toggle` | Default header theme toggle button |
|
|
182
182
|
| `l_ui_authentication` | Default header login/register buttons (Devise) |
|
|
183
183
|
| `l_ui_navigation_toggle` | Default header sidebar toggle button |
|
|
184
|
+
| `l_ui_popover(id: nil, placement: :bottom, align: :start, container: {})` | Floating panel anchored to a trigger, built on the native `popover` attribute |
|
|
184
185
|
|
|
185
186
|
## CSS classes
|
|
186
187
|
|
|
@@ -192,13 +193,17 @@ Key components:
|
|
|
192
193
|
|---|---|
|
|
193
194
|
| 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`) |
|
|
194
195
|
| Buttons | `.l-ui-button`, `--primary`, `--outline`, `--outline-danger`, `--full`, `--icon` |
|
|
196
|
+
| Hero | `.l-ui-hero` (pair with `.l-ui-bleed`), `__inner`, `__title`, `__title-accent`, `__subtitle`, `__actions`, `__media`, `__media-img` |
|
|
195
197
|
| Surfaces | `.l-ui-surface`, `--highlighted`, `--sm`, `--collapsible`, `--collapsible-highlighted` |
|
|
198
|
+
| Cards | `.l-ui-card`, `--gradient`, `__eyebrow`, `__title`, `__icon`, `__body` |
|
|
199
|
+
| Tints | `.l-ui-tint-1` … `.l-ui-tint-5` (shared decorative palette; sets the `--l-ui-tint-*` role variables) |
|
|
196
200
|
| Forms | `.l-ui-form`, `.l-ui-form__group`, `.l-ui-form__field`, `.l-ui-label`, `.l-ui-select` |
|
|
197
201
|
| Tables | `.l-ui-table`, `.l-ui-table__header`, `.l-ui-table__cell`, `--primary`, `--action`, `.l-ui-table__action`, `--danger` |
|
|
198
202
|
| Badges | `.l-ui-badge`, `--rounded`, `--default`, `--success`, `--warning`, `--danger` |
|
|
199
203
|
| Notices | `.l-ui-notice` (base), `--success`, `--warning`, `--error` |
|
|
200
204
|
| Tabs | `.l-ui-tabs`, `.l-ui-tabs__list`, `.l-ui-tabs__tab`, `--active` |
|
|
201
205
|
| Modal | `.l-ui-modal`, `.l-ui-modal__header`, `.l-ui-modal__body` |
|
|
206
|
+
| Popover | `.l-ui-popover` |
|
|
202
207
|
|
|
203
208
|
## Stimulus controllers
|
|
204
209
|
|
|
@@ -214,6 +219,7 @@ All controllers use the `l-ui--` namespace and are auto-registered via importmap
|
|
|
214
219
|
| Panel resize | `l-ui--panel-resize` | Panel width drag handle |
|
|
215
220
|
| Modal | `l-ui--modal` | Native `<dialog>` with focus trap |
|
|
216
221
|
| Tabs | `l-ui--tabs` | Accessible tabbed interface |
|
|
222
|
+
| Popover | `l-ui--popover` | Positions a native `popover`-attribute element relative to its trigger, with auto-flip |
|
|
217
223
|
| Search form | `l-ui--search-form` | Multi-scope search with Turbo support and pagination param preservation |
|
|
218
224
|
|
|
219
225
|
## Theming
|
|
@@ -106,6 +106,31 @@ Accessible tabbed interface with keyboard navigation.
|
|
|
106
106
|
</div>
|
|
107
107
|
```
|
|
108
108
|
|
|
109
|
+
## Popover (`l-ui--popover`)
|
|
110
|
+
|
|
111
|
+
Positions a native `popover`-attribute element relative to its trigger, with auto-flip near viewport edges. Showing, hiding, light-dismiss (outside click), and Escape-to-close are all handled by the browser via the `popover` attribute - this controller only handles placement.
|
|
112
|
+
|
|
113
|
+
**Targets:** `trigger`, `popover`
|
|
114
|
+
**Values:** `placement` (String, default `"bottom"`; one of `"top"`, `"bottom"`, `"left"`, `"right"`), `align` (String, default `"start"`; `"start"` or `"end"` - which edge of the popover flushes with the trigger on the cross axis)
|
|
115
|
+
|
|
116
|
+
```html
|
|
117
|
+
<div data-controller="l-ui--popover">
|
|
118
|
+
<button type="button" popovertarget="my-popover"
|
|
119
|
+
data-l-ui--popover-target="trigger"
|
|
120
|
+
class="l-ui-button l-ui-button--outline">
|
|
121
|
+
Open popover
|
|
122
|
+
</button>
|
|
123
|
+
<div id="my-popover" popover="auto" class="l-ui-popover"
|
|
124
|
+
data-l-ui--popover-target="popover">
|
|
125
|
+
Content here.
|
|
126
|
+
</div>
|
|
127
|
+
</div>
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Features:
|
|
131
|
+
- Repositions on open, and while open on window resize/scroll
|
|
132
|
+
- Flips to the opposite side if the preferred placement would overflow the viewport
|
|
133
|
+
|
|
109
134
|
## Panel (`l-ui--panel`)
|
|
110
135
|
|
|
111
136
|
Resizable side panel. Full-width overlay on mobile, docked sidebar 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`):
|
|
@@ -210,6 +287,16 @@ Always combine the base block with a modifier, e.g. `<span class="l-ui-badge l-u
|
|
|
210
287
|
.l-ui-modal__body Scrollable modal content
|
|
211
288
|
```
|
|
212
289
|
|
|
290
|
+
## Popover
|
|
291
|
+
|
|
292
|
+
```
|
|
293
|
+
.l-ui-popover popover-attribute element; positioned by the l-ui--popover Stimulus controller
|
|
294
|
+
.l-ui-popover__menu Full-width list wrapper for a menu of actions inside a popover
|
|
295
|
+
.l-ui-popover__menu-item Full-width action link/button inside l-ui-popover__menu
|
|
296
|
+
.l-ui-popover__menu-item--danger Danger styling for a destructive menu item
|
|
297
|
+
.l-ui-popover__menu-divider Horizontal rule separating groups of items (apply to an <hr>)
|
|
298
|
+
```
|
|
299
|
+
|
|
213
300
|
## Breadcrumbs
|
|
214
301
|
|
|
215
302
|
```
|
|
@@ -322,6 +322,51 @@ The button does not need to be inside the helper's wrapper, and no `data-control
|
|
|
322
322
|
|
|
323
323
|
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.
|
|
324
324
|
|
|
325
|
+
## Popover
|
|
326
|
+
|
|
327
|
+
```ruby
|
|
328
|
+
l_ui_popover(id: nil, placement: :bottom, align: :start, container: {}, &block)
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
- `id` (String, optional) - DOM id for the popover element; defaults to an auto-generated id
|
|
332
|
+
- `placement` (Symbol, optional) - `:top`, `:bottom` (default), `:left`, or `:right`. Flips automatically if it would overflow the viewport
|
|
333
|
+
- `align` (Symbol, optional) - `:start` (default) flushes the popover's leading edge with the trigger's; `:end` flushes its trailing edge instead. For example, `placement: :bottom, align: :end` hangs the popover down and to the left of a trigger at the right end of a row
|
|
334
|
+
- `container` (Hash, optional) - extra HTML attributes for the wrapping `<div>` (e.g. `class:`)
|
|
335
|
+
- `&block` - the block's content is the popover body; call `p.trigger(**options, &block)` inside it to render the trigger button
|
|
336
|
+
|
|
337
|
+
```erb
|
|
338
|
+
<%= l_ui_popover(placement: :bottom) do |p| %>
|
|
339
|
+
<% p.trigger(class: "l-ui-button l-ui-button--outline") do %>
|
|
340
|
+
Options
|
|
341
|
+
<% end %>
|
|
342
|
+
<p>Popover content.</p>
|
|
343
|
+
<% end %>
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
Renders the trigger `<button popovertarget="...">` and the `popover="auto"` element (`class="l-ui-popover"`), and wires the `l-ui--popover` Stimulus controller for placement. Showing, hiding, light-dismiss (outside click), and Escape-to-close are all handled natively by the browser via the `popover` attribute - no Stimulus action is needed to open or close it.
|
|
347
|
+
|
|
348
|
+
If the trigger contains only an icon (no visible text), pass `aria-label:` to `p.trigger` so the button has an accessible name.
|
|
349
|
+
|
|
350
|
+
For a list of actions (e.g. a "more" menu on a table row), wrap the items in a `<div class="l-ui-popover__menu">` and give each link/button `l-ui-popover__menu-item` (add `l-ui-popover__menu-item--danger` for destructive actions, and an `<hr class="l-ui-popover__menu-divider">` to separate groups of items):
|
|
351
|
+
|
|
352
|
+
```erb
|
|
353
|
+
<%= l_ui_table(@users, columns: [...],
|
|
354
|
+
actions: ->(r) {
|
|
355
|
+
l_ui_popover(align: :end) do |p|
|
|
356
|
+
p.trigger(class: "l-ui-button l-ui-button--outline l-ui-button--icon l-ui-button--small", "aria-label" => "Actions for #{r.name}") do
|
|
357
|
+
image_tag "layered_ui/icon_more.svg", alt: "", class: "l-ui-icon l-ui-icon--sm", aria: { hidden: true }
|
|
358
|
+
end
|
|
359
|
+
tag.div(class: "l-ui-popover__menu") do
|
|
360
|
+
safe_join([
|
|
361
|
+
link_to("Edit", edit_user_path(r), class: "l-ui-popover__menu-item"),
|
|
362
|
+
tag.hr(class: "l-ui-popover__menu-divider"),
|
|
363
|
+
button_to("Delete", user_path(r), method: :delete, class: "l-ui-popover__menu-item l-ui-popover__menu-item--danger")
|
|
364
|
+
])
|
|
365
|
+
end
|
|
366
|
+
end
|
|
367
|
+
}) %>
|
|
368
|
+
```
|
|
369
|
+
|
|
325
370
|
## Header
|
|
326
371
|
|
|
327
372
|
```ruby
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,34 @@
|
|
|
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.20.0] - 2026-07-08
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Popovers: floating panels anchored to a trigger button, built on the native HTML `popover` attribute. Showing, hiding, light-dismiss (outside click), and Escape-to-close are all handled by the browser; the new `l-ui--popover` Stimulus controller only computes placement, flipping to the opposite side if the preferred side would overflow the viewport. Use the `l_ui_popover` helper (options: `id:`, `placement:` of `:top`/`:bottom`/`:left`/`:right`, `align:` of `:start`/`:end`, `container:`) with `p.trigger` for the button, or wire the `l-ui-popover` class and controller targets by hand for full control over markup.
|
|
10
|
+
- Popover menus: `l-ui-popover__menu`, `l-ui-popover__menu-item`, `l-ui-popover__menu-item--danger`, and `l-ui-popover__menu-divider` style a list of actions inside a popover - e.g. a "more" menu on a table row via `l_ui_table`'s `actions:` proc. Give an icon-only trigger an `aria-label` so it has an accessible name.
|
|
11
|
+
- `icon_more.svg` (vertical ellipsis) added to the bundled icon set, for "more actions" popover triggers.
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
|
|
15
|
+
- Navigation items now show a `surface` background on hover, matching the hover treatment of other menu rows. The active item keeps its highlighted background on hover.
|
|
16
|
+
- The overrides generator now stamps the gem version it was generated from into the overrides file's header comment.
|
|
17
|
+
|
|
18
|
+
## [0.19.0] - 2026-06-15
|
|
19
|
+
|
|
20
|
+
### Added
|
|
21
|
+
|
|
22
|
+
- `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).
|
|
23
|
+
- `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`.
|
|
24
|
+
- 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).
|
|
25
|
+
- `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.
|
|
26
|
+
- `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.
|
|
27
|
+
- 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).
|
|
28
|
+
|
|
29
|
+
### Breaking
|
|
30
|
+
|
|
31
|
+
- 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`.
|
|
32
|
+
|
|
5
33
|
## [0.18.4] - 2026-06-14
|
|
6
34
|
|
|
7
35
|
### Added
|
|
@@ -56,10 +84,13 @@ All notable changes to this project will be documented in this file. This projec
|
|
|
56
84
|
- `--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.
|
|
57
85
|
- `.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.
|
|
58
86
|
|
|
87
|
+
### Breaking
|
|
88
|
+
|
|
89
|
+
- `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`.
|
|
90
|
+
- 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.
|
|
91
|
+
|
|
59
92
|
### Changed
|
|
60
93
|
|
|
61
|
-
- **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`.
|
|
62
|
-
- **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.
|
|
63
94
|
- 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.
|
|
64
95
|
- 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).
|
|
65
96
|
|
data/README.md
CHANGED
|
@@ -36,7 +36,7 @@ An open source, Rails 8+ engine that provides WCAG 2.2 AA compliant design token
|
|
|
36
36
|
- **Dark/light theme** - system preference detection with localStorage persistence and manual toggle
|
|
37
37
|
- **Responsive layout** - header, sidebar navigation, main content area, and optional resizable panel
|
|
38
38
|
- **WCAG 2.2 AA compliant** - skip links, focus indicators, ARIA attributes, and 4.5:1 contrast ratios
|
|
39
|
-
- **Components** - buttons, forms, surfaces, tables, tabs, notices, badges, conversations, modals, and pagination
|
|
39
|
+
- **Components** - buttons, forms, surfaces, tables, tabs, notices, badges, conversations, modals, popovers, and pagination
|
|
40
40
|
- **Optional integrations** - Devise authentication and Pagy pagination with styled views
|
|
41
41
|
- **Customisable branding** - Override the default logos and icons and colors
|
|
42
42
|
- **Google Lighthouse** - `layered-ui-rails` scores a [perfect 100](https://github.com/layered-ai-public/layered-ui-rails/raw/refs/heads/main/test/dummy/app/assets/images/lighthouse.webp) across all four Google Lighthouse categories - performance, accessibility, best practices, and SEO
|
|
Binary file
|
|
Binary file
|
|
@@ -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
|
|
@@ -715,12 +1035,16 @@
|
|
|
715
1035
|
w-full min-h-[40px]
|
|
716
1036
|
gap-3 px-3
|
|
717
1037
|
text-sm font-medium text-foreground-muted
|
|
1038
|
+
hover:bg-surface
|
|
718
1039
|
rounded-sm
|
|
719
1040
|
focus-ring
|
|
720
1041
|
transition-colors;
|
|
721
1042
|
}
|
|
722
1043
|
|
|
723
|
-
|
|
1044
|
+
/* Repeats hover so the active background wins over the base item's
|
|
1045
|
+
hover:bg-surface - equal specificity, but this rule comes later. */
|
|
1046
|
+
.l-ui-navigation__item--active,
|
|
1047
|
+
.l-ui-navigation__item--active:hover {
|
|
724
1048
|
@apply text-foreground
|
|
725
1049
|
bg-surface-highlighted;
|
|
726
1050
|
}
|
|
@@ -1922,6 +2246,45 @@ pre.l-ui-surface {
|
|
|
1922
2246
|
px-5 py-4;
|
|
1923
2247
|
}
|
|
1924
2248
|
|
|
2249
|
+
/* Popover */
|
|
2250
|
+
|
|
2251
|
+
.l-ui-popover {
|
|
2252
|
+
@apply fixed inset-auto
|
|
2253
|
+
max-w-xs
|
|
2254
|
+
m-0 p-3
|
|
2255
|
+
text-sm text-foreground
|
|
2256
|
+
bg-background
|
|
2257
|
+
border border-border rounded-sm;
|
|
2258
|
+
}
|
|
2259
|
+
|
|
2260
|
+
/* A list of actions inside a popover (e.g. a table row's "more" menu).
|
|
2261
|
+
Negative margin cancels l-ui-popover's own padding; px-1/py-1 restore a
|
|
2262
|
+
small inset for the list itself. */
|
|
2263
|
+
.l-ui-popover__menu {
|
|
2264
|
+
@apply flex flex-col
|
|
2265
|
+
-m-3 px-1 py-1;
|
|
2266
|
+
}
|
|
2267
|
+
|
|
2268
|
+
.l-ui-popover__menu-item {
|
|
2269
|
+
@apply block w-full
|
|
2270
|
+
px-3 py-2
|
|
2271
|
+
text-sm text-left text-foreground
|
|
2272
|
+
hover:bg-surface focus:bg-surface
|
|
2273
|
+
rounded-sm
|
|
2274
|
+
focus-ring
|
|
2275
|
+
transition-colors
|
|
2276
|
+
cursor-pointer;
|
|
2277
|
+
}
|
|
2278
|
+
|
|
2279
|
+
.l-ui-popover__menu-item--danger {
|
|
2280
|
+
@apply text-danger;
|
|
2281
|
+
}
|
|
2282
|
+
|
|
2283
|
+
.l-ui-popover__menu-divider {
|
|
2284
|
+
@apply my-1
|
|
2285
|
+
border-t border-border;
|
|
2286
|
+
}
|
|
2287
|
+
|
|
1925
2288
|
/* Safari */
|
|
1926
2289
|
@media (max-width: 767px) {
|
|
1927
2290
|
/* iOS Safari ignores overflow:hidden on body; position:fixed is required */
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
module Layered
|
|
2
|
+
module Ui
|
|
3
|
+
module PopoverHelper
|
|
4
|
+
# Renders a floating panel anchored to a trigger button, built on the
|
|
5
|
+
# native HTML +popover+ attribute. Showing, hiding, light-dismiss, and
|
|
6
|
+
# Escape-to-close are all handled by the browser; the +l-ui--popover+
|
|
7
|
+
# Stimulus controller only computes placement (with auto-flip near
|
|
8
|
+
# viewport edges).
|
|
9
|
+
#
|
|
10
|
+
# <%= l_ui_popover(placement: :bottom) do |p| %>
|
|
11
|
+
# <% p.trigger(class: "l-ui-button l-ui-button--outline") do %>
|
|
12
|
+
# Options
|
|
13
|
+
# <% end %>
|
|
14
|
+
# <p>Popover content.</p>
|
|
15
|
+
# <% end %>
|
|
16
|
+
#
|
|
17
|
+
# Note: use +<% p.trigger %>+ (without the equals sign) so its content is
|
|
18
|
+
# captured by the builder rather than written to the body buffer.
|
|
19
|
+
#
|
|
20
|
+
# Options:
|
|
21
|
+
# id: (String) DOM id for the popover element; defaults to an auto-generated id.
|
|
22
|
+
# placement: (Symbol) :top, :bottom (default), :left, or :right. Flips automatically if it would overflow the viewport.
|
|
23
|
+
# align: (Symbol) :start (default) flushes the popover's leading edge with the trigger's; :end flushes its trailing edge instead
|
|
24
|
+
# (e.g. placement: :bottom, align: :end hangs the popover down and to the left of a trigger at the right end of a row).
|
|
25
|
+
# container: (Hash) Extra HTML attributes for the wrapping <div>.
|
|
26
|
+
def l_ui_popover(id: nil, placement: :bottom, align: :start, container: {}, &block)
|
|
27
|
+
id ||= "l-ui-popover-#{SecureRandom.hex(4)}"
|
|
28
|
+
builder = PopoverBuilder.new(self, id: id)
|
|
29
|
+
body_content = capture { block.call(builder) }
|
|
30
|
+
|
|
31
|
+
container_attrs = container.deep_dup
|
|
32
|
+
container_data = container_attrs[:data] || {}
|
|
33
|
+
existing_controller = container_data.delete(:controller) || container_data.delete("controller")
|
|
34
|
+
container_data[:controller] = [existing_controller, "l-ui--popover"].compact.reject(&:empty?).join(" ")
|
|
35
|
+
container_data[:"l-ui--popover-placement-value"] = placement
|
|
36
|
+
container_data[:"l-ui--popover-align-value"] = align
|
|
37
|
+
container_attrs[:data] = container_data
|
|
38
|
+
|
|
39
|
+
tag.div(**container_attrs) do
|
|
40
|
+
safe_join([
|
|
41
|
+
builder.trigger_html || ActiveSupport::SafeBuffer.new,
|
|
42
|
+
render_popover(builder, body_content)
|
|
43
|
+
])
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
class PopoverBuilder
|
|
48
|
+
attr_reader :id, :trigger_html
|
|
49
|
+
|
|
50
|
+
def initialize(view, id:)
|
|
51
|
+
@view = view
|
|
52
|
+
@id = id
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def trigger(**options, &block)
|
|
56
|
+
options = options.deep_dup
|
|
57
|
+
options[:type] ||= "button"
|
|
58
|
+
options[:popovertarget] = id
|
|
59
|
+
data = options[:data] || {}
|
|
60
|
+
data[:"l-ui--popover-target"] = "trigger"
|
|
61
|
+
options[:data] = data
|
|
62
|
+
content = @view.capture(&block)
|
|
63
|
+
@trigger_html = @view.tag.button(content, **options)
|
|
64
|
+
nil
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
private
|
|
69
|
+
|
|
70
|
+
def render_popover(builder, body_content)
|
|
71
|
+
tag.div(
|
|
72
|
+
body_content,
|
|
73
|
+
id: builder.id,
|
|
74
|
+
popover: "auto",
|
|
75
|
+
class: "l-ui-popover",
|
|
76
|
+
data: { "l-ui--popover-target" => "popover" }
|
|
77
|
+
)
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
|
|
3
|
+
// Positions a native `popover`-attribute element relative to its trigger.
|
|
4
|
+
// Showing, hiding, light-dismiss, and Escape-to-close are all handled by the
|
|
5
|
+
// browser via the popover attribute; this controller only computes placement
|
|
6
|
+
// (with auto-flip near viewport edges), since CSS anchor positioning isn't
|
|
7
|
+
// yet reliable across all supported browsers.
|
|
8
|
+
export default class extends Controller {
|
|
9
|
+
static targets = ["trigger", "popover"]
|
|
10
|
+
static values = {
|
|
11
|
+
placement: { type: String, default: "bottom" },
|
|
12
|
+
align: { type: String, default: "start" }
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
static GAP = 8
|
|
16
|
+
|
|
17
|
+
popoverTargetConnected(element) {
|
|
18
|
+
this._toggleHandler = (event) => {
|
|
19
|
+
if (event.newState === "open") {
|
|
20
|
+
this.position()
|
|
21
|
+
this._addRepositionListeners()
|
|
22
|
+
} else {
|
|
23
|
+
this._removeRepositionListeners()
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
element.addEventListener("toggle", this._toggleHandler)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
popoverTargetDisconnected(element) {
|
|
30
|
+
element.removeEventListener("toggle", this._toggleHandler)
|
|
31
|
+
this._removeRepositionListeners()
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
disconnect() {
|
|
35
|
+
this._removeRepositionListeners()
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Compute and apply the popover's fixed position relative to the trigger,
|
|
39
|
+
// flipping to the opposite side if the preferred placement would overflow
|
|
40
|
+
// the viewport.
|
|
41
|
+
position() {
|
|
42
|
+
if (!this.hasTriggerTarget || !this.hasPopoverTarget) return
|
|
43
|
+
|
|
44
|
+
const gap = this.constructor.GAP
|
|
45
|
+
const trigger = this.triggerTarget.getBoundingClientRect()
|
|
46
|
+
const popover = this.popoverTarget.getBoundingClientRect()
|
|
47
|
+
const align = this.alignValue
|
|
48
|
+
|
|
49
|
+
// The cross-axis coordinate (horizontal for top/bottom, vertical for
|
|
50
|
+
// left/right): "start" flushes the popover's leading edge with the
|
|
51
|
+
// trigger's, "end" flushes its trailing edge instead - e.g. a "bottom"
|
|
52
|
+
// placement with "end" align hangs the popover down and to the left of
|
|
53
|
+
// a trigger sitting at the right end of a row.
|
|
54
|
+
const crossAxisStart = (placement) =>
|
|
55
|
+
placement === "top" || placement === "bottom"
|
|
56
|
+
? align === "end" ? trigger.right - popover.width : trigger.left
|
|
57
|
+
: align === "end" ? trigger.bottom - popover.height : trigger.top
|
|
58
|
+
|
|
59
|
+
const coordsFor = (placement) => {
|
|
60
|
+
switch (placement) {
|
|
61
|
+
case "top":
|
|
62
|
+
return { top: trigger.top - popover.height - gap, left: crossAxisStart(placement) }
|
|
63
|
+
case "left":
|
|
64
|
+
return { top: crossAxisStart(placement), left: trigger.left - popover.width - gap }
|
|
65
|
+
case "right":
|
|
66
|
+
return { top: crossAxisStart(placement), left: trigger.right + gap }
|
|
67
|
+
default:
|
|
68
|
+
return { top: trigger.bottom + gap, left: crossAxisStart(placement) }
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
let placement = this.placementValue
|
|
73
|
+
let coords = coordsFor(placement)
|
|
74
|
+
|
|
75
|
+
if (placement === "bottom" && coords.top + popover.height > window.innerHeight) {
|
|
76
|
+
placement = "top"
|
|
77
|
+
coords = coordsFor(placement)
|
|
78
|
+
} else if (placement === "top" && coords.top < 0) {
|
|
79
|
+
placement = "bottom"
|
|
80
|
+
coords = coordsFor(placement)
|
|
81
|
+
} else if (placement === "right" && coords.left + popover.width > window.innerWidth) {
|
|
82
|
+
placement = "left"
|
|
83
|
+
coords = coordsFor(placement)
|
|
84
|
+
} else if (placement === "left" && coords.left < 0) {
|
|
85
|
+
placement = "right"
|
|
86
|
+
coords = coordsFor(placement)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
let left = coords.left
|
|
90
|
+
if (placement === "top" || placement === "bottom") {
|
|
91
|
+
left = Math.min(Math.max(gap, left), window.innerWidth - popover.width - gap)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
this.popoverTarget.style.top = `${Math.max(gap, coords.top)}px`
|
|
95
|
+
this.popoverTarget.style.left = `${left}px`
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Private
|
|
99
|
+
|
|
100
|
+
_addRepositionListeners() {
|
|
101
|
+
this._repositionHandler = () => this.position()
|
|
102
|
+
window.addEventListener("resize", this._repositionHandler)
|
|
103
|
+
window.addEventListener("scroll", this._repositionHandler, true)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
_removeRepositionListeners() {
|
|
107
|
+
if (!this._repositionHandler) return
|
|
108
|
+
window.removeEventListener("resize", this._repositionHandler)
|
|
109
|
+
window.removeEventListener("scroll", this._repositionHandler, true)
|
|
110
|
+
this._repositionHandler = null
|
|
111
|
+
}
|
|
112
|
+
}
|
|
@@ -6,6 +6,7 @@ import PanelController from "layered_ui/controllers/l_ui/panel_controller"
|
|
|
6
6
|
import PanelResizeController from "layered_ui/controllers/l_ui/panel_resize_controller"
|
|
7
7
|
import PanelButtonController from "layered_ui/controllers/l_ui/panel_button_controller"
|
|
8
8
|
import ModalController from "layered_ui/controllers/l_ui/modal_controller"
|
|
9
|
+
import PopoverController from "layered_ui/controllers/l_ui/popover_controller"
|
|
9
10
|
import SearchFormController from "layered_ui/controllers/l_ui/search_form_controller"
|
|
10
11
|
import TabsController from "layered_ui/controllers/l_ui/tabs_controller"
|
|
11
12
|
|
|
@@ -17,4 +18,5 @@ application.register("l-ui--panel", PanelController)
|
|
|
17
18
|
application.register("l-ui--panel-resize", PanelResizeController)
|
|
18
19
|
application.register("l-ui--panel-button", PanelButtonController)
|
|
19
20
|
application.register("l-ui--modal", ModalController)
|
|
21
|
+
application.register("l-ui--popover", PopoverController)
|
|
20
22
|
application.register("l-ui--tabs", TabsController)
|
|
@@ -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 %>
|
data/config/importmap.rb
CHANGED
|
@@ -13,6 +13,7 @@ pin "layered_ui/controllers/l_ui/navigation_section_controller", to: "layered_ui
|
|
|
13
13
|
pin "layered_ui/controllers/l_ui/panel_controller", to: "layered_ui/controllers/l_ui/panel_controller.js"
|
|
14
14
|
pin "layered_ui/controllers/l_ui/panel_resize_controller", to: "layered_ui/controllers/l_ui/panel_resize_controller.js"
|
|
15
15
|
pin "layered_ui/controllers/l_ui/panel_button_controller", to: "layered_ui/controllers/l_ui/panel_button_controller.js"
|
|
16
|
+
pin "layered_ui/controllers/l_ui/popover_controller", to: "layered_ui/controllers/l_ui/popover_controller.js"
|
|
16
17
|
pin "layered_ui/controllers/l_ui/search_form_controller", to: "layered_ui/controllers/l_ui/search_form_controller.js"
|
|
17
18
|
pin "layered_ui/controllers/l_ui/tabs_controller", to: "layered_ui/controllers/l_ui/tabs_controller.js"
|
|
18
19
|
pin "layered_ui/controllers/l_ui/theme_controller", to: "layered_ui/controllers/l_ui/theme_controller.js"
|
|
@@ -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
|
|
@@ -20,7 +23,7 @@ module Layered
|
|
|
20
23
|
def overrides_content
|
|
21
24
|
<<~CSS
|
|
22
25
|
/*
|
|
23
|
-
* layered-ui-rails color overrides
|
|
26
|
+
* layered-ui-rails color overrides (generated from v#{Layered::Ui::VERSION})
|
|
24
27
|
*
|
|
25
28
|
* Uncomment and edit any variable below to customise the UI.
|
|
26
29
|
* This file is NOT overwritten by the install generator, so your
|
|
@@ -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
|
data/lib/layered/ui/engine.rb
CHANGED
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.20.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
|
|
@@ -201,6 +203,7 @@ files:
|
|
|
201
203
|
- app/assets/images/layered_ui/icon_linkedin.svg
|
|
202
204
|
- app/assets/images/layered_ui/icon_mail.svg
|
|
203
205
|
- app/assets/images/layered_ui/icon_moon.svg
|
|
206
|
+
- app/assets/images/layered_ui/icon_more.svg
|
|
204
207
|
- app/assets/images/layered_ui/icon_panel_close.svg
|
|
205
208
|
- app/assets/images/layered_ui/icon_sun.svg
|
|
206
209
|
- app/assets/images/layered_ui/icon_x.svg
|
|
@@ -218,6 +221,7 @@ files:
|
|
|
218
221
|
- app/helpers/layered/ui/modal_helper.rb
|
|
219
222
|
- app/helpers/layered/ui/navigation_helper.rb
|
|
220
223
|
- app/helpers/layered/ui/pagy_helper.rb
|
|
224
|
+
- app/helpers/layered/ui/popover_helper.rb
|
|
221
225
|
- app/helpers/layered/ui/ransack_helper.rb
|
|
222
226
|
- app/helpers/layered/ui/table_helper.rb
|
|
223
227
|
- app/helpers/layered/ui/title_bar_helper.rb
|
|
@@ -227,6 +231,7 @@ files:
|
|
|
227
231
|
- app/javascript/layered_ui/controllers/l_ui/panel_button_controller.js
|
|
228
232
|
- app/javascript/layered_ui/controllers/l_ui/panel_controller.js
|
|
229
233
|
- app/javascript/layered_ui/controllers/l_ui/panel_resize_controller.js
|
|
234
|
+
- app/javascript/layered_ui/controllers/l_ui/popover_controller.js
|
|
230
235
|
- app/javascript/layered_ui/controllers/l_ui/search_form_controller.js
|
|
231
236
|
- app/javascript/layered_ui/controllers/l_ui/tabs_controller.js
|
|
232
237
|
- app/javascript/layered_ui/controllers/l_ui/theme_controller.js
|