layered-ui-rails 0.20.0 → 0.22.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 +2 -0
- data/.claude/skills/layered-ui-rails/references/CONTROLLERS.md +19 -1
- data/.claude/skills/layered-ui-rails/references/CSS.md +20 -0
- data/.claude/skills/layered-ui-rails/references/HELPERS.md +44 -3
- data/CHANGELOG.md +17 -0
- data/README.md +1 -1
- data/app/assets/images/layered_ui/icon_close_circle.svg +3 -0
- data/app/assets/tailwind/layered_ui/engine.css +132 -1
- data/app/helpers/layered/ui/popover_helper.rb +5 -1
- data/app/helpers/layered/ui/table_helper.rb +14 -3
- data/app/helpers/layered/ui/tag_helper.rb +180 -0
- data/app/javascript/layered_ui/controllers/l_ui/popover_controller.js +12 -1
- data/app/javascript/layered_ui/controllers/l_ui/scroll_hint_controller.js +46 -0
- data/app/javascript/layered_ui/index.js +2 -0
- data/config/importmap.rb +1 -0
- data/lib/layered/ui/engine.rb +1 -0
- data/lib/layered/ui/version.rb +1 -1
- metadata +4 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2371574fe91ff33cc9926d8e5fccf86afd408b90f61dc570514bd86d5f1bb149
|
|
4
|
+
data.tar.gz: ef320467046f71ad12c3b16f0f738188f3a5b40cf705d50e9fe8f11ea1512f6e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f5ea052a4964edc0edea159e1179aee36a32ec491c2cf39ed09de01af8be343e0bb289f9e5e911d5e95a887379357fa284966ff6d4471edeaa16f3c2cd3dc81b
|
|
7
|
+
data.tar.gz: 31a3897b1c16acc5923b6a442004f4f2ccc9e9f731270724ce39739e014df8afef062af1516a8a95aeb009b35727e602129bdc7cdbc8fa024d072c527dabdad8
|
|
@@ -182,6 +182,7 @@ Quick reference:
|
|
|
182
182
|
| `l_ui_authentication` | Default header login/register buttons (Devise) |
|
|
183
183
|
| `l_ui_navigation_toggle` | Default header sidebar toggle button |
|
|
184
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 |
|
|
185
186
|
|
|
186
187
|
## CSS classes
|
|
187
188
|
|
|
@@ -200,6 +201,7 @@ Key components:
|
|
|
200
201
|
| Forms | `.l-ui-form`, `.l-ui-form__group`, `.l-ui-form__field`, `.l-ui-label`, `.l-ui-select` |
|
|
201
202
|
| Tables | `.l-ui-table`, `.l-ui-table__header`, `.l-ui-table__cell`, `--primary`, `--action`, `.l-ui-table__action`, `--danger` |
|
|
202
203
|
| Badges | `.l-ui-badge`, `--rounded`, `--default`, `--success`, `--warning`, `--danger` |
|
|
204
|
+
| Tags | `.l-ui-tag`, `--rounded`, `__text`, `__button`, `__remove` |
|
|
203
205
|
| Notices | `.l-ui-notice` (base), `--success`, `--warning`, `--error` |
|
|
204
206
|
| Tabs | `.l-ui-tabs`, `.l-ui-tabs__list`, `.l-ui-tabs__tab`, `--active` |
|
|
205
207
|
| Modal | `.l-ui-modal`, `.l-ui-modal__header`, `.l-ui-modal__body` |
|
|
@@ -111,7 +111,7 @@ Accessible tabbed interface with keyboard navigation.
|
|
|
111
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
112
|
|
|
113
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)
|
|
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), `open` (Boolean, default `false`; when true the popover opens as soon as the controller connects, shown and positioned in the same task so it never paints unpositioned - e.g. re-opening a filter popover after a form submission re-renders the page)
|
|
115
115
|
|
|
116
116
|
```html
|
|
117
117
|
<div data-controller="l-ui--popover">
|
|
@@ -130,6 +130,7 @@ Positions a native `popover`-attribute element relative to its trigger, with aut
|
|
|
130
130
|
Features:
|
|
131
131
|
- Repositions on open, and while open on window resize/scroll
|
|
132
132
|
- Flips to the opposite side if the preferred placement would overflow the viewport
|
|
133
|
+
- `data-l-ui--popover-open-value="true"` opens the popover on connect without a flash of unpositioned content
|
|
133
134
|
|
|
134
135
|
## Panel (`l-ui--panel`)
|
|
135
136
|
|
|
@@ -202,3 +203,20 @@ When multiple search forms exist on one page (each with a different `scope` valu
|
|
|
202
203
|
<%= l_ui_pagy(@users_pagy) %>
|
|
203
204
|
<% end %>
|
|
204
205
|
```
|
|
206
|
+
|
|
207
|
+
## Scroll hint (`l-ui--scroll-hint`)
|
|
208
|
+
|
|
209
|
+
Fades the clipped edges of a horizontal scroller while more content is available in that direction, so it is obvious the region can be scrolled.
|
|
210
|
+
|
|
211
|
+
**Targets:** `scroller` (the scrollable element)
|
|
212
|
+
**Behaviour:** toggles `l-ui-scroll-hint--left`/`l-ui-scroll-hint--right` on the wrapper from the scroll position; updates on scroll and when the scroller or its content resizes. While the scroller overflows it is given `tabindex="0"` so keyboard users can scroll it (WCAG 2.1.1); the attribute is removed when it fits.
|
|
213
|
+
|
|
214
|
+
`l_ui_table` wires this up automatically. For hand-written tables (or any other horizontal scroller), wrap the scrolling element:
|
|
215
|
+
|
|
216
|
+
```html
|
|
217
|
+
<div class="l-ui-scroll-hint" data-controller="l-ui--scroll-hint">
|
|
218
|
+
<div class="l-ui-table-container" data-l-ui--scroll-hint-target="scroller">
|
|
219
|
+
<table class="l-ui-table">...</table>
|
|
220
|
+
</div>
|
|
221
|
+
</div>
|
|
222
|
+
```
|
|
@@ -204,6 +204,7 @@ Always combine the `l-ui-surface` base class with any modifiers (e.g. `l-ui-surf
|
|
|
204
204
|
|
|
205
205
|
```
|
|
206
206
|
.l-ui-table Table element
|
|
207
|
+
.l-ui-table--floating-actions Modifier: pins action cells to the right-hand edge of the scroll container during horizontal scrolling (sticky; solid background with a faded leading edge)
|
|
207
208
|
.l-ui-table__header <thead> row
|
|
208
209
|
.l-ui-table__header-cell <th> cell
|
|
209
210
|
.l-ui-table__header-cell--action Right-aligned action header
|
|
@@ -217,6 +218,9 @@ Always combine the `l-ui-surface` base class with any modifiers (e.g. `l-ui-surf
|
|
|
217
218
|
.l-ui-table__action Action link/button (inside a __cell--action)
|
|
218
219
|
.l-ui-table__action--danger Danger modifier (combine with l-ui-table__action)
|
|
219
220
|
.l-ui-table-container Overflow wrapper for responsive tables
|
|
221
|
+
.l-ui-scroll-hint Wrapper around a horizontal scroller (e.g. l-ui-table-container); fades the clipped edge while more content is available (paired with the l-ui--scroll-hint controller)
|
|
222
|
+
.l-ui-scroll-hint--left Fade visible on the left edge (toggled by the controller)
|
|
223
|
+
.l-ui-scroll-hint--right Fade visible on the right edge (toggled by the controller)
|
|
220
224
|
```
|
|
221
225
|
|
|
222
226
|
WCAG 2.2 AA table pattern:
|
|
@@ -269,6 +273,22 @@ Always combine the base block with one variant modifier (e.g. `class="l-ui-notic
|
|
|
269
273
|
|
|
270
274
|
Always combine the base block with a modifier, e.g. `<span class="l-ui-badge l-ui-badge--success">`.
|
|
271
275
|
|
|
276
|
+
## Tags
|
|
277
|
+
|
|
278
|
+
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.
|
|
279
|
+
|
|
280
|
+
```
|
|
281
|
+
.l-ui-tag-row Canonical container for a row of tags plus any trailing actions (e.g. a filter bar); wrapping flex row with gap
|
|
282
|
+
.l-ui-tag Tag container (div); carries colour and shape but no padding - segments supply it
|
|
283
|
+
.l-ui-tag--rounded Pill shape (matching l-ui-badge--rounded); default is the subtle badge radius
|
|
284
|
+
.l-ui-tag__text Static label segment (span)
|
|
285
|
+
.l-ui-tag__button Interactive label segment (button or a); focus ring + pointer
|
|
286
|
+
.l-ui-tag__label Inner wrapper (span) around a text/button segment's content; truncates long labels with an ellipsis
|
|
287
|
+
.l-ui-tag__remove Trailing remove segment (button or a) holding a ✕ icon (e.g. l-ui-icon--xs); needs an aria-label
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
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. A tag never grows past its container; wrap label content in `l-ui-tag__label` so it truncates with an ellipsis instead of overflowing. Prefer the `l_ui_tag` helper (see HELPERS.md), which renders the label wrapper and wires an optional popover.
|
|
291
|
+
|
|
272
292
|
## Tabs
|
|
273
293
|
|
|
274
294
|
```
|
|
@@ -197,8 +197,8 @@ Returns a `<th>` element with sort link and ARIA sort attributes.
|
|
|
197
197
|
|
|
198
198
|
```ruby
|
|
199
199
|
l_ui_table(records, columns:, caption: nil, actions: nil,
|
|
200
|
-
actions_label: "Actions",
|
|
201
|
-
turbo_frame: nil, row_id: nil)
|
|
200
|
+
actions_label: "Actions", floating_actions: false,
|
|
201
|
+
query: nil, url: nil, turbo_frame: nil, row_id: nil)
|
|
202
202
|
```
|
|
203
203
|
|
|
204
204
|
- `records` (ActiveRecord::Relation or Array) - the collection to render
|
|
@@ -206,11 +206,14 @@ l_ui_table(records, columns:, caption: nil, actions: nil,
|
|
|
206
206
|
- `caption` (String, optional) - visually hidden table caption for accessibility
|
|
207
207
|
- `actions` (Proc, optional) - receives (record), returns action cell content
|
|
208
208
|
- `actions_label` (String) - header text for the actions column, default "Actions"
|
|
209
|
+
- `floating_actions` (Boolean) - pin the actions column to the right-hand edge of the scroll container while the table scrolls horizontally (adds `l-ui-table--floating-actions`), default false
|
|
209
210
|
- `query` (Ransack::Search, optional) - enables sortable column headers
|
|
210
211
|
- `url` (String, optional) - sort link URL (passed to `l_ui_sort_link`)
|
|
211
212
|
- `turbo_frame` (String, optional) - turbo frame target for sort links
|
|
212
213
|
- `row_id` (Proc, optional) - receives (record), returns the `<tr>` id. Defaults to `dom_id(record)` for records that respond to `to_key` (ActiveRecord). Return `nil` to omit the id.
|
|
213
214
|
|
|
215
|
+
The rendered table is wrapped in an `l-ui-scroll-hint` element wired to the `l-ui--scroll-hint` controller, which fades the clipped edge when the table overflows horizontally.
|
|
216
|
+
|
|
214
217
|
Column options:
|
|
215
218
|
- `attribute` (Symbol) - used for label generation and sort links
|
|
216
219
|
- `label` (String, optional) - custom header text; defaults to humanised attribute
|
|
@@ -325,12 +328,13 @@ Calling `dialog.showModal()` directly is not supported - it bypasses the `l-ui--
|
|
|
325
328
|
## Popover
|
|
326
329
|
|
|
327
330
|
```ruby
|
|
328
|
-
l_ui_popover(id: nil, placement: :bottom, align: :start, container: {}, &block)
|
|
331
|
+
l_ui_popover(id: nil, placement: :bottom, align: :start, open: false, container: {}, &block)
|
|
329
332
|
```
|
|
330
333
|
|
|
331
334
|
- `id` (String, optional) - DOM id for the popover element; defaults to an auto-generated id
|
|
332
335
|
- `placement` (Symbol, optional) - `:top`, `:bottom` (default), `:left`, or `:right`. Flips automatically if it would overflow the viewport
|
|
333
336
|
- `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
|
|
337
|
+
- `open` (Boolean, optional) - when `true`, the popover opens as soon as its Stimulus controller connects, shown and positioned in the same task so it never paints unpositioned. Useful for re-opening a popover after a form submission re-renders the page (e.g. `open: params[:filtering].present?`). Defaults to `false`
|
|
334
338
|
- `container` (Hash, optional) - extra HTML attributes for the wrapping `<div>` (e.g. `class:`)
|
|
335
339
|
- `&block` - the block's content is the popover body; call `p.trigger(**options, &block)` inside it to render the trigger button
|
|
336
340
|
|
|
@@ -367,6 +371,43 @@ For a list of actions (e.g. a "more" menu on a table row), wrap the items in a `
|
|
|
367
371
|
}) %>
|
|
368
372
|
```
|
|
369
373
|
|
|
374
|
+
## Tag
|
|
375
|
+
|
|
376
|
+
```ruby
|
|
377
|
+
l_ui_tag(rounded: false, container: {}, &block)
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
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.
|
|
381
|
+
|
|
382
|
+
- `rounded` (Boolean, optional) - pill shape (matching `l-ui-badge--rounded`); defaults to `false` for the subtle badge radius
|
|
383
|
+
- `container` (Hash, optional) - extra HTML attributes for the wrapping `<div>` (e.g. `class:`)
|
|
384
|
+
- `&block` - declare segments on the builder; they render in call order. Use `<%` (not `<%=`) - segment content is captured by the builder
|
|
385
|
+
|
|
386
|
+
Builder methods:
|
|
387
|
+
|
|
388
|
+
- `t.text(content = nil, **opts, &block)` - static label segment (`<span class="l-ui-tag__text">`)
|
|
389
|
+
- `t.button(**opts, &block)` - button segment (`<button class="l-ui-tag__button">`); opens the tag's popover when one is declared
|
|
390
|
+
- `t.link(url, **opts, &block)` - link segment, styled like a button segment
|
|
391
|
+
- `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
|
|
392
|
+
- `t.popover(id: nil, placement: :bottom, align: :start, open: false, &block)` - attaches a popover; the block is the popover body. Options match `l_ui_popover`
|
|
393
|
+
|
|
394
|
+
Text, button, and link segments wrap their content in `<span class="l-ui-tag__label">`, which truncates long labels with an ellipsis when the tag is squeezed (the tag itself never grows past its container).
|
|
395
|
+
|
|
396
|
+
```erb
|
|
397
|
+
<%= l_ui_tag do |t| %>
|
|
398
|
+
<% t.button(aria: { label: "Edit status filter" }) do %>
|
|
399
|
+
Status: Active
|
|
400
|
+
<% end %>
|
|
401
|
+
<% t.remove remove_filter_path, aria: { label: "Remove status filter" },
|
|
402
|
+
data: { turbo_frame: "results" } %>
|
|
403
|
+
<% t.popover do %>
|
|
404
|
+
<p>Filter controls.</p>
|
|
405
|
+
<% end %>
|
|
406
|
+
<% end %>
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
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.
|
|
410
|
+
|
|
370
411
|
## Header
|
|
371
412
|
|
|
372
413
|
```ruby
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,23 @@
|
|
|
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.22.0] - 2026-07-10
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Floating table actions: pass `floating_actions: true` to `l_ui_table` (or add `l-ui-table--floating-actions` to a hand-written table) to pin the actions column to the right-hand edge of the scroll container, so row actions stay visible while the table scrolls horizontally. The pinned cells sit on a solid background with a faded leading edge, and the container gains scroll padding so focused cells scroll clear of the pinned column.
|
|
10
|
+
- Scroll hint: the new `l-ui--scroll-hint` Stimulus controller and `l-ui-scroll-hint` wrapper fade the clipped edges of a horizontal scroller while more content is available in that direction. While the scroller overflows it is made keyboard-focusable so the region stays keyboard-scrollable. `l_ui_table` wraps every table in a scroll hint automatically; wrap hand-written tables (or any other horizontal scroller) yourself.
|
|
11
|
+
- Popover `open:` option: `l_ui_popover(open: true)` (and `t.popover(open: true)` on a tag) opens the popover as soon as its Stimulus controller connects, shown and positioned in the same task so it never paints unpositioned - useful for re-opening a filter popover after a form submission re-renders the page.
|
|
12
|
+
- `l-ui-tag-row`: the canonical container for a row of tags plus any trailing actions (e.g. a filter bar) - a wrapping flex row with a gap, so tags move onto new lines on narrow screens instead of overflowing.
|
|
13
|
+
- Tag truncation: a tag never grows past its container, and label segments wrap their content in the new `l-ui-tag__label` so long labels truncate with an ellipsis instead of overflowing. The `l_ui_tag` helper renders the wrapper automatically; add it yourself in hand-written markup.
|
|
14
|
+
|
|
15
|
+
## [0.21.0] - 2026-07-09
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
|
|
19
|
+
- 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.
|
|
20
|
+
- `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.
|
|
21
|
+
|
|
5
22
|
## [0.20.0] - 2026-07-08
|
|
6
23
|
|
|
7
24
|
### 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, popovers, 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>
|
|
@@ -1655,7 +1655,16 @@ pre.l-ui-surface {
|
|
|
1655
1655
|
/* Tables */
|
|
1656
1656
|
|
|
1657
1657
|
.l-ui-table-container {
|
|
1658
|
-
@apply overflow-x-auto
|
|
1658
|
+
@apply overflow-x-auto
|
|
1659
|
+
focus-ring;
|
|
1660
|
+
scrollbar-width: thin;
|
|
1661
|
+
scrollbar-color: var(--color-border) transparent;
|
|
1662
|
+
}
|
|
1663
|
+
|
|
1664
|
+
/* Scrolls focused cells clear of the pinned actions column (WCAG 2.4.11) */
|
|
1665
|
+
|
|
1666
|
+
.l-ui-table-container:has(.l-ui-table--floating-actions) {
|
|
1667
|
+
@apply scroll-pr-20;
|
|
1659
1668
|
}
|
|
1660
1669
|
|
|
1661
1670
|
.l-ui-table {
|
|
@@ -1739,6 +1748,50 @@ pre.l-ui-surface {
|
|
|
1739
1748
|
@apply pr-0;
|
|
1740
1749
|
}
|
|
1741
1750
|
|
|
1751
|
+
/* The 1.5rem fade-in must match the leading padding so the actions always
|
|
1752
|
+
sit on solid background; z-10 lifts the cells above the scroll-hint fade */
|
|
1753
|
+
|
|
1754
|
+
.l-ui-table--floating-actions th.l-ui-table__header-cell--action,
|
|
1755
|
+
.l-ui-table--floating-actions td.l-ui-table__cell--action {
|
|
1756
|
+
@apply sticky right-0 z-10
|
|
1757
|
+
pl-6 pr-2
|
|
1758
|
+
bg-linear-to-r from-background/0 to-background to-[1.5rem];
|
|
1759
|
+
}
|
|
1760
|
+
|
|
1761
|
+
/* Scroll hint */
|
|
1762
|
+
|
|
1763
|
+
/* The --left/--right modifiers are toggled by the l-ui--scroll-hint
|
|
1764
|
+
controller from the scroll position of the wrapped scroller */
|
|
1765
|
+
|
|
1766
|
+
.l-ui-scroll-hint {
|
|
1767
|
+
@apply relative;
|
|
1768
|
+
}
|
|
1769
|
+
|
|
1770
|
+
.l-ui-scroll-hint::before,
|
|
1771
|
+
.l-ui-scroll-hint::after {
|
|
1772
|
+
@apply absolute inset-y-0
|
|
1773
|
+
w-8
|
|
1774
|
+
opacity-0
|
|
1775
|
+
transition-opacity
|
|
1776
|
+
pointer-events-none;
|
|
1777
|
+
content: "";
|
|
1778
|
+
}
|
|
1779
|
+
|
|
1780
|
+
.l-ui-scroll-hint::before {
|
|
1781
|
+
@apply left-0
|
|
1782
|
+
bg-linear-to-r from-background to-background/0;
|
|
1783
|
+
}
|
|
1784
|
+
|
|
1785
|
+
.l-ui-scroll-hint::after {
|
|
1786
|
+
@apply right-0
|
|
1787
|
+
bg-linear-to-l from-background to-background/0;
|
|
1788
|
+
}
|
|
1789
|
+
|
|
1790
|
+
.l-ui-scroll-hint--left::before,
|
|
1791
|
+
.l-ui-scroll-hint--right::after {
|
|
1792
|
+
@apply opacity-100;
|
|
1793
|
+
}
|
|
1794
|
+
|
|
1742
1795
|
/* Pagination */
|
|
1743
1796
|
|
|
1744
1797
|
@utility pagination-base {
|
|
@@ -1855,6 +1908,84 @@ pre.l-ui-surface {
|
|
|
1855
1908
|
bg-error-bg;
|
|
1856
1909
|
}
|
|
1857
1910
|
|
|
1911
|
+
/* Tags */
|
|
1912
|
+
|
|
1913
|
+
/* An interactive tag - e.g. an email recipient or an active filter. Related
|
|
1914
|
+
to the badge visually, but a distinct control: a badge is a static styled
|
|
1915
|
+
span, while a tag is a container with interactive segments. The container
|
|
1916
|
+
carries no padding of its own - each segment supplies its share, so every
|
|
1917
|
+
part of the tag is a hit target. */
|
|
1918
|
+
|
|
1919
|
+
.l-ui-tag-row {
|
|
1920
|
+
@apply flex flex-wrap items-center
|
|
1921
|
+
gap-2;
|
|
1922
|
+
}
|
|
1923
|
+
|
|
1924
|
+
/* The row wraps whole items onto new lines, so a squeezed trailing action
|
|
1925
|
+
should move down rather than wrap its label mid-text. Scoped here because
|
|
1926
|
+
elsewhere (e.g. a full-width button in a narrow panel) nowrap would force
|
|
1927
|
+
horizontal overflow instead of wrapping. */
|
|
1928
|
+
.l-ui-tag-row .l-ui-button {
|
|
1929
|
+
@apply whitespace-nowrap;
|
|
1930
|
+
}
|
|
1931
|
+
|
|
1932
|
+
.l-ui-tag {
|
|
1933
|
+
@apply inline-flex items-stretch
|
|
1934
|
+
max-w-full min-h-[32px]
|
|
1935
|
+
text-xs font-medium
|
|
1936
|
+
text-foreground-muted
|
|
1937
|
+
bg-surface-highlighted
|
|
1938
|
+
rounded;
|
|
1939
|
+
}
|
|
1940
|
+
|
|
1941
|
+
/* Pill shape, matching l-ui-badge--rounded. */
|
|
1942
|
+
.l-ui-tag--rounded {
|
|
1943
|
+
@apply !rounded-full;
|
|
1944
|
+
}
|
|
1945
|
+
|
|
1946
|
+
.l-ui-tag__text {
|
|
1947
|
+
@apply flex items-center
|
|
1948
|
+
min-w-0
|
|
1949
|
+
gap-1 px-3;
|
|
1950
|
+
}
|
|
1951
|
+
|
|
1952
|
+
.l-ui-tag__button {
|
|
1953
|
+
@apply flex items-center
|
|
1954
|
+
min-w-0
|
|
1955
|
+
gap-1 px-3
|
|
1956
|
+
rounded
|
|
1957
|
+
focus-ring
|
|
1958
|
+
cursor-pointer;
|
|
1959
|
+
}
|
|
1960
|
+
|
|
1961
|
+
/* This wrapper exists so long labels can truncate: ellipsis has no effect on
|
|
1962
|
+
text sitting directly inside a flex container like the segment. */
|
|
1963
|
+
.l-ui-tag__label {
|
|
1964
|
+
@apply overflow-hidden
|
|
1965
|
+
text-ellipsis whitespace-nowrap;
|
|
1966
|
+
}
|
|
1967
|
+
|
|
1968
|
+
/* Segment focus rings follow the tag's shape. */
|
|
1969
|
+
.l-ui-tag--rounded .l-ui-tag__button,
|
|
1970
|
+
.l-ui-tag--rounded .l-ui-tag__remove {
|
|
1971
|
+
@apply !rounded-full;
|
|
1972
|
+
}
|
|
1973
|
+
|
|
1974
|
+
/* When a remove segment follows, it owns the gap between the label and the
|
|
1975
|
+
✕ (its left padding), so the label segment gives up its right padding. */
|
|
1976
|
+
.l-ui-tag__text:has(+ .l-ui-tag__remove),
|
|
1977
|
+
.l-ui-tag__button:has(+ .l-ui-tag__remove) {
|
|
1978
|
+
@apply pr-0;
|
|
1979
|
+
}
|
|
1980
|
+
|
|
1981
|
+
.l-ui-tag__remove {
|
|
1982
|
+
@apply flex items-center
|
|
1983
|
+
pl-1.5 pr-2
|
|
1984
|
+
rounded
|
|
1985
|
+
focus-ring
|
|
1986
|
+
cursor-pointer;
|
|
1987
|
+
}
|
|
1988
|
+
|
|
1858
1989
|
/* Panel */
|
|
1859
1990
|
|
|
1860
1991
|
.l-ui-panel__button {
|
|
@@ -22,8 +22,11 @@ module Layered
|
|
|
22
22
|
# placement: (Symbol) :top, :bottom (default), :left, or :right. Flips automatically if it would overflow the viewport.
|
|
23
23
|
# align: (Symbol) :start (default) flushes the popover's leading edge with the trigger's; :end flushes its trailing edge instead
|
|
24
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
|
+
# open: (Boolean) When true, the popover opens as soon as its Stimulus controller connects - shown and positioned in the same
|
|
26
|
+
# task, so it never paints unpositioned. Useful for re-opening a popover after a form submission re-renders
|
|
27
|
+
# the page. Defaults to false.
|
|
25
28
|
# container: (Hash) Extra HTML attributes for the wrapping <div>.
|
|
26
|
-
def l_ui_popover(id: nil, placement: :bottom, align: :start, container: {}, &block)
|
|
29
|
+
def l_ui_popover(id: nil, placement: :bottom, align: :start, open: false, container: {}, &block)
|
|
27
30
|
id ||= "l-ui-popover-#{SecureRandom.hex(4)}"
|
|
28
31
|
builder = PopoverBuilder.new(self, id: id)
|
|
29
32
|
body_content = capture { block.call(builder) }
|
|
@@ -34,6 +37,7 @@ module Layered
|
|
|
34
37
|
container_data[:controller] = [existing_controller, "l-ui--popover"].compact.reject(&:empty?).join(" ")
|
|
35
38
|
container_data[:"l-ui--popover-placement-value"] = placement
|
|
36
39
|
container_data[:"l-ui--popover-align-value"] = align
|
|
40
|
+
container_data[:"l-ui--popover-open-value"] = true if open
|
|
37
41
|
container_attrs[:data] = container_data
|
|
38
42
|
|
|
39
43
|
tag.div(**container_attrs) do
|
|
@@ -31,6 +31,14 @@ module Layered
|
|
|
31
31
|
# sortable: (Boolean) Show sort link when query: is provided. Defaults to true.
|
|
32
32
|
# render: (Proc) Required. Receives (record), returns cell content.
|
|
33
33
|
#
|
|
34
|
+
# Pass +floating_actions: true+ to pin the actions column to the
|
|
35
|
+
# right-hand edge of the scroll container, so row actions stay visible
|
|
36
|
+
# while the table scrolls horizontally.
|
|
37
|
+
#
|
|
38
|
+
# The table is wrapped in an +l-ui-scroll-hint+ element wired to the
|
|
39
|
+
# +l-ui--scroll-hint+ controller, which fades the clipped edge while
|
|
40
|
+
# more columns are available in that direction.
|
|
41
|
+
#
|
|
34
42
|
# Pass +query:+ (a Ransack search object) and +turbo_frame:+ to enable
|
|
35
43
|
# sortable column headers via +l_ui_sort_link+.
|
|
36
44
|
#
|
|
@@ -38,7 +46,7 @@ module Layered
|
|
|
38
46
|
# +to_key+ (i.e. ActiveRecord), so individual rows can be targeted by
|
|
39
47
|
# Turbo Streams. Pass +row_id:+ as a proc to override (return +nil+ to
|
|
40
48
|
# omit the id).
|
|
41
|
-
def l_ui_table(records, columns:, caption: nil, actions: nil, actions_label: "Actions", query: nil, url: nil, turbo_frame: nil, row_id: nil)
|
|
49
|
+
def l_ui_table(records, columns:, caption: nil, actions: nil, actions_label: "Actions", floating_actions: false, query: nil, url: nil, turbo_frame: nil, row_id: nil)
|
|
42
50
|
columns = normalise_table_columns(columns)
|
|
43
51
|
col_count = columns.size + (actions ? 1 : 0)
|
|
44
52
|
|
|
@@ -75,8 +83,11 @@ module Layered
|
|
|
75
83
|
end
|
|
76
84
|
|
|
77
85
|
caption_tag = caption ? tag.caption(caption, class: "l-ui-sr-only") : nil
|
|
78
|
-
|
|
79
|
-
|
|
86
|
+
table_classes = ["l-ui-table"]
|
|
87
|
+
table_classes << "l-ui-table--floating-actions" if actions && floating_actions
|
|
88
|
+
table = tag.table(class: table_classes.join(" ")) { safe_join([caption_tag, thead, tbody].compact) }
|
|
89
|
+
container = tag.div(table, class: "l-ui-table-container", data: { "l-ui--scroll-hint-target" => "scroller" })
|
|
90
|
+
tag.div(container, class: "l-ui-scroll-hint", data: { controller: "l-ui--scroll-hint" })
|
|
80
91
|
end
|
|
81
92
|
|
|
82
93
|
private
|
|
@@ -0,0 +1,180 @@
|
|
|
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, open: false, &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_data[:"l-ui--popover-open-value"] = true if builder.popover_open
|
|
63
|
+
container_attrs[:data] = container_data
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
tag.div(**container_attrs) do
|
|
67
|
+
parts = builder.segments.map { |segment| l_ui_tag_segment(builder, segment) }
|
|
68
|
+
parts << l_ui_tag_popover(builder) if builder.popover?
|
|
69
|
+
safe_join(parts)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
class TagBuilder
|
|
74
|
+
attr_reader :segments, :popover_id, :popover_placement, :popover_align, :popover_open, :popover_body
|
|
75
|
+
|
|
76
|
+
def initialize(view)
|
|
77
|
+
@view = view
|
|
78
|
+
@segments = []
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def text(content = nil, **options, &block)
|
|
82
|
+
content = @view.capture(&block) if block
|
|
83
|
+
@segments << { kind: :text, content: content, options: options }
|
|
84
|
+
nil
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def button(**options, &block)
|
|
88
|
+
content = block ? @view.capture(&block) : nil
|
|
89
|
+
@segments << { kind: :button, content: content, options: options }
|
|
90
|
+
nil
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def link(url, **options, &block)
|
|
94
|
+
content = block ? @view.capture(&block) : nil
|
|
95
|
+
@segments << { kind: :link, url: url, content: content, options: options }
|
|
96
|
+
nil
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def remove(url = nil, **options, &block)
|
|
100
|
+
content = block ? @view.capture(&block) : nil
|
|
101
|
+
@segments << { kind: :remove, url: url, content: content, options: options }
|
|
102
|
+
nil
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def popover(id: nil, placement: :bottom, align: :start, open: false, &block)
|
|
106
|
+
@popover_id = id || "l-ui-tag-popover-#{SecureRandom.hex(4)}"
|
|
107
|
+
@popover_placement = placement
|
|
108
|
+
@popover_align = align
|
|
109
|
+
@popover_open = open
|
|
110
|
+
@popover_body = @view.capture(&block)
|
|
111
|
+
nil
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def popover?
|
|
115
|
+
!@popover_id.nil?
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
private
|
|
120
|
+
|
|
121
|
+
def l_ui_tag_segment(builder, segment)
|
|
122
|
+
options = segment[:options].deep_dup
|
|
123
|
+
|
|
124
|
+
case segment[:kind]
|
|
125
|
+
when :text
|
|
126
|
+
options[:class] = class_names("l-ui-tag__text", options[:class])
|
|
127
|
+
tag.span(l_ui_tag_label(segment[:content]), **options)
|
|
128
|
+
when :button
|
|
129
|
+
options[:class] = class_names("l-ui-tag__button", options[:class])
|
|
130
|
+
options[:type] ||= "button"
|
|
131
|
+
options[:popovertarget] ||= builder.popover_id if builder.popover?
|
|
132
|
+
tag.button(l_ui_tag_label(segment[:content]), **options)
|
|
133
|
+
when :link
|
|
134
|
+
options[:class] = class_names("l-ui-tag__button", options[:class])
|
|
135
|
+
link_to(l_ui_tag_label(segment[:content]), segment[:url], options)
|
|
136
|
+
when :remove
|
|
137
|
+
options[:class] = class_names("l-ui-tag__remove", options[:class])
|
|
138
|
+
content = segment[:content] || l_ui_tag_remove_icon
|
|
139
|
+
if segment[:url]
|
|
140
|
+
link_to(content, segment[:url], options)
|
|
141
|
+
else
|
|
142
|
+
options[:type] ||= "button"
|
|
143
|
+
tag.button(content, **options)
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# Wraps a label segment's content so long labels can truncate with an
|
|
149
|
+
# ellipsis - ellipsis has no effect on text sitting directly inside a
|
|
150
|
+
# flex container like the segment.
|
|
151
|
+
def l_ui_tag_label(content)
|
|
152
|
+
tag.span(content, class: "l-ui-tag__label")
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# Filled x-circle, matching the bundled +icon_close_circle.svg+ asset.
|
|
156
|
+
# Rendered inline (rather than via image_tag) so it inherits the tag's
|
|
157
|
+
# muted +currentColor+; an <img>-loaded SVG cannot.
|
|
158
|
+
def l_ui_tag_remove_icon
|
|
159
|
+
tag.svg(
|
|
160
|
+
tag.path("fill-rule" => "evenodd", "clip-rule" => "evenodd",
|
|
161
|
+
"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"),
|
|
162
|
+
class: "l-ui-icon--sm",
|
|
163
|
+
fill: "currentColor",
|
|
164
|
+
"viewBox" => "0 0 24 24",
|
|
165
|
+
"aria-hidden" => "true"
|
|
166
|
+
)
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def l_ui_tag_popover(builder)
|
|
170
|
+
tag.div(
|
|
171
|
+
builder.popover_body,
|
|
172
|
+
id: builder.popover_id,
|
|
173
|
+
popover: "auto",
|
|
174
|
+
class: "l-ui-popover",
|
|
175
|
+
data: { "l-ui--popover-target" => "popover" }
|
|
176
|
+
)
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
end
|
|
@@ -9,7 +9,8 @@ export default class extends Controller {
|
|
|
9
9
|
static targets = ["trigger", "popover"]
|
|
10
10
|
static values = {
|
|
11
11
|
placement: { type: String, default: "bottom" },
|
|
12
|
-
align: { type: String, default: "start" }
|
|
12
|
+
align: { type: String, default: "start" },
|
|
13
|
+
open: { type: Boolean, default: false }
|
|
13
14
|
}
|
|
14
15
|
|
|
15
16
|
static GAP = 8
|
|
@@ -24,6 +25,16 @@ export default class extends Controller {
|
|
|
24
25
|
}
|
|
25
26
|
}
|
|
26
27
|
element.addEventListener("toggle", this._toggleHandler)
|
|
28
|
+
|
|
29
|
+
// Open on connect when requested (e.g. re-opening a filter popover after
|
|
30
|
+
// a form submission re-renders the page). showPopover() and position()
|
|
31
|
+
// must run back-to-back in the same task: the native toggle event is
|
|
32
|
+
// dispatched asynchronously, so relying on the listener alone would paint
|
|
33
|
+
// the popover unpositioned at the viewport origin for one frame.
|
|
34
|
+
if (this.openValue && !element.matches(":popover-open")) {
|
|
35
|
+
element.showPopover()
|
|
36
|
+
this.position()
|
|
37
|
+
}
|
|
27
38
|
}
|
|
28
39
|
|
|
29
40
|
popoverTargetDisconnected(element) {
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
|
|
3
|
+
// Fades the clipped edges of a horizontal scroller while more content is
|
|
4
|
+
// available in that direction, by toggling the l-ui-scroll-hint--left/--right
|
|
5
|
+
// modifiers on its wrapper. Also makes the scroller keyboard-focusable while
|
|
6
|
+
// it overflows, so the region stays keyboard-scrollable.
|
|
7
|
+
export default class extends Controller {
|
|
8
|
+
static targets = ["scroller"]
|
|
9
|
+
|
|
10
|
+
initialize() {
|
|
11
|
+
this.update = this.update.bind(this)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
connect() {
|
|
15
|
+
this.scrollerTarget.addEventListener("scroll", this.update, { passive: true })
|
|
16
|
+
|
|
17
|
+
this.resizeObserver = new ResizeObserver(this.update)
|
|
18
|
+
this.resizeObserver.observe(this.scrollerTarget)
|
|
19
|
+
if (this.scrollerTarget.firstElementChild) {
|
|
20
|
+
this.resizeObserver.observe(this.scrollerTarget.firstElementChild)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
this.update()
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
disconnect() {
|
|
27
|
+
this.scrollerTarget.removeEventListener("scroll", this.update)
|
|
28
|
+
this.resizeObserver.disconnect()
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
update() {
|
|
32
|
+
const scroller = this.scrollerTarget
|
|
33
|
+
// 1px tolerance for fractional scroll positions on high-DPI screens
|
|
34
|
+
const overflows = scroller.scrollWidth - scroller.clientWidth > 1
|
|
35
|
+
|
|
36
|
+
// Keep the region keyboard-scrollable while it overflows (WCAG 2.1.1)
|
|
37
|
+
if (overflows) {
|
|
38
|
+
scroller.setAttribute("tabindex", "0")
|
|
39
|
+
} else {
|
|
40
|
+
scroller.removeAttribute("tabindex")
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
this.element.classList.toggle("l-ui-scroll-hint--left", scroller.scrollLeft > 1)
|
|
44
|
+
this.element.classList.toggle("l-ui-scroll-hint--right", overflows && scroller.scrollLeft < scroller.scrollWidth - scroller.clientWidth - 1)
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -7,6 +7,7 @@ import PanelResizeController from "layered_ui/controllers/l_ui/panel_resize_cont
|
|
|
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
9
|
import PopoverController from "layered_ui/controllers/l_ui/popover_controller"
|
|
10
|
+
import ScrollHintController from "layered_ui/controllers/l_ui/scroll_hint_controller"
|
|
10
11
|
import SearchFormController from "layered_ui/controllers/l_ui/search_form_controller"
|
|
11
12
|
import TabsController from "layered_ui/controllers/l_ui/tabs_controller"
|
|
12
13
|
|
|
@@ -19,4 +20,5 @@ application.register("l-ui--panel-resize", PanelResizeController)
|
|
|
19
20
|
application.register("l-ui--panel-button", PanelButtonController)
|
|
20
21
|
application.register("l-ui--modal", ModalController)
|
|
21
22
|
application.register("l-ui--popover", PopoverController)
|
|
23
|
+
application.register("l-ui--scroll-hint", ScrollHintController)
|
|
22
24
|
application.register("l-ui--tabs", TabsController)
|
data/config/importmap.rb
CHANGED
|
@@ -14,6 +14,7 @@ pin "layered_ui/controllers/l_ui/panel_controller", to: "layered_ui/controllers/
|
|
|
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
16
|
pin "layered_ui/controllers/l_ui/popover_controller", to: "layered_ui/controllers/l_ui/popover_controller.js"
|
|
17
|
+
pin "layered_ui/controllers/l_ui/scroll_hint_controller", to: "layered_ui/controllers/l_ui/scroll_hint_controller.js"
|
|
17
18
|
pin "layered_ui/controllers/l_ui/search_form_controller", to: "layered_ui/controllers/l_ui/search_form_controller.js"
|
|
18
19
|
pin "layered_ui/controllers/l_ui/tabs_controller", to: "layered_ui/controllers/l_ui/tabs_controller.js"
|
|
19
20
|
pin "layered_ui/controllers/l_ui/theme_controller", to: "layered_ui/controllers/l_ui/theme_controller.js"
|
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.22.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
|
|
@@ -224,6 +225,7 @@ files:
|
|
|
224
225
|
- app/helpers/layered/ui/popover_helper.rb
|
|
225
226
|
- app/helpers/layered/ui/ransack_helper.rb
|
|
226
227
|
- app/helpers/layered/ui/table_helper.rb
|
|
228
|
+
- app/helpers/layered/ui/tag_helper.rb
|
|
227
229
|
- app/helpers/layered/ui/title_bar_helper.rb
|
|
228
230
|
- app/javascript/layered_ui/controllers/l_ui/modal_controller.js
|
|
229
231
|
- app/javascript/layered_ui/controllers/l_ui/navigation_controller.js
|
|
@@ -232,6 +234,7 @@ files:
|
|
|
232
234
|
- app/javascript/layered_ui/controllers/l_ui/panel_controller.js
|
|
233
235
|
- app/javascript/layered_ui/controllers/l_ui/panel_resize_controller.js
|
|
234
236
|
- app/javascript/layered_ui/controllers/l_ui/popover_controller.js
|
|
237
|
+
- app/javascript/layered_ui/controllers/l_ui/scroll_hint_controller.js
|
|
235
238
|
- app/javascript/layered_ui/controllers/l_ui/search_form_controller.js
|
|
236
239
|
- app/javascript/layered_ui/controllers/l_ui/tabs_controller.js
|
|
237
240
|
- app/javascript/layered_ui/controllers/l_ui/theme_controller.js
|