layered-ui-rails 0.14.0 → 0.16.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 +1 -1
- data/.claude/skills/layered-ui-rails/references/CONTROLLERS.md +1 -1
- data/.claude/skills/layered-ui-rails/references/CSS.md +11 -1
- data/.claude/skills/layered-ui-rails/references/HELPERS.md +42 -1
- data/AGENTS.md +1 -1
- data/CHANGELOG.md +37 -0
- data/app/assets/images/layered_ui/icon_panel_close.svg +4 -0
- data/app/assets/tailwind/layered/ui/styles.css +46 -19
- data/app/helpers/layered/ui/form_helper.rb +2 -2
- data/app/helpers/layered/ui/modal_helper.rb +112 -0
- data/app/javascript/layered_ui/controllers/l_ui/modal_controller.js +26 -4
- data/app/javascript/layered_ui/controllers/l_ui/panel_resize_controller.js +1 -1
- data/app/javascript/layered_ui/utilities/layout.js +1 -1
- data/app/views/layered/ui/managed_resource/_form.html.erb +1 -1
- data/app/views/layouts/layered_ui/_header.html.erb +6 -0
- data/app/views/layouts/layered_ui/_panel.html.erb +3 -3
- data/lib/layered/ui/engine.rb +1 -0
- data/lib/layered/ui/version.rb +1 -1
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e263735e08a190494c81dd8c50552f0f407e80bca6704abea9dca2f3dc78ce37
|
|
4
|
+
data.tar.gz: 611458285efd46bc8c6b64f5766b65626152ed0d10d8d1cfef3eac6226f6f3b4
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 31aeaab41317883a2a027520b3aa656c5bc987825f51a7d79000d2746c7088046fe691a53827ad43aa2f0360558dd7208956bf5f18aad61536defea559f197cb
|
|
7
|
+
data.tar.gz: 128ac9723c177fbbcd4ff5a03f677e3c24fe924209b8a04fa985340b3b561bf07e3616e899cae352a1f02085c1e7f63a0c6aeb28137f453565017a0001e2a717
|
|
@@ -37,7 +37,7 @@ Then render the engine layout from your application layout. Place all `content_f
|
|
|
37
37
|
|
|
38
38
|
## Layout structure
|
|
39
39
|
|
|
40
|
-
The engine layout provides a fixed header (63px), optional sidebar navigation (
|
|
40
|
+
The engine layout provides a fixed header (63px), optional sidebar navigation (256px wide), optional resizable panel (320px default), and a main content area. Dark mode is built in with a toggle and localStorage persistence.
|
|
41
41
|
|
|
42
42
|
### Content blocks
|
|
43
43
|
|
|
@@ -140,7 +140,7 @@ Drag handle for resizing the panel width on desktop.
|
|
|
140
140
|
**Keyboard:** Arrow Left/Right (10px), Shift+Arrow (50px), Home/End
|
|
141
141
|
**Storage key:** `panelWidth` (pixel value)
|
|
142
142
|
|
|
143
|
-
- Min width:
|
|
143
|
+
- Min width: 256px
|
|
144
144
|
- Default width: 480px
|
|
145
145
|
- Double-click handle to reset to default
|
|
146
146
|
|
|
@@ -17,6 +17,16 @@ Two patterns deviate from strict BEM by design - leave them in place rather than
|
|
|
17
17
|
|
|
18
18
|
This gives author-written links inside long-form content (Markdown, prose, ad-hoc views) consistent underline/colour treatment without forcing an explicit class on every link. Engine elements opt out automatically because their class names contain `l-ui-`. Side effect: any host-app class containing the substring `l-ui-` will also opt an `<a>` out of bare-link styling.
|
|
19
19
|
|
|
20
|
+
## Body modifiers
|
|
21
|
+
|
|
22
|
+
Applied to `<body>` via the `:l_ui_body_class` yield to toggle layout-level behaviour:
|
|
23
|
+
|
|
24
|
+
```
|
|
25
|
+
.l-ui-body--always-show-navigation Pin sidebar navigation open on desktop
|
|
26
|
+
.l-ui-body--hide-header Hide the header and collapse its reserved space
|
|
27
|
+
.l-ui-body--header-contained Constrain the header's inner row to max-w-7xl (landing pages)
|
|
28
|
+
```
|
|
29
|
+
|
|
20
30
|
## Page layout
|
|
21
31
|
|
|
22
32
|
```
|
|
@@ -249,6 +259,7 @@ Always combine the base block with a modifier, e.g. `<span class="l-ui-badge l-u
|
|
|
249
259
|
```
|
|
250
260
|
.l-ui-header-container Fixed header container
|
|
251
261
|
.l-ui-header Header flexbox
|
|
262
|
+
.l-ui-header__links Inline link list for landing-page headers (yielded via :l_ui_header_links)
|
|
252
263
|
.l-ui-header__icon Header icon (responsive)
|
|
253
264
|
.l-ui-header__icon--light Light theme icon
|
|
254
265
|
.l-ui-header__icon--dark Dark theme icon
|
|
@@ -271,7 +282,6 @@ Always combine the base block with a modifier, e.g. `<span class="l-ui-badge l-u
|
|
|
271
282
|
.l-ui-panel__button--dragging During drag
|
|
272
283
|
.l-ui-panel__button--snapping Snapping to edge
|
|
273
284
|
.l-ui-panel__button--hidden Hidden when panel is open (toggled by Stimulus)
|
|
274
|
-
.l-ui-panel__close-button Panel close button (combine with l-ui-button l-ui-button--icon)
|
|
275
285
|
.l-ui-panel__icon Panel button inline SVG icon
|
|
276
286
|
.l-ui-panel__icon--light Panel button icon (light, for custom image override)
|
|
277
287
|
.l-ui-panel__icon--dark Panel button icon (dark, for custom image override)
|
|
@@ -208,13 +208,14 @@ Formats a date/time value as `"%-d %b %Y, %H:%M"` (e.g. "15 Apr 2026, 10:30"). R
|
|
|
208
208
|
## Form
|
|
209
209
|
|
|
210
210
|
```ruby
|
|
211
|
-
l_ui_form(record, fields:, url:, method: nil)
|
|
211
|
+
l_ui_form(record, fields:, url:, method: nil, submit: nil)
|
|
212
212
|
```
|
|
213
213
|
|
|
214
214
|
- `record` (ActiveRecord) - the model instance
|
|
215
215
|
- `fields` (Array<Hash>) - field definitions (see below)
|
|
216
216
|
- `url` (String) - form action URL
|
|
217
217
|
- `method` (Symbol, optional) - HTTP method override
|
|
218
|
+
- `submit` (String, optional) - submit button text; defaults to "Create" for new records and "Save" for persisted records
|
|
218
219
|
|
|
219
220
|
Renders a complete form with all fields, error summary, and submit button via the `layered/ui/managed_resource/form` partial.
|
|
220
221
|
|
|
@@ -246,6 +247,46 @@ l_ui_field_error_id(record, attribute) # Error element ID for aria-describedby
|
|
|
246
247
|
l_ui_field_hint_id(record, attribute) # Hint element ID for aria-describedby
|
|
247
248
|
```
|
|
248
249
|
|
|
250
|
+
## Modal
|
|
251
|
+
|
|
252
|
+
```ruby
|
|
253
|
+
l_ui_modal(title:, id: nil, heading_level: :h3, container: {}, &block)
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
- `title` (String) - heading shown in the modal header and used for `aria-labelledby`
|
|
257
|
+
- `id` (String, optional) - DOM id for the `<dialog>`; defaults to an auto-generated id
|
|
258
|
+
- `heading_level` (Symbol, optional) - heading tag for the title (e.g. `:h2`, `:h3`). Defaults to `:h3`
|
|
259
|
+
- `container` (Hash, optional) - extra HTML attributes for the wrapping `<div>` (e.g. `class:`)
|
|
260
|
+
- `&block` - the block's content is the modal body; call `m.trigger(**options, &block)` inside it to render a colocated trigger button
|
|
261
|
+
|
|
262
|
+
```erb
|
|
263
|
+
<%= l_ui_modal(title: "Socials") do |m| %>
|
|
264
|
+
<% m.trigger(class: "l-ui-button l-ui-button--outline") do %>
|
|
265
|
+
Open socials
|
|
266
|
+
<% end %>
|
|
267
|
+
<p>Body content.</p>
|
|
268
|
+
<% end %>
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
Renders the trigger `<button>` (if `m.trigger` is called), the `<dialog class="l-ui-modal">` with a header (title + close button), and wires the `l-ui--modal` Stimulus controller, including backdrop-click close.
|
|
272
|
+
|
|
273
|
+
If the trigger contains only an icon (no visible text), pass `aria-label:` to `m.trigger` so the button has an accessible name.
|
|
274
|
+
|
|
275
|
+
`m.trigger` renders a colocated trigger button. To open the same modal from elsewhere on the page (or to have multiple triggers), give the modal a known `id:` and add `data-l-ui-modal-open="<that id>"` to any button:
|
|
276
|
+
|
|
277
|
+
```erb
|
|
278
|
+
<%= l_ui_modal(title: "Confirm", id: "confirm-modal") do |m| %>
|
|
279
|
+
<% m.trigger(class: "l-ui-button") do %>Open<% end %>
|
|
280
|
+
<p>Body content.</p>
|
|
281
|
+
<% end %>
|
|
282
|
+
|
|
283
|
+
<button type="button" data-l-ui-modal-open="confirm-modal">Open from elsewhere</button>
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
The button does not need to be inside the helper's wrapper, and no `data-controller` or `data-action` is required - the `l-ui--modal` controller listens at the document level for clicks on `[data-l-ui-modal-open]` matching its dialog id.
|
|
287
|
+
|
|
288
|
+
Calling `dialog.showModal()` directly is not supported - it bypasses the `l-ui--modal` controller and skips scroll lock, focus restoration, open-count tracking, and the screen-reader announcement.
|
|
289
|
+
|
|
249
290
|
## Authentication
|
|
250
291
|
|
|
251
292
|
```ruby
|
data/AGENTS.md
CHANGED
|
@@ -6,7 +6,7 @@ Guidance for AI agents working in this repository.
|
|
|
6
6
|
|
|
7
7
|
- **Entry:** `require "layered-ui-rails"` → `lib/layered/ui.rb` → `lib/layered/ui/engine.rb`
|
|
8
8
|
- **Engine:** importmap, assets, Pagy helpers when present; helpers: `AuthenticationHelper`, `NavigationHelper`, `PagyHelper`
|
|
9
|
-
- **CSS** `app/assets/tailwind/layered/ui/styles.css`: OKLCH tokens, `.dark` on `<html>`, `@theme` utilities (`bg-background`, etc.), BEM components (`.l-ui-button--primary`, etc.). Layout: 63px header,
|
|
9
|
+
- **CSS** `app/assets/tailwind/layered/ui/styles.css`: OKLCH tokens, `.dark` on `<html>`, `@theme` utilities (`bg-background`, etc.), BEM components (`.l-ui-button--primary`, etc.). Layout: 63px header, 256px sidebar, 320px panel. WCAG 2.2 AA.
|
|
10
10
|
- **CSS `@apply`:** Multi-line with grouping, following the Prettier Tailwind plugin order: layout → sizing → spacing → typography → backgrounds → borders → effects → transitions → interactivity. Within each group, follow Tailwind's own ordering (not alphabetical). State variants (`hover:`, `focus:`, `active:`, `disabled:`) and responsive prefixes (`sm:`, `md:`, `lg:`) are grouped with their base utility. Single utilities may stay on one line.
|
|
11
11
|
- **Generators:** `bin/rails generate layered:ui:install` (copy CSS, import CSS, import JS)
|
|
12
12
|
- **JS** `app/javascript/layered_ui/`: Stimulus controllers registered as `l-ui--theme`, `l-ui--navigation`, `l-ui--panel`, `l-ui--modal`, `l-ui--tabs`
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,43 @@
|
|
|
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.16.0] - 2026-05-16
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- `l_ui_modal` helper with external-trigger support for declaratively rendering modal dialogs
|
|
10
|
+
- `l-ui-header--contained` modifier and a yield slot for header links
|
|
11
|
+
- `submit:` option on `l_ui_form` to override the submit button text (defaults to "Create" for new records, "Save" for persisted)
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
|
|
15
|
+
- Header links now left-aligned
|
|
16
|
+
- Navigation container right border restored at `md` and above; panel container left border restored
|
|
17
|
+
- Modal backdrop now tinted in addition to the existing blur (foreground at 25% in light mode, background at 60% in dark mode)
|
|
18
|
+
- Active navigation items now round on all four corners (`rounded-sm`) rather than the left only
|
|
19
|
+
- Panel close button now uses `l-ui-button--primary` instead of `l-ui-button--outline`
|
|
20
|
+
- Icons inside `l-ui-button--primary` now invert in light mode and stay un-inverted in dark mode, so they always contrast against the accent-coloured button background
|
|
21
|
+
|
|
22
|
+
## [0.15.0] - 2026-05-11
|
|
23
|
+
|
|
24
|
+
### Breaking
|
|
25
|
+
|
|
26
|
+
- `l-ui-panel__close-button` modifier removed; the panel close button is now an `l-ui-button--outline l-ui-button--icon`
|
|
27
|
+
- Navigation width increased from 240px to 256px (affects `NAV_WIDTH` consumers and the panel resize minimum width)
|
|
28
|
+
|
|
29
|
+
### Added
|
|
30
|
+
|
|
31
|
+
- `icon_panel_close.svg` asset for the panel close button
|
|
32
|
+
|
|
33
|
+
### Changed
|
|
34
|
+
|
|
35
|
+
- Borders removed from navigation container (right and top), navigation user block, panel container (left edge), and panel header
|
|
36
|
+
- `h2` elements now have a bottom border for stronger visual separation
|
|
37
|
+
- Table cell padding evened out to `p-2`
|
|
38
|
+
- Panel header padding adjusted (`p-4 pb-0`) and header button gap increased to `gap-4`
|
|
39
|
+
- Modal backdrop click detection now uses the dialog's bounding rect, so clicks on the dialog's own padding no longer close it
|
|
40
|
+
- Navigation item corners rounded on the left only (`rounded-l-sm`)
|
|
41
|
+
|
|
5
42
|
## [0.14.0] - 2026-05-10
|
|
6
43
|
|
|
7
44
|
### Breaking
|
|
@@ -118,7 +118,8 @@
|
|
|
118
118
|
}
|
|
119
119
|
|
|
120
120
|
h2 {
|
|
121
|
-
@apply heading text-lg
|
|
121
|
+
@apply heading text-lg
|
|
122
|
+
border-b border-border pb-2;
|
|
122
123
|
}
|
|
123
124
|
|
|
124
125
|
h3 {
|
|
@@ -360,7 +361,7 @@
|
|
|
360
361
|
@apply overflow-x-auto
|
|
361
362
|
my-3 p-3
|
|
362
363
|
bg-surface
|
|
363
|
-
rounded-
|
|
364
|
+
rounded-sm;
|
|
364
365
|
}
|
|
365
366
|
|
|
366
367
|
.l-ui-markdown pre code {
|
|
@@ -456,7 +457,7 @@
|
|
|
456
457
|
}
|
|
457
458
|
|
|
458
459
|
.l-ui-body--always-show-navigation .l-ui-page--with-navigation {
|
|
459
|
-
@apply md:ml-[
|
|
460
|
+
@apply md:ml-[256px];
|
|
460
461
|
}
|
|
461
462
|
|
|
462
463
|
.l-ui-page__vertically-centered {
|
|
@@ -488,11 +489,29 @@
|
|
|
488
489
|
px-4 py-3;
|
|
489
490
|
}
|
|
490
491
|
|
|
492
|
+
.l-ui-body--header-contained .l-ui-header {
|
|
493
|
+
@apply w-full max-w-7xl
|
|
494
|
+
mx-auto;
|
|
495
|
+
}
|
|
496
|
+
|
|
491
497
|
.l-ui-header__navigation {
|
|
492
498
|
@apply flex items-center
|
|
493
499
|
gap-4;
|
|
494
500
|
}
|
|
495
501
|
|
|
502
|
+
.l-ui-header__links {
|
|
503
|
+
@apply hidden sm:flex items-center
|
|
504
|
+
mr-auto ml-8
|
|
505
|
+
gap-8
|
|
506
|
+
text-sm font-medium;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
.l-ui-header__links a {
|
|
510
|
+
@apply text-foreground-muted
|
|
511
|
+
rounded-sm
|
|
512
|
+
focus-ring;
|
|
513
|
+
}
|
|
514
|
+
|
|
496
515
|
.l-ui-header__icon {
|
|
497
516
|
@apply h-5.5 w-auto;
|
|
498
517
|
}
|
|
@@ -562,6 +581,10 @@
|
|
|
562
581
|
@apply dark:invert;
|
|
563
582
|
}
|
|
564
583
|
|
|
584
|
+
.l-ui-button--primary .l-ui-icon {
|
|
585
|
+
@apply invert dark:invert-0;
|
|
586
|
+
}
|
|
587
|
+
|
|
565
588
|
.l-ui-icon--xs {
|
|
566
589
|
@apply w-4 h-4;
|
|
567
590
|
}
|
|
@@ -585,11 +608,11 @@
|
|
|
585
608
|
/* Navigation */
|
|
586
609
|
|
|
587
610
|
.l-ui-navigation-container {
|
|
588
|
-
@apply fixed top-[var(--header-height)] left-0 bottom-0
|
|
611
|
+
@apply fixed top-[calc(var(--header-height)+1px)] left-0 bottom-0
|
|
589
612
|
z-50
|
|
590
|
-
w-[
|
|
613
|
+
w-[256px]
|
|
591
614
|
bg-background
|
|
592
|
-
border-
|
|
615
|
+
md:border-r md:border-border
|
|
593
616
|
-translate-x-full
|
|
594
617
|
transition-transform duration-300;
|
|
595
618
|
}
|
|
@@ -726,13 +749,12 @@
|
|
|
726
749
|
.l-ui-navigation__links {
|
|
727
750
|
@apply flex flex-col flex-1 overflow-y-auto
|
|
728
751
|
min-h-0
|
|
729
|
-
gap-0.5 px-3
|
|
752
|
+
gap-0.5 px-3 py-3;
|
|
730
753
|
}
|
|
731
754
|
|
|
732
755
|
.l-ui-navigation__user {
|
|
733
756
|
@apply shrink-0
|
|
734
|
-
p-4
|
|
735
|
-
border-t border-border;
|
|
757
|
+
p-4;
|
|
736
758
|
}
|
|
737
759
|
|
|
738
760
|
.l-ui-navigation__user-name-and-email {
|
|
@@ -1268,7 +1290,7 @@ pre.l-ui-surface {
|
|
|
1268
1290
|
}
|
|
1269
1291
|
|
|
1270
1292
|
.l-ui-table__cell {
|
|
1271
|
-
@apply
|
|
1293
|
+
@apply p-2
|
|
1272
1294
|
text-sm whitespace-nowrap text-foreground-muted;
|
|
1273
1295
|
}
|
|
1274
1296
|
|
|
@@ -1486,7 +1508,7 @@ pre.l-ui-surface {
|
|
|
1486
1508
|
z-40
|
|
1487
1509
|
w-full md:w-[480px]
|
|
1488
1510
|
bg-background
|
|
1489
|
-
|
|
1511
|
+
border-l border-border
|
|
1490
1512
|
transform translate-x-full
|
|
1491
1513
|
transition-transform duration-300;
|
|
1492
1514
|
}
|
|
@@ -1529,13 +1551,13 @@ pre.l-ui-surface {
|
|
|
1529
1551
|
|
|
1530
1552
|
.l-ui-panel__header-content {
|
|
1531
1553
|
@apply flex items-center
|
|
1532
|
-
|
|
1554
|
+
p-4 py-3;
|
|
1533
1555
|
}
|
|
1534
1556
|
|
|
1535
1557
|
.l-ui-panel__header-buttons {
|
|
1536
1558
|
@apply flex items-center
|
|
1537
1559
|
w-full
|
|
1538
|
-
gap-
|
|
1560
|
+
gap-4;
|
|
1539
1561
|
}
|
|
1540
1562
|
|
|
1541
1563
|
.l-ui-panel__header-heading {
|
|
@@ -1551,10 +1573,6 @@ pre.l-ui-surface {
|
|
|
1551
1573
|
gap-2;
|
|
1552
1574
|
}
|
|
1553
1575
|
|
|
1554
|
-
.l-ui-panel__close-button {
|
|
1555
|
-
@apply -mr-4;
|
|
1556
|
-
}
|
|
1557
|
-
|
|
1558
1576
|
.l-ui-panel__body {
|
|
1559
1577
|
@apply flex-1 overflow-y-auto overscroll-contain
|
|
1560
1578
|
p-4 space-y-4;
|
|
@@ -1649,7 +1667,7 @@ pre.l-ui-surface {
|
|
|
1649
1667
|
p-4 gap-4
|
|
1650
1668
|
text-foreground
|
|
1651
1669
|
bg-surface
|
|
1652
|
-
rounded-
|
|
1670
|
+
rounded-sm;
|
|
1653
1671
|
}
|
|
1654
1672
|
|
|
1655
1673
|
.l-ui-message--sent .l-ui-message__bubble {
|
|
@@ -1794,8 +1812,17 @@ pre.l-ui-surface {
|
|
|
1794
1812
|
border-0 md:border md:border-border rounded-none md:rounded-sm;
|
|
1795
1813
|
}
|
|
1796
1814
|
|
|
1815
|
+
.l-ui-modal[open] {
|
|
1816
|
+
@apply flex flex-col;
|
|
1817
|
+
}
|
|
1818
|
+
|
|
1797
1819
|
.l-ui-modal::backdrop {
|
|
1798
1820
|
@apply backdrop-blur-sm;
|
|
1821
|
+
background-color: color-mix(in oklch, var(--foreground) 25%, transparent);
|
|
1822
|
+
}
|
|
1823
|
+
|
|
1824
|
+
.dark .l-ui-modal::backdrop {
|
|
1825
|
+
background-color: color-mix(in oklch, var(--background) 60%, transparent);
|
|
1799
1826
|
}
|
|
1800
1827
|
|
|
1801
1828
|
.l-ui-modal__header {
|
|
@@ -1805,7 +1832,7 @@ pre.l-ui-surface {
|
|
|
1805
1832
|
}
|
|
1806
1833
|
|
|
1807
1834
|
.l-ui-modal__body {
|
|
1808
|
-
@apply overflow-y-auto
|
|
1835
|
+
@apply flex-1 min-h-0 overflow-y-auto
|
|
1809
1836
|
px-5 py-4;
|
|
1810
1837
|
}
|
|
1811
1838
|
|
|
@@ -10,9 +10,9 @@ module Layered
|
|
|
10
10
|
# fields: Post.l_managed_resource_fields,
|
|
11
11
|
# url: managed_posts_path)
|
|
12
12
|
#
|
|
13
|
-
def l_ui_form(record, fields:, url:, method: nil)
|
|
13
|
+
def l_ui_form(record, fields:, url:, method: nil, submit: nil)
|
|
14
14
|
render partial: "layered/ui/managed_resource/form",
|
|
15
|
-
locals: { record: record, fields: fields, url: url, method: method }
|
|
15
|
+
locals: { record: record, fields: fields, url: url, method: method, submit: submit }
|
|
16
16
|
end
|
|
17
17
|
|
|
18
18
|
# Normalises a raw field config hash into a canonical form.
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
module Layered
|
|
2
|
+
module Ui
|
|
3
|
+
module ModalHelper
|
|
4
|
+
# Renders a modal as a <dialog> wired up to the +l-ui--modal+ Stimulus
|
|
5
|
+
# controller. The block's content is the modal body; call +m.trigger+ to
|
|
6
|
+
# render a colocated trigger button.
|
|
7
|
+
#
|
|
8
|
+
# <%= l_ui_modal(title: "Socials") do |m| %>
|
|
9
|
+
# <% m.trigger(class: "l-ui-button l-ui-button--outline") do %>
|
|
10
|
+
# Open socials
|
|
11
|
+
# <% end %>
|
|
12
|
+
# <p>Body content.</p>
|
|
13
|
+
# <% end %>
|
|
14
|
+
#
|
|
15
|
+
# To open the modal from elsewhere on the page (or to have multiple
|
|
16
|
+
# triggers), give it a known +id:+ and add +data-l-ui-modal-open="<id>"+
|
|
17
|
+
# to any button. The button does not need to live inside the helper's
|
|
18
|
+
# wrapper; the +l-ui--modal+ controller listens at the document level
|
|
19
|
+
# for matching clicks.
|
|
20
|
+
#
|
|
21
|
+
# <button type="button" data-l-ui-modal-open="confirm-modal">Open</button>
|
|
22
|
+
#
|
|
23
|
+
# Calling +dialog.showModal()+ directly is not supported: it bypasses the
|
|
24
|
+
# +l-ui--modal+ controller and skips scroll lock, focus restoration, open-
|
|
25
|
+
# count tracking, and the screen-reader announcement.
|
|
26
|
+
#
|
|
27
|
+
# Note: use +<% m.trigger %>+ (without the equals sign) so its content is
|
|
28
|
+
# captured by the builder rather than written to the body buffer.
|
|
29
|
+
#
|
|
30
|
+
# Options:
|
|
31
|
+
# title: (String) Required. Modal heading; also used for aria-labelledby.
|
|
32
|
+
# id: (String) DOM id for the <dialog>; defaults to an auto-generated id.
|
|
33
|
+
# heading_level: (Symbol) Heading tag for the title (e.g. :h2, :h3). Defaults to :h3.
|
|
34
|
+
# container: (Hash) Extra HTML attributes for the wrapping <div>.
|
|
35
|
+
def l_ui_modal(title:, id: nil, heading_level: :h3, container: {}, &block)
|
|
36
|
+
id ||= "l-ui-modal-#{SecureRandom.hex(4)}"
|
|
37
|
+
builder = ModalBuilder.new(self, title: title, id: id, heading_level: heading_level)
|
|
38
|
+
body_content = capture { block.call(builder) }
|
|
39
|
+
|
|
40
|
+
container_attrs = container.deep_dup
|
|
41
|
+
container_data = container_attrs[:data] || {}
|
|
42
|
+
existing_controller = container_data.delete(:controller) || container_data.delete("controller")
|
|
43
|
+
container_data[:controller] = [existing_controller, "l-ui--modal"].compact.reject(&:empty?).join(" ")
|
|
44
|
+
container_attrs[:data] = container_data
|
|
45
|
+
|
|
46
|
+
tag.div(**container_attrs) do
|
|
47
|
+
safe_join([
|
|
48
|
+
builder.trigger_html || ActiveSupport::SafeBuffer.new,
|
|
49
|
+
render_modal_dialog(builder, body_content)
|
|
50
|
+
])
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
class ModalBuilder
|
|
55
|
+
attr_reader :title, :id, :heading_level, :trigger_html
|
|
56
|
+
|
|
57
|
+
def initialize(view, title:, id:, heading_level: :h3)
|
|
58
|
+
@view = view
|
|
59
|
+
@title = title
|
|
60
|
+
@id = id
|
|
61
|
+
@heading_level = heading_level
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def trigger(**options, &block)
|
|
65
|
+
options = options.deep_dup
|
|
66
|
+
options[:type] ||= "button"
|
|
67
|
+
data = options[:data] || {}
|
|
68
|
+
existing_action = data.delete(:action) || data.delete("action")
|
|
69
|
+
data[:action] = [existing_action, "l-ui--modal#open"].compact.reject(&:empty?).join(" ")
|
|
70
|
+
options[:data] = data
|
|
71
|
+
content = @view.capture(&block)
|
|
72
|
+
@trigger_html = @view.tag.button(content, **options)
|
|
73
|
+
nil
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
private
|
|
78
|
+
|
|
79
|
+
def render_modal_dialog(builder, body_content)
|
|
80
|
+
heading_id = "#{builder.id}-heading"
|
|
81
|
+
|
|
82
|
+
dialog_attrs = {
|
|
83
|
+
id: builder.id,
|
|
84
|
+
class: "l-ui-modal",
|
|
85
|
+
data: {
|
|
86
|
+
"l-ui--modal-target" => "dialog",
|
|
87
|
+
"action" => "click->l-ui--modal#closeOnBackdrop"
|
|
88
|
+
},
|
|
89
|
+
aria: { labelledby: heading_id }
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
tag.dialog(**dialog_attrs) do
|
|
93
|
+
safe_join([
|
|
94
|
+
tag.div(class: "l-ui-modal__header") do
|
|
95
|
+
safe_join([
|
|
96
|
+
content_tag(builder.heading_level, builder.title, id: heading_id),
|
|
97
|
+
tag.button(
|
|
98
|
+
image_tag("layered_ui/icon_close.svg", alt: "", class: "l-ui-icon l-ui-icon--sm", aria: { hidden: true }),
|
|
99
|
+
class: "l-ui-button l-ui-button--icon",
|
|
100
|
+
type: "button",
|
|
101
|
+
data: { action: "l-ui--modal#close" },
|
|
102
|
+
"aria-label" => "Close"
|
|
103
|
+
)
|
|
104
|
+
])
|
|
105
|
+
end,
|
|
106
|
+
tag.div(body_content, class: "l-ui-modal__body")
|
|
107
|
+
])
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
@@ -39,14 +39,26 @@ export default class extends Controller {
|
|
|
39
39
|
this.dialogTarget.close()
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
-
// Close the modal when clicking outside the dialog
|
|
42
|
+
// Close the modal when clicking outside the dialog rect (on the backdrop).
|
|
43
|
+
// Comparing against event.target alone is unreliable: clicks on the dialog's
|
|
44
|
+
// own padding/empty space also have target === dialog, which would close it.
|
|
43
45
|
closeOnBackdrop(event) {
|
|
44
|
-
if (event.target
|
|
46
|
+
if (event.target !== this.dialogTarget) return
|
|
47
|
+
|
|
48
|
+
const rect = this.dialogTarget.getBoundingClientRect()
|
|
49
|
+
const insideDialog =
|
|
50
|
+
event.clientX >= rect.left &&
|
|
51
|
+
event.clientX <= rect.right &&
|
|
52
|
+
event.clientY >= rect.top &&
|
|
53
|
+
event.clientY <= rect.bottom
|
|
54
|
+
|
|
55
|
+
if (!insideDialog) {
|
|
45
56
|
this.close()
|
|
46
57
|
}
|
|
47
58
|
}
|
|
48
59
|
|
|
49
|
-
// Handle the close event on the dialog
|
|
60
|
+
// Handle the close event on the dialog and document-level clicks from
|
|
61
|
+
// external triggers (any button with data-l-ui-modal-open="<dialog id>").
|
|
50
62
|
dialogTargetConnected(element) {
|
|
51
63
|
this._closeHandler = () => {
|
|
52
64
|
this.constructor.openCount = Math.max(0, this.constructor.openCount - 1)
|
|
@@ -60,11 +72,21 @@ export default class extends Controller {
|
|
|
60
72
|
announce("Dialog closed", this)
|
|
61
73
|
}
|
|
62
74
|
element.addEventListener("close", this._closeHandler)
|
|
75
|
+
|
|
76
|
+
this._externalOpenHandler = (event) => {
|
|
77
|
+
if (!element.id) return
|
|
78
|
+
const trigger = event.target.closest(`[data-l-ui-modal-open="${CSS.escape(element.id)}"]`)
|
|
79
|
+
if (!trigger) return
|
|
80
|
+
event.preventDefault()
|
|
81
|
+
this.open()
|
|
82
|
+
}
|
|
83
|
+
document.addEventListener("click", this._externalOpenHandler)
|
|
63
84
|
}
|
|
64
85
|
|
|
65
|
-
// Remove
|
|
86
|
+
// Remove listeners
|
|
66
87
|
dialogTargetDisconnected(element) {
|
|
67
88
|
element.removeEventListener("close", this._closeHandler)
|
|
89
|
+
document.removeEventListener("click", this._externalOpenHandler)
|
|
68
90
|
}
|
|
69
91
|
|
|
70
92
|
disconnect() {
|
|
@@ -2,7 +2,7 @@ import { Controller } from "@hotwired/stimulus"
|
|
|
2
2
|
import { storageGet, storageSet, storageRemove } from "layered_ui/utilities/storage"
|
|
3
3
|
import { isMobile, hasNavigation, NAV_WIDTH } from "layered_ui/utilities/layout"
|
|
4
4
|
|
|
5
|
-
const MIN_WIDTH =
|
|
5
|
+
const MIN_WIDTH = 256
|
|
6
6
|
const DEFAULT_WIDTH = 480
|
|
7
7
|
|
|
8
8
|
export default class extends Controller {
|
|
@@ -7,6 +7,12 @@
|
|
|
7
7
|
<%= yield(:l_ui_logo_dark).presence || image_tag("layered_ui/logo_dark.svg", alt: "", class: "l-ui-header__logo l-ui-header__logo--dark") %>
|
|
8
8
|
<% end %>
|
|
9
9
|
|
|
10
|
+
<% if yield(:l_ui_header_links).present? %>
|
|
11
|
+
<nav class="l-ui-header__links" aria-label="Sections">
|
|
12
|
+
<%= yield(:l_ui_header_links) %>
|
|
13
|
+
</nav>
|
|
14
|
+
<% end %>
|
|
15
|
+
|
|
10
16
|
<nav class="l-ui-header__navigation" aria-label="Header navigation">
|
|
11
17
|
<button
|
|
12
18
|
type="button"
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
aria-label="Resize panel"
|
|
18
18
|
aria-orientation="vertical"
|
|
19
19
|
aria-valuenow="480"
|
|
20
|
-
aria-valuemin="
|
|
20
|
+
aria-valuemin="256"
|
|
21
21
|
aria-valuemax="1200"
|
|
22
22
|
aria-valuetext="Panel width: 480 pixels"
|
|
23
23
|
tabindex="0"
|
|
@@ -35,13 +35,13 @@
|
|
|
35
35
|
|
|
36
36
|
<div class="l-ui-panel__header-actions">
|
|
37
37
|
<button type="button"
|
|
38
|
-
class="l-ui-button l-ui-button--
|
|
38
|
+
class="l-ui-button l-ui-button--primary l-ui-button--icon"
|
|
39
39
|
aria-label="Hide panel"
|
|
40
40
|
aria-controls="panel"
|
|
41
41
|
title="Toggle panel (Ctrl+i / ⌘i)"
|
|
42
42
|
data-action="click->l-ui--panel#toggle"
|
|
43
43
|
data-l-ui--panel-target="hideButton">
|
|
44
|
-
<%= image_tag "layered_ui/
|
|
44
|
+
<%= image_tag "layered_ui/icon_panel_close.svg", alt: "", class: "l-ui-icon l-ui-icon--sm", aria: { hidden: true } %>
|
|
45
45
|
</button>
|
|
46
46
|
</div>
|
|
47
47
|
</div>
|
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.16.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- layered.ai
|
|
@@ -201,6 +201,7 @@ files:
|
|
|
201
201
|
- app/assets/images/layered_ui/icon_linkedin.svg
|
|
202
202
|
- app/assets/images/layered_ui/icon_mail.svg
|
|
203
203
|
- app/assets/images/layered_ui/icon_moon.svg
|
|
204
|
+
- app/assets/images/layered_ui/icon_panel_close.svg
|
|
204
205
|
- app/assets/images/layered_ui/icon_sun.svg
|
|
205
206
|
- app/assets/images/layered_ui/icon_x.svg
|
|
206
207
|
- app/assets/images/layered_ui/icon_youtube.svg
|
|
@@ -212,6 +213,7 @@ files:
|
|
|
212
213
|
- app/helpers/layered/ui/authentication_helper.rb
|
|
213
214
|
- app/helpers/layered/ui/breadcrumbs_helper.rb
|
|
214
215
|
- app/helpers/layered/ui/form_helper.rb
|
|
216
|
+
- app/helpers/layered/ui/modal_helper.rb
|
|
215
217
|
- app/helpers/layered/ui/navigation_helper.rb
|
|
216
218
|
- app/helpers/layered/ui/pagy_helper.rb
|
|
217
219
|
- app/helpers/layered/ui/ransack_helper.rb
|