layered-ui-rails 0.19.0 → 0.21.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 +5 -0
- data/.claude/skills/layered-ui-rails/references/CONTROLLERS.md +25 -0
- data/.claude/skills/layered-ui-rails/references/CSS.md +24 -0
- data/.claude/skills/layered-ui-rails/references/HELPERS.md +80 -0
- data/CHANGELOG.md +20 -0
- data/README.md +1 -1
- data/app/assets/images/layered_ui/icon_close_circle.svg +3 -0
- data/app/assets/images/layered_ui/icon_more.svg +5 -0
- data/app/assets/tailwind/layered_ui/engine.css +100 -1
- data/app/helpers/layered/ui/popover_helper.rb +81 -0
- data/app/helpers/layered/ui/tag_helper.rb +171 -0
- data/app/javascript/layered_ui/controllers/l_ui/popover_controller.js +112 -0
- data/app/javascript/layered_ui/index.js +2 -0
- data/config/importmap.rb +1 -0
- data/lib/generators/layered/ui/create_overrides_generator.rb +1 -1
- data/lib/layered/ui/engine.rb +2 -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: 9ced93c0df2346914e675fcdf3dd10657a079d3dcc046271dfb6bb24e6ff2c8c
|
|
4
|
+
data.tar.gz: 5e64064b0fdb591d0621118350bf5c1b181c88de7799a02a9b7f2e7e628f5ad0
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9408f35f4b9b2d2a57d0227ec06cbf55a8f8c9ac692d97e1a0a6e9765f0b369bf9732aea5d8742b4c454c1cc555e258303abb2e57f878c352db18b5416fb454b
|
|
7
|
+
data.tar.gz: e24b88f24d9f70192bdfa75166f287da9bb01165c6af436b25efc812e7d83e1c5df2cd915896a75aa5c26a4c699c2fbcd3ac6661a0f8f0888d94d8366daac241
|
|
@@ -181,6 +181,8 @@ 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 |
|
|
185
|
+
| `l_ui_tag(rounded: false, container: {})` | Interactive tag built from segments (`t.text`, `t.button`, `t.link`, `t.remove`) with an optional attached popover (`t.popover`); `rounded: true` for a pill shape |
|
|
184
186
|
|
|
185
187
|
## CSS classes
|
|
186
188
|
|
|
@@ -199,9 +201,11 @@ Key components:
|
|
|
199
201
|
| Forms | `.l-ui-form`, `.l-ui-form__group`, `.l-ui-form__field`, `.l-ui-label`, `.l-ui-select` |
|
|
200
202
|
| Tables | `.l-ui-table`, `.l-ui-table__header`, `.l-ui-table__cell`, `--primary`, `--action`, `.l-ui-table__action`, `--danger` |
|
|
201
203
|
| Badges | `.l-ui-badge`, `--rounded`, `--default`, `--success`, `--warning`, `--danger` |
|
|
204
|
+
| Tags | `.l-ui-tag`, `--rounded`, `__text`, `__button`, `__remove` |
|
|
202
205
|
| Notices | `.l-ui-notice` (base), `--success`, `--warning`, `--error` |
|
|
203
206
|
| Tabs | `.l-ui-tabs`, `.l-ui-tabs__list`, `.l-ui-tabs__tab`, `--active` |
|
|
204
207
|
| Modal | `.l-ui-modal`, `.l-ui-modal__header`, `.l-ui-modal__body` |
|
|
208
|
+
| Popover | `.l-ui-popover` |
|
|
205
209
|
|
|
206
210
|
## Stimulus controllers
|
|
207
211
|
|
|
@@ -217,6 +221,7 @@ All controllers use the `l-ui--` namespace and are auto-registered via importmap
|
|
|
217
221
|
| Panel resize | `l-ui--panel-resize` | Panel width drag handle |
|
|
218
222
|
| Modal | `l-ui--modal` | Native `<dialog>` with focus trap |
|
|
219
223
|
| Tabs | `l-ui--tabs` | Accessible tabbed interface |
|
|
224
|
+
| Popover | `l-ui--popover` | Positions a native `popover`-attribute element relative to its trigger, with auto-flip |
|
|
220
225
|
| Search form | `l-ui--search-form` | Multi-scope search with Turbo support and pagination param preservation |
|
|
221
226
|
|
|
222
227
|
## 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.
|
|
@@ -269,6 +269,20 @@ Always combine the base block with one variant modifier (e.g. `class="l-ui-notic
|
|
|
269
269
|
|
|
270
270
|
Always combine the base block with a modifier, e.g. `<span class="l-ui-badge l-ui-badge--success">`.
|
|
271
271
|
|
|
272
|
+
## Tags
|
|
273
|
+
|
|
274
|
+
Interactive tag - e.g. an email recipient or an active filter. A badge is a static styled span; a tag is a container with separately interactive segments.
|
|
275
|
+
|
|
276
|
+
```
|
|
277
|
+
.l-ui-tag Tag container (div); carries colour and shape but no padding - segments supply it
|
|
278
|
+
.l-ui-tag--rounded Pill shape (matching l-ui-badge--rounded); default is the subtle badge radius
|
|
279
|
+
.l-ui-tag__text Static label segment (span)
|
|
280
|
+
.l-ui-tag__button Interactive label segment (button or a); focus ring + pointer
|
|
281
|
+
.l-ui-tag__remove Trailing remove segment (button or a) holding a ✕ icon (e.g. l-ui-icon--xs); needs an aria-label
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
A tag takes the same subtle corner radius as a badge by default; add `l-ui-tag--rounded` for a pill. Segments must be direct children in label-then-remove order; when a remove segment follows, the label segment automatically drops its right padding so the remove owns the gap and every part of the tag stays clickable. Prefer the `l_ui_tag` helper (see HELPERS.md), which also wires an optional popover.
|
|
285
|
+
|
|
272
286
|
## Tabs
|
|
273
287
|
|
|
274
288
|
```
|
|
@@ -287,6 +301,16 @@ Always combine the base block with a modifier, e.g. `<span class="l-ui-badge l-u
|
|
|
287
301
|
.l-ui-modal__body Scrollable modal content
|
|
288
302
|
```
|
|
289
303
|
|
|
304
|
+
## Popover
|
|
305
|
+
|
|
306
|
+
```
|
|
307
|
+
.l-ui-popover popover-attribute element; positioned by the l-ui--popover Stimulus controller
|
|
308
|
+
.l-ui-popover__menu Full-width list wrapper for a menu of actions inside a popover
|
|
309
|
+
.l-ui-popover__menu-item Full-width action link/button inside l-ui-popover__menu
|
|
310
|
+
.l-ui-popover__menu-item--danger Danger styling for a destructive menu item
|
|
311
|
+
.l-ui-popover__menu-divider Horizontal rule separating groups of items (apply to an <hr>)
|
|
312
|
+
```
|
|
313
|
+
|
|
290
314
|
## Breadcrumbs
|
|
291
315
|
|
|
292
316
|
```
|
|
@@ -322,6 +322,86 @@ 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
|
+
|
|
370
|
+
## Tag
|
|
371
|
+
|
|
372
|
+
```ruby
|
|
373
|
+
l_ui_tag(rounded: false, container: {}, &block)
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
Renders an interactive tag (`class="l-ui-tag"`) - e.g. an email recipient or an active filter. Visually related to `l-ui-badge` but a distinct control: a badge is a static styled span, while a tag is a container whose segments (label, remove) are separately interactive.
|
|
377
|
+
|
|
378
|
+
- `rounded` (Boolean, optional) - pill shape (matching `l-ui-badge--rounded`); defaults to `false` for the subtle badge radius
|
|
379
|
+
- `container` (Hash, optional) - extra HTML attributes for the wrapping `<div>` (e.g. `class:`)
|
|
380
|
+
- `&block` - declare segments on the builder; they render in call order. Use `<%` (not `<%=`) - segment content is captured by the builder
|
|
381
|
+
|
|
382
|
+
Builder methods:
|
|
383
|
+
|
|
384
|
+
- `t.text(content = nil, **opts, &block)` - static label segment (`<span class="l-ui-tag__text">`)
|
|
385
|
+
- `t.button(**opts, &block)` - button segment (`<button class="l-ui-tag__button">`); opens the tag's popover when one is declared
|
|
386
|
+
- `t.link(url, **opts, &block)` - link segment, styled like a button segment
|
|
387
|
+
- `t.remove(url = nil, **opts, &block)` - trailing remove segment (`l-ui-tag__remove`): a link when `url` is given, otherwise a button. Renders a ✕ icon unless the block supplies custom content. Pass `aria: { label: ... }` so it has an accessible name
|
|
388
|
+
- `t.popover(id: nil, placement: :bottom, align: :start, &block)` - attaches a popover; the block is the popover body. Options match `l_ui_popover`
|
|
389
|
+
|
|
390
|
+
```erb
|
|
391
|
+
<%= l_ui_tag do |t| %>
|
|
392
|
+
<% t.button(aria: { label: "Edit status filter" }) do %>
|
|
393
|
+
Status: Active
|
|
394
|
+
<% end %>
|
|
395
|
+
<% t.remove remove_filter_path, aria: { label: "Remove status filter" },
|
|
396
|
+
data: { turbo_frame: "results" } %>
|
|
397
|
+
<% t.popover do %>
|
|
398
|
+
<p>Filter controls.</p>
|
|
399
|
+
<% end %>
|
|
400
|
+
<% end %>
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
When a popover is declared, the tag container itself becomes the `l-ui--popover` controller root and placement target - the popover aligns with the whole tag rather than the label button inside it - and button segments are wired to open it via `popovertarget`. A tag without a popover renders no Stimulus wiring.
|
|
404
|
+
|
|
325
405
|
## Header
|
|
326
406
|
|
|
327
407
|
```ruby
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,26 @@
|
|
|
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.21.0] - 2026-07-09
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Tags: interactive tags (`l-ui-tag`), such as email recipients or active filters. Visually related to badges but a distinct control: a badge is a static styled span, while a tag is a container whose segments - `l-ui-tag__text` (static label), `l-ui-tag__button` (button or link label), and `l-ui-tag__remove` (trailing ✕ link or button) - are separately interactive, each carrying its share of the tag's padding so every part is a hit target. Takes the same subtle corner radius as a badge by default, with `l-ui-tag--rounded` for a pill shape (matching `l-ui-badge--rounded`). The `l_ui_tag` helper builds the tag from builder calls (`t.text`, `t.button`, `t.link`, `t.remove`, `t.popover`) and takes a `rounded:` option; declaring `t.popover` makes the whole tag the popover's placement target and wires button segments to open it via `popovertarget`. The default `t.remove` control is a filled x-circle.
|
|
10
|
+
- `icon_close_circle.svg` (a filled circle with a cross knocked out) added to the bundled icon set, used by the tag remove control and available as a general-purpose icon.
|
|
11
|
+
|
|
12
|
+
## [0.20.0] - 2026-07-08
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
|
|
16
|
+
- 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.
|
|
17
|
+
- 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.
|
|
18
|
+
- `icon_more.svg` (vertical ellipsis) added to the bundled icon set, for "more actions" popover triggers.
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
|
|
22
|
+
- 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.
|
|
23
|
+
- The overrides generator now stamps the gem version it was generated from into the overrides file's header comment.
|
|
24
|
+
|
|
5
25
|
## [0.19.0] - 2026-06-15
|
|
6
26
|
|
|
7
27
|
### Added
|
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, tags, 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
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
<svg fill="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
|
2
|
+
<path fill-rule="evenodd" clip-rule="evenodd" d="M12 2.25c-5.385 0-9.75 4.365-9.75 9.75s4.365 9.75 9.75 9.75 9.75-4.365 9.75-9.75S17.385 2.25 12 2.25Zm-1.72 6.97a.75.75 0 0 0-1.06 1.06L10.94 12l-1.72 1.72a.75.75 0 1 0 1.06 1.06L12 13.06l1.72 1.72a.75.75 0 1 0 1.06-1.06L13.06 12l1.72-1.72a.75.75 0 1 0-1.06-1.06L12 10.94l-1.72-1.72Z" />
|
|
3
|
+
</svg>
|
|
@@ -1035,12 +1035,16 @@
|
|
|
1035
1035
|
w-full min-h-[40px]
|
|
1036
1036
|
gap-3 px-3
|
|
1037
1037
|
text-sm font-medium text-foreground-muted
|
|
1038
|
+
hover:bg-surface
|
|
1038
1039
|
rounded-sm
|
|
1039
1040
|
focus-ring
|
|
1040
1041
|
transition-colors;
|
|
1041
1042
|
}
|
|
1042
1043
|
|
|
1043
|
-
|
|
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 {
|
|
1044
1048
|
@apply text-foreground
|
|
1045
1049
|
bg-surface-highlighted;
|
|
1046
1050
|
}
|
|
@@ -1851,6 +1855,62 @@ pre.l-ui-surface {
|
|
|
1851
1855
|
bg-error-bg;
|
|
1852
1856
|
}
|
|
1853
1857
|
|
|
1858
|
+
/* Tags */
|
|
1859
|
+
|
|
1860
|
+
/* An interactive tag - e.g. an email recipient or an active filter. Related
|
|
1861
|
+
to the badge visually, but a distinct control: a badge is a static styled
|
|
1862
|
+
span, while a tag is a container with interactive segments. The container
|
|
1863
|
+
carries no padding of its own - each segment supplies its share, so every
|
|
1864
|
+
part of the tag is a hit target. */
|
|
1865
|
+
|
|
1866
|
+
.l-ui-tag {
|
|
1867
|
+
@apply inline-flex items-stretch
|
|
1868
|
+
min-h-[32px]
|
|
1869
|
+
text-xs font-medium
|
|
1870
|
+
text-foreground-muted
|
|
1871
|
+
bg-surface-highlighted
|
|
1872
|
+
rounded;
|
|
1873
|
+
}
|
|
1874
|
+
|
|
1875
|
+
/* Pill shape, matching l-ui-badge--rounded. */
|
|
1876
|
+
.l-ui-tag--rounded {
|
|
1877
|
+
@apply !rounded-full;
|
|
1878
|
+
}
|
|
1879
|
+
|
|
1880
|
+
.l-ui-tag__text {
|
|
1881
|
+
@apply flex items-center
|
|
1882
|
+
gap-1 px-3;
|
|
1883
|
+
}
|
|
1884
|
+
|
|
1885
|
+
.l-ui-tag__button {
|
|
1886
|
+
@apply flex items-center
|
|
1887
|
+
gap-1 px-3
|
|
1888
|
+
rounded
|
|
1889
|
+
focus-ring
|
|
1890
|
+
cursor-pointer;
|
|
1891
|
+
}
|
|
1892
|
+
|
|
1893
|
+
/* Segment focus rings follow the tag's shape. */
|
|
1894
|
+
.l-ui-tag--rounded .l-ui-tag__button,
|
|
1895
|
+
.l-ui-tag--rounded .l-ui-tag__remove {
|
|
1896
|
+
@apply !rounded-full;
|
|
1897
|
+
}
|
|
1898
|
+
|
|
1899
|
+
/* When a remove segment follows, it owns the gap between the label and the
|
|
1900
|
+
✕ (its left padding), so the label segment gives up its right padding. */
|
|
1901
|
+
.l-ui-tag__text:has(+ .l-ui-tag__remove),
|
|
1902
|
+
.l-ui-tag__button:has(+ .l-ui-tag__remove) {
|
|
1903
|
+
@apply pr-0;
|
|
1904
|
+
}
|
|
1905
|
+
|
|
1906
|
+
.l-ui-tag__remove {
|
|
1907
|
+
@apply flex items-center
|
|
1908
|
+
pl-1.5 pr-2
|
|
1909
|
+
rounded
|
|
1910
|
+
focus-ring
|
|
1911
|
+
cursor-pointer;
|
|
1912
|
+
}
|
|
1913
|
+
|
|
1854
1914
|
/* Panel */
|
|
1855
1915
|
|
|
1856
1916
|
.l-ui-panel__button {
|
|
@@ -2242,6 +2302,45 @@ pre.l-ui-surface {
|
|
|
2242
2302
|
px-5 py-4;
|
|
2243
2303
|
}
|
|
2244
2304
|
|
|
2305
|
+
/* Popover */
|
|
2306
|
+
|
|
2307
|
+
.l-ui-popover {
|
|
2308
|
+
@apply fixed inset-auto
|
|
2309
|
+
max-w-xs
|
|
2310
|
+
m-0 p-3
|
|
2311
|
+
text-sm text-foreground
|
|
2312
|
+
bg-background
|
|
2313
|
+
border border-border rounded-sm;
|
|
2314
|
+
}
|
|
2315
|
+
|
|
2316
|
+
/* A list of actions inside a popover (e.g. a table row's "more" menu).
|
|
2317
|
+
Negative margin cancels l-ui-popover's own padding; px-1/py-1 restore a
|
|
2318
|
+
small inset for the list itself. */
|
|
2319
|
+
.l-ui-popover__menu {
|
|
2320
|
+
@apply flex flex-col
|
|
2321
|
+
-m-3 px-1 py-1;
|
|
2322
|
+
}
|
|
2323
|
+
|
|
2324
|
+
.l-ui-popover__menu-item {
|
|
2325
|
+
@apply block w-full
|
|
2326
|
+
px-3 py-2
|
|
2327
|
+
text-sm text-left text-foreground
|
|
2328
|
+
hover:bg-surface focus:bg-surface
|
|
2329
|
+
rounded-sm
|
|
2330
|
+
focus-ring
|
|
2331
|
+
transition-colors
|
|
2332
|
+
cursor-pointer;
|
|
2333
|
+
}
|
|
2334
|
+
|
|
2335
|
+
.l-ui-popover__menu-item--danger {
|
|
2336
|
+
@apply text-danger;
|
|
2337
|
+
}
|
|
2338
|
+
|
|
2339
|
+
.l-ui-popover__menu-divider {
|
|
2340
|
+
@apply my-1
|
|
2341
|
+
border-t border-border;
|
|
2342
|
+
}
|
|
2343
|
+
|
|
2245
2344
|
/* Safari */
|
|
2246
2345
|
@media (max-width: 767px) {
|
|
2247
2346
|
/* 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,171 @@
|
|
|
1
|
+
module Layered
|
|
2
|
+
module Ui
|
|
3
|
+
module TagHelper
|
|
4
|
+
# Renders an interactive tag - e.g. an email recipient or an active
|
|
5
|
+
# filter. Visually related to +l-ui-badge+, but a distinct control: a
|
|
6
|
+
# badge is a static styled span, while a tag is a container whose
|
|
7
|
+
# segments (label, remove) are separately interactive.
|
|
8
|
+
#
|
|
9
|
+
# <%= l_ui_tag do |t| %>
|
|
10
|
+
# <% t.text "alice@example.com" %>
|
|
11
|
+
# <% t.remove aria: { label: "Remove alice@example.com" } %>
|
|
12
|
+
# <% end %>
|
|
13
|
+
#
|
|
14
|
+
# A tag may carry a popover. The whole tag becomes the placement target
|
|
15
|
+
# (so the popover aligns with the tag rather than the segment inside
|
|
16
|
+
# it), and button segments open it via +popovertarget+:
|
|
17
|
+
#
|
|
18
|
+
# <%= l_ui_tag do |t| %>
|
|
19
|
+
# <% t.button(aria: { label: "Edit status filter" }) do %>
|
|
20
|
+
# Status: Active
|
|
21
|
+
# <% end %>
|
|
22
|
+
# <% t.remove remove_filter_path, aria: { label: "Remove status filter" } %>
|
|
23
|
+
# <% t.popover do %>
|
|
24
|
+
# <p>Filter controls.</p>
|
|
25
|
+
# <% end %>
|
|
26
|
+
# <% end %>
|
|
27
|
+
#
|
|
28
|
+
# Note: use +<% t.button %>+ (without the equals sign) so segment content
|
|
29
|
+
# is captured by the builder rather than written to the body buffer.
|
|
30
|
+
#
|
|
31
|
+
# Segments render in the order they are declared; the popover element is
|
|
32
|
+
# always appended last. Give icon-only segments (such as +t.remove+) an
|
|
33
|
+
# +aria-label+ so they have an accessible name.
|
|
34
|
+
#
|
|
35
|
+
# Options:
|
|
36
|
+
# rounded: (Boolean) Pill shape (matching +l-ui-badge--rounded+); defaults to false for the subtle badge shape.
|
|
37
|
+
# container: (Hash) Extra HTML attributes for the wrapping <div>.
|
|
38
|
+
#
|
|
39
|
+
# Builder methods:
|
|
40
|
+
# t.text(content = nil, **options, &block) Static label segment (<span>).
|
|
41
|
+
# t.button(**options, &block) Button segment; opens the tag's popover when one is declared.
|
|
42
|
+
# t.link(url, **options, &block) Link segment, styled like a button segment.
|
|
43
|
+
# t.remove(url = nil, **options, &block) Trailing remove segment: a link when +url+ is given, otherwise a
|
|
44
|
+
# button. Renders a ✕ icon unless the block supplies custom content.
|
|
45
|
+
# t.popover(id: nil, placement: :bottom, align: :start, &block)
|
|
46
|
+
# Attaches a popover to the tag; the block is the popover body.
|
|
47
|
+
# Options match +l_ui_popover+.
|
|
48
|
+
def l_ui_tag(rounded: false, container: {}, &block)
|
|
49
|
+
builder = TagBuilder.new(self)
|
|
50
|
+
capture { block.call(builder) }
|
|
51
|
+
|
|
52
|
+
container_attrs = container.deep_dup
|
|
53
|
+
container_attrs[:class] = class_names("l-ui-tag", ("l-ui-tag--rounded" if rounded), container_attrs[:class])
|
|
54
|
+
|
|
55
|
+
if builder.popover?
|
|
56
|
+
container_data = container_attrs[:data] || {}
|
|
57
|
+
existing_controller = container_data.delete(:controller) || container_data.delete("controller")
|
|
58
|
+
container_data[:controller] = [existing_controller, "l-ui--popover"].compact.reject(&:empty?).join(" ")
|
|
59
|
+
container_data[:"l-ui--popover-target"] = "trigger"
|
|
60
|
+
container_data[:"l-ui--popover-placement-value"] = builder.popover_placement
|
|
61
|
+
container_data[:"l-ui--popover-align-value"] = builder.popover_align
|
|
62
|
+
container_attrs[:data] = container_data
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
tag.div(**container_attrs) do
|
|
66
|
+
parts = builder.segments.map { |segment| l_ui_tag_segment(builder, segment) }
|
|
67
|
+
parts << l_ui_tag_popover(builder) if builder.popover?
|
|
68
|
+
safe_join(parts)
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
class TagBuilder
|
|
73
|
+
attr_reader :segments, :popover_id, :popover_placement, :popover_align, :popover_body
|
|
74
|
+
|
|
75
|
+
def initialize(view)
|
|
76
|
+
@view = view
|
|
77
|
+
@segments = []
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def text(content = nil, **options, &block)
|
|
81
|
+
content = @view.capture(&block) if block
|
|
82
|
+
@segments << { kind: :text, content: content, options: options }
|
|
83
|
+
nil
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def button(**options, &block)
|
|
87
|
+
content = block ? @view.capture(&block) : nil
|
|
88
|
+
@segments << { kind: :button, content: content, options: options }
|
|
89
|
+
nil
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def link(url, **options, &block)
|
|
93
|
+
content = block ? @view.capture(&block) : nil
|
|
94
|
+
@segments << { kind: :link, url: url, content: content, options: options }
|
|
95
|
+
nil
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def remove(url = nil, **options, &block)
|
|
99
|
+
content = block ? @view.capture(&block) : nil
|
|
100
|
+
@segments << { kind: :remove, url: url, content: content, options: options }
|
|
101
|
+
nil
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def popover(id: nil, placement: :bottom, align: :start, &block)
|
|
105
|
+
@popover_id = id || "l-ui-tag-popover-#{SecureRandom.hex(4)}"
|
|
106
|
+
@popover_placement = placement
|
|
107
|
+
@popover_align = align
|
|
108
|
+
@popover_body = @view.capture(&block)
|
|
109
|
+
nil
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def popover?
|
|
113
|
+
!@popover_id.nil?
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
private
|
|
118
|
+
|
|
119
|
+
def l_ui_tag_segment(builder, segment)
|
|
120
|
+
options = segment[:options].deep_dup
|
|
121
|
+
|
|
122
|
+
case segment[:kind]
|
|
123
|
+
when :text
|
|
124
|
+
options[:class] = class_names("l-ui-tag__text", options[:class])
|
|
125
|
+
tag.span(segment[:content], **options)
|
|
126
|
+
when :button
|
|
127
|
+
options[:class] = class_names("l-ui-tag__button", options[:class])
|
|
128
|
+
options[:type] ||= "button"
|
|
129
|
+
options[:popovertarget] ||= builder.popover_id if builder.popover?
|
|
130
|
+
tag.button(segment[:content], **options)
|
|
131
|
+
when :link
|
|
132
|
+
options[:class] = class_names("l-ui-tag__button", options[:class])
|
|
133
|
+
link_to(segment[:content], segment[:url], options)
|
|
134
|
+
when :remove
|
|
135
|
+
options[:class] = class_names("l-ui-tag__remove", options[:class])
|
|
136
|
+
content = segment[:content] || l_ui_tag_remove_icon
|
|
137
|
+
if segment[:url]
|
|
138
|
+
link_to(content, segment[:url], options)
|
|
139
|
+
else
|
|
140
|
+
options[:type] ||= "button"
|
|
141
|
+
tag.button(content, **options)
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# Filled x-circle, matching the bundled +icon_close_circle.svg+ asset.
|
|
147
|
+
# Rendered inline (rather than via image_tag) so it inherits the tag's
|
|
148
|
+
# muted +currentColor+; an <img>-loaded SVG cannot.
|
|
149
|
+
def l_ui_tag_remove_icon
|
|
150
|
+
tag.svg(
|
|
151
|
+
tag.path("fill-rule" => "evenodd", "clip-rule" => "evenodd",
|
|
152
|
+
"d" => "M12 2.25c-5.385 0-9.75 4.365-9.75 9.75s4.365 9.75 9.75 9.75 9.75-4.365 9.75-9.75S17.385 2.25 12 2.25Zm-1.72 6.97a.75.75 0 0 0-1.06 1.06L10.94 12l-1.72 1.72a.75.75 0 1 0 1.06 1.06L12 13.06l1.72 1.72a.75.75 0 1 0 1.06-1.06L13.06 12l1.72-1.72a.75.75 0 1 0-1.06-1.06L12 10.94l-1.72-1.72Z"),
|
|
153
|
+
class: "l-ui-icon--sm",
|
|
154
|
+
fill: "currentColor",
|
|
155
|
+
"viewBox" => "0 0 24 24",
|
|
156
|
+
"aria-hidden" => "true"
|
|
157
|
+
)
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def l_ui_tag_popover(builder)
|
|
161
|
+
tag.div(
|
|
162
|
+
builder.popover_body,
|
|
163
|
+
id: builder.popover_id,
|
|
164
|
+
popover: "auto",
|
|
165
|
+
class: "l-ui-popover",
|
|
166
|
+
data: { "l-ui--popover-target" => "popover" }
|
|
167
|
+
)
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
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)
|
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"
|
|
@@ -23,7 +23,7 @@ module Layered
|
|
|
23
23
|
def overrides_content
|
|
24
24
|
<<~CSS
|
|
25
25
|
/*
|
|
26
|
-
* layered-ui-rails color overrides
|
|
26
|
+
* layered-ui-rails color overrides (generated from v#{Layered::Ui::VERSION})
|
|
27
27
|
*
|
|
28
28
|
* Uncomment and edit any variable below to customise the UI.
|
|
29
29
|
* This file is NOT overwritten by the install generator, so your
|
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.21.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- layered.ai
|
|
@@ -193,6 +193,7 @@ files:
|
|
|
193
193
|
- app/assets/images/layered_ui/icon_chevron_down.svg
|
|
194
194
|
- app/assets/images/layered_ui/icon_chevron_right.svg
|
|
195
195
|
- app/assets/images/layered_ui/icon_close.svg
|
|
196
|
+
- app/assets/images/layered_ui/icon_close_circle.svg
|
|
196
197
|
- app/assets/images/layered_ui/icon_dark.svg
|
|
197
198
|
- app/assets/images/layered_ui/icon_discord.svg
|
|
198
199
|
- app/assets/images/layered_ui/icon_github.svg
|
|
@@ -203,6 +204,7 @@ files:
|
|
|
203
204
|
- app/assets/images/layered_ui/icon_linkedin.svg
|
|
204
205
|
- app/assets/images/layered_ui/icon_mail.svg
|
|
205
206
|
- app/assets/images/layered_ui/icon_moon.svg
|
|
207
|
+
- app/assets/images/layered_ui/icon_more.svg
|
|
206
208
|
- app/assets/images/layered_ui/icon_panel_close.svg
|
|
207
209
|
- app/assets/images/layered_ui/icon_sun.svg
|
|
208
210
|
- app/assets/images/layered_ui/icon_x.svg
|
|
@@ -220,8 +222,10 @@ files:
|
|
|
220
222
|
- app/helpers/layered/ui/modal_helper.rb
|
|
221
223
|
- app/helpers/layered/ui/navigation_helper.rb
|
|
222
224
|
- app/helpers/layered/ui/pagy_helper.rb
|
|
225
|
+
- app/helpers/layered/ui/popover_helper.rb
|
|
223
226
|
- app/helpers/layered/ui/ransack_helper.rb
|
|
224
227
|
- app/helpers/layered/ui/table_helper.rb
|
|
228
|
+
- app/helpers/layered/ui/tag_helper.rb
|
|
225
229
|
- app/helpers/layered/ui/title_bar_helper.rb
|
|
226
230
|
- app/javascript/layered_ui/controllers/l_ui/modal_controller.js
|
|
227
231
|
- app/javascript/layered_ui/controllers/l_ui/navigation_controller.js
|
|
@@ -229,6 +233,7 @@ files:
|
|
|
229
233
|
- app/javascript/layered_ui/controllers/l_ui/panel_button_controller.js
|
|
230
234
|
- app/javascript/layered_ui/controllers/l_ui/panel_controller.js
|
|
231
235
|
- app/javascript/layered_ui/controllers/l_ui/panel_resize_controller.js
|
|
236
|
+
- app/javascript/layered_ui/controllers/l_ui/popover_controller.js
|
|
232
237
|
- app/javascript/layered_ui/controllers/l_ui/search_form_controller.js
|
|
233
238
|
- app/javascript/layered_ui/controllers/l_ui/tabs_controller.js
|
|
234
239
|
- app/javascript/layered_ui/controllers/l_ui/theme_controller.js
|