layered-ui-rails 0.18.0 → 0.18.2
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 +4 -2
- data/.claude/skills/layered-ui-rails/references/CSS.md +10 -4
- data/.claude/skills/layered-ui-rails/references/HELPERS.md +16 -2
- data/AGENTS.md +1 -0
- data/CHANGELOG.md +23 -0
- data/app/assets/tailwind/layered_ui/engine.css +48 -2
- data/app/javascript/layered_ui/controllers/l_ui/navigation_section_controller.js +32 -1
- data/app/views/layouts/layered_ui/_panel.html.erb +1 -1
- data/lib/layered/ui/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b140e481d1660db5080801447a42917679da886a673473d2b6b53446e50c8d24
|
|
4
|
+
data.tar.gz: d838daaf32f4c0cbadffc12b599c6e6bfbcde149ab57296f826ac2ef6cf6423f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c0eb45b0d7a15313afeefc7b4ce39002c22fdcf8de41cb438865543c3d48926e9e307de14b86609ced6e642557745dcebaa4b60adb002c3fa154c57d8f0b6535
|
|
7
|
+
data.tar.gz: d475623cf9c06a78c0ec1486ace51231a914e7e6751c96aeeaeb653b893375171a23b0170af8258b2255f0d80897556399dfdee027211008a9f6931e32cfe0eb
|
|
@@ -132,6 +132,8 @@ Populate layout regions with `content_for` (always above the render call):
|
|
|
132
132
|
Body class modifiers:
|
|
133
133
|
- `l-ui-body--always-show-navigation` - pins navigation as a sidebar on desktop
|
|
134
134
|
- `l-ui-body--hide-header` - hides the header and collapses its space
|
|
135
|
+
- `l-ui-body--glass-header` - glass header (translucent + blur); content scrolls under it
|
|
136
|
+
- `l-ui-body--flush-top` - zeroes the page's top gutter so a hero sits flush at the top, behind the header (pair with `--glass-header` or `--hide-header`)
|
|
135
137
|
|
|
136
138
|
### Controller instance variables
|
|
137
139
|
|
|
@@ -153,7 +155,7 @@ Quick reference:
|
|
|
153
155
|
|
|
154
156
|
| Helper | Purpose |
|
|
155
157
|
|---|---|
|
|
156
|
-
| `l_ui_navigation_item(label, path, ...)` | Sidebar nav link (supports `icon:`, `match: :starts_with`, `expandable:`) |
|
|
158
|
+
| `l_ui_navigation_item(label, path, ...)` | Sidebar nav link (supports `icon:`, `match: :starts_with`, `expandable:`). For valid `icon:` names and the missing-asset gotcha, see the "Icons" section in `references/HELPERS.md` |
|
|
157
159
|
| `l_ui_navigation_section(heading = nil, ...)` | Group nav items; supports `collapsible:`, `storage_key:`, `separated:` |
|
|
158
160
|
| `l_ui_breadcrumbs(&block)` | Breadcrumb nav wrapper |
|
|
159
161
|
| `l_ui_breadcrumb_item(label, path = nil)` | Individual breadcrumb |
|
|
@@ -178,7 +180,7 @@ Key components:
|
|
|
178
180
|
|
|
179
181
|
| Component | Key classes |
|
|
180
182
|
|---|---|
|
|
181
|
-
| Page layout | `.l-ui-page`, `--with-navigation`, `__vertically-centered`, `__narrow` (narrow ~384px column, md:max-w-sm) |
|
|
183
|
+
| Page layout | `.l-ui-page`, `--with-navigation`, `__vertically-centered`, `__narrow` (narrow ~384px column, md:max-w-sm), `__contained` (wide column capped at `--l-ui-contained-width`) |
|
|
182
184
|
| Buttons | `.l-ui-button`, `--primary`, `--outline`, `--outline-danger`, `--full`, `--icon` |
|
|
183
185
|
| Surfaces | `.l-ui-surface`, `--highlighted`, `--sm`, `--collapsible`, `--collapsible-highlighted` |
|
|
184
186
|
| Forms | `.l-ui-form`, `.l-ui-form__group`, `.l-ui-form__field`, `.l-ui-label`, `.l-ui-select` |
|
|
@@ -24,7 +24,9 @@ Applied to `<body>` via the `:l_ui_body_class` yield to toggle layout-level beha
|
|
|
24
24
|
```
|
|
25
25
|
.l-ui-body--always-show-navigation Pin sidebar navigation open on desktop
|
|
26
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
|
|
27
|
+
.l-ui-body--header-contained Constrain the header's inner row to --l-ui-contained-width (landing pages)
|
|
28
|
+
.l-ui-body--glass-header Glass header (translucent + backdrop blur); content scrolls under it
|
|
29
|
+
.l-ui-body--flush-top Zero the page's top gutter so the first section sits flush at the top, behind the header
|
|
28
30
|
```
|
|
29
31
|
|
|
30
32
|
## Page layout
|
|
@@ -34,12 +36,13 @@ Applied to `<body>` via the `:l_ui_body_class` yield to toggle layout-level beha
|
|
|
34
36
|
.l-ui-page--with-navigation Left margin for sidebar on desktop
|
|
35
37
|
.l-ui-page__vertically-centered Centred layout element (e.g. login pages)
|
|
36
38
|
.l-ui-page__narrow Narrow column (md:max-w-sm, ~384px) for any compact, centred content
|
|
39
|
+
.l-ui-page__contained Centred column capped at --l-ui-contained-width; aligns with the contained header
|
|
37
40
|
.l-ui-bleed Breaks a child out to the page edge (full-bleed hero etc.)
|
|
38
41
|
```
|
|
39
42
|
|
|
40
|
-
`.l-ui-page` (and `--with-navigation`) is applied by the engine layout around your view's `yield` - you do not add it yourself. Wrapping your view content in another `.l-ui-page` nests two containers. The `__vertically-centered` and `__narrow` elements are used *inside* views for centred, compact content (the Devise auth pages are one example, but they are general-purpose). `__narrow` caps width at `md:max-w-sm` (~384px). For a wider content area,
|
|
43
|
+
`.l-ui-page` (and `--with-navigation`) is applied by the engine layout around your view's `yield` - you do not add it yourself. Wrapping your view content in another `.l-ui-page` nests two containers. The `__vertically-centered` and `__narrow` elements are used *inside* views for centred, compact content (the Devise auth pages are one example, but they are general-purpose). `__narrow` caps width at `md:max-w-sm` (~384px). For a wider content area, wrap your content in `.l-ui-page__contained`: it caps width at the `--l-ui-contained-width` token (default `80rem`) and centres with `mx-auto`. Because the contained header (`.l-ui-body--header-contained`) reads the same token, the two line up automatically - override `--l-ui-contained-width` once to move both. Left to itself, `.l-ui-page` holds content to the full available width.
|
|
41
44
|
|
|
42
|
-
The page gutter (horizontal and bottom padding) is the `--l-ui-gutter` custom property (default `1rem`), shared by `.l-ui-page` and `.l-ui-header` so they always align. Override it on a container to change the gutter in one place rather than reverse-engineering padding values. To take a single child edge-to-edge (a full-bleed hero, a banner), add `.l-ui-bleed` to it - it cancels exactly the current gutter, so no negative-margin guesswork. Note `.l-ui-bleed` only handles the horizontal edges; content still sits below the page's top padding (header offset + gutter).
|
|
45
|
+
The page gutter (horizontal and bottom padding) is the `--l-ui-gutter` custom property (default `1rem`), shared by `.l-ui-page` and `.l-ui-header` so they always align. Override it on a container to change the gutter in one place rather than reverse-engineering padding values. To take a single child edge-to-edge (a full-bleed hero, a banner), add `.l-ui-bleed` to it - it cancels exactly the current gutter, so no negative-margin guesswork. Note `.l-ui-bleed` only handles the horizontal edges; content still sits below the page's top padding (header offset + gutter). To take the *top* edge too - a hero that starts at the very top of the viewport, behind the header - add `.l-ui-body--flush-top`, which zeroes the page's top padding. On its own over the opaque header that would hide content, so pair it with `.l-ui-body--glass-header` (the header turns translucent and content shows through the blur) or `.l-ui-body--hide-header`. Give the hero its own internal top padding (at least `--header-height`) so its content clears the floating header.
|
|
43
46
|
|
|
44
47
|
`.l-ui-page` is a flex column (`flex flex-1 flex-col`) and uses `overflow-x-clip` to hold its content to the available width: intrinsically wide content (tables, code blocks, long unbroken strings) is clipped rather than expanding the page or adding a horizontal page scrollbar. So make such content scroll internally (e.g. wrap a table in an `overflow-x-auto` element) instead of expecting the page to grow.
|
|
45
48
|
|
|
@@ -60,7 +63,8 @@ Always combine the `l-ui-button` base class with a colour modifier (e.g. `l-ui-b
|
|
|
60
63
|
Icon variants (combine with the `l-ui-button` base):
|
|
61
64
|
|
|
62
65
|
```
|
|
63
|
-
.l-ui-button--icon Icon-only button (
|
|
66
|
+
.l-ui-button--icon Icon-only button (44px square, no text)
|
|
67
|
+
Add --small (l-ui-button--icon l-ui-button--small) for a 32px square icon button
|
|
64
68
|
.l-ui-button--navigation-toggle Mobile navigation toggle
|
|
65
69
|
```
|
|
66
70
|
|
|
@@ -391,6 +395,8 @@ Tier 2 - Full palette (override individually as needed):
|
|
|
391
395
|
--error-bg Error background
|
|
392
396
|
--error-text Error text
|
|
393
397
|
--header-height Header height (default 63px)
|
|
398
|
+
--l-ui-gutter Page/header horizontal + bottom padding (default 1rem)
|
|
399
|
+
--l-ui-contained-width Max width of contained content - header + .l-ui-page__contained (default 80rem)
|
|
394
400
|
```
|
|
395
401
|
|
|
396
402
|
Override --button-primary-text when your accent color needs a different text/icon color on buttons (e.g. a pink accent with white button text in dark mode). Override --button-primary-icon instead when only the icon color should change.
|
|
@@ -14,13 +14,13 @@ l_ui_navigation_section(heading = nil, icon: nil, icon_path: nil, icon_html: nil
|
|
|
14
14
|
- `path` (String) - URL
|
|
15
15
|
- `active` (Boolean, optional) - force the active style; defaults to a match against the current request path. Does not affect `aria-current`, which is set only when `current_page?(path)` is true
|
|
16
16
|
- `match` (Symbol) - `:exact` (default) uses `current_page?`; `:starts_with` activates when the request path begins with `path` (use for parents whose sub-routes should keep the parent highlighted)
|
|
17
|
-
- `icon` (String, optional) - icon name
|
|
17
|
+
- `icon` (String, optional) - icon name. Resolves to the asset `layered_ui/icon_NAME.svg`. See "Icons" below for the names that ship with the gem and how to add your own. A name with no matching asset raises `Propshaft::MissingAssetError` (a 500 in dev) when the page renders, so only pass a name you know resolves
|
|
18
18
|
- `icon_path` (String, optional) - explicit asset path; takes precedence over `icon:`
|
|
19
19
|
- `icon_html` (ActiveSupport::SafeBuffer, optional) - pre-rendered icon markup for icon-font libraries (e.g. `tag.i(class: "fa-solid fa-house")`); takes precedence over `icon:` and `icon_path:`. Must be already html-safe - plain strings will be escaped. Never pass user-controlled input
|
|
20
20
|
|
|
21
21
|
`l_ui_navigation_section`:
|
|
22
22
|
- `heading` (String, optional) - non-clickable section heading; omit for an unlabelled group. To expose the parent route of a section, add an "Overview" item inside the block
|
|
23
|
-
- `icon` (String, optional) -
|
|
23
|
+
- `icon` (String, optional) - icon name next to the heading; resolves the same way as `l_ui_navigation_item`'s `icon:` (see "Icons" below)
|
|
24
24
|
- `icon_path` (String, optional) - explicit asset path next to the heading; takes precedence over `icon:`
|
|
25
25
|
- `icon_html` (String, optional) - pre-rendered icon markup; takes precedence over `icon:` and `icon_path:`
|
|
26
26
|
- `collapsible` (Boolean) - when `true` and a heading is given, renders a toggle button with a chevron and wires `aria-controls` to the panel
|
|
@@ -43,6 +43,20 @@ l_ui_navigation_section(heading = nil, icon: nil, icon_path: nil, icon_html: nil
|
|
|
43
43
|
<% end %>
|
|
44
44
|
```
|
|
45
45
|
|
|
46
|
+
### Icons
|
|
47
|
+
|
|
48
|
+
The `icon:` argument (on `l_ui_navigation_item` and `l_ui_navigation_section`) resolves to the asset `layered_ui/icon_NAME.svg`. There is no fallback: a name with no matching asset raises `Propshaft::MissingAssetError` and renders a 500. Only pass a name you know resolves.
|
|
49
|
+
|
|
50
|
+
Names that ship with the gem (use the bare name, e.g. `icon: "globe"`):
|
|
51
|
+
|
|
52
|
+
- General-purpose (black `currentColor` sources, recoloured for dark mode by `dark:invert` - safe to use via `icon:`): `home`, `globe`, `mail`, `github`, `discord`, `linkedin`, `x`, `youtube`
|
|
53
|
+
- Internal UI (engine chrome - theme toggle, nav chevrons, close buttons): `close`, `chevron_down`, `chevron_right`, `hamburger`, `sun`, `moon`, `light`, `dark`, `panel_close`. Several are CSS mask shapes or carry baked theme colours (e.g. `chevron_down` is white, `dark` fills white), so they will **not** render correctly through `icon:`. Don't use these for nav icons
|
|
54
|
+
|
|
55
|
+
To add your own, drop an SVG in the host app at `app/assets/images/layered_ui/icon_NAME.svg` and pass `icon: "NAME"`. Alternatively:
|
|
56
|
+
|
|
57
|
+
- `icon_path:` - point at any asset path directly (e.g. `icon_path: "my_icons/star.svg"`), bypassing the `layered_ui/icon_` convention
|
|
58
|
+
- `icon_html:` - pass pre-rendered, html-safe markup for icon-font libraries (e.g. `tag.i(class: "fa-solid fa-house")`); never pass user-controlled input
|
|
59
|
+
|
|
46
60
|
## Breadcrumbs
|
|
47
61
|
|
|
48
62
|
```ruby
|
data/AGENTS.md
CHANGED
|
@@ -27,6 +27,7 @@ Guidance for AI agents working in this repository.
|
|
|
27
27
|
- Use l-ui classes only in engine views, with no additional Tailwind utilities, as Tailwind classes referenced only inside the engine will not be generated by the host app’s build
|
|
28
28
|
- Dummy app documentation pages may use additional Tailwind utilities, but should favour l-ui classes where possible
|
|
29
29
|
- Importmap for JS (no bundler)
|
|
30
|
+
- Put new changes on a branch with a meaningful name, but do not commit; the user will ask when ready to commit
|
|
30
31
|
- We are currently in pre-release, so breaking changes may be introduced. Flag any expected breaking changes clearly before making them, but do not avoid necessary improvements solely to preserve backwards compatibility
|
|
31
32
|
- CRITICAL: Retain WCAG 2.2 AA compliance. Do not be too pedantic though as this introduces audit loops which are undesirable.
|
|
32
33
|
- A WCAG 2.2 AA table looks like this:
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,29 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file. This project follows [Semantic Versioning](https://semver.org/).
|
|
4
4
|
|
|
5
|
+
## [0.18.2] - 2026-06-08
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- `--l-ui-contained-width` token (default `80rem`) and `l-ui-page__contained`, a centred content column capped at that width. The contained header (`l-ui-body--header-contained`) now reads the same token, so header and page content align and can be moved together with a single override.
|
|
10
|
+
- `l-ui-body--glass-header`: glass header (translucent background plus backdrop blur, dropping the solid bottom border) so page content scrolls beneath it.
|
|
11
|
+
- `l-ui-body--flush-top`: zeroes the page's top padding so the first section sits flush at the top of the viewport, behind the header. Pair with `l-ui-body--glass-header` (content shows through the frosted header) or `l-ui-body--hide-header` for full-bleed heroes.
|
|
12
|
+
|
|
13
|
+
## [0.18.1] - 2026-06-07
|
|
14
|
+
|
|
15
|
+
### Added
|
|
16
|
+
|
|
17
|
+
- Small icon buttons: combining `l-ui-button--icon` with `l-ui-button--small` now renders a 32px square (still above the WCAG 2.2 AA 24px target-size minimum) instead of stretching to 44px wide, since `--small` previously only shrank the height. The panel close button now uses this variant.
|
|
18
|
+
- Documented the icons bundled with the gem and clarified `icon:` usage: the name resolves to `layered_ui/icon_NAME.svg`, and an unknown name raises `Propshaft::MissingAssetError`. Added an "Available icons" section to the dummy app showing the general-purpose set (internal-UI chrome icons are not usable via `icon:`).
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
|
|
22
|
+
- Navigation sub-sections now animate open by transitioning height from 0 to their natural height, replacing the per-item slide keyframe animation. Respects `prefers-reduced-motion`.
|
|
23
|
+
|
|
24
|
+
### Fixed
|
|
25
|
+
|
|
26
|
+
- Navigation section chevron no longer animates when restoring its saved open/closed state on page load; it now renders in position.
|
|
27
|
+
|
|
5
28
|
## [0.18.0] - 2026-06-07
|
|
6
29
|
|
|
7
30
|
### Fixed
|
|
@@ -81,6 +81,7 @@
|
|
|
81
81
|
--error-text: oklch(0.2248 0.0874 28.11);
|
|
82
82
|
--header-height: 63px;
|
|
83
83
|
--l-ui-gutter: 1rem;
|
|
84
|
+
--l-ui-contained-width: 80rem;
|
|
84
85
|
}
|
|
85
86
|
|
|
86
87
|
.dark {
|
|
@@ -466,6 +467,13 @@
|
|
|
466
467
|
@apply md:ml-[256px];
|
|
467
468
|
}
|
|
468
469
|
|
|
470
|
+
/* Zero the page's top gutter so the first section sits flush at the viewport top, behind the
|
|
471
|
+
header. Intended with .l-ui-body--glass-header (content shows through the glass header) or
|
|
472
|
+
.l-ui-body--hide-header; over an opaque header it would tuck content out of sight. */
|
|
473
|
+
.l-ui-body--flush-top .l-ui-page {
|
|
474
|
+
@apply pt-0;
|
|
475
|
+
}
|
|
476
|
+
|
|
469
477
|
.l-ui-page__vertically-centered {
|
|
470
478
|
@apply flex flex-1 flex-col items-center justify-center;
|
|
471
479
|
}
|
|
@@ -477,6 +485,12 @@
|
|
|
477
485
|
my-auto;
|
|
478
486
|
}
|
|
479
487
|
|
|
488
|
+
/* Centred content column capped at --l-ui-contained-width; aligns with the contained header. */
|
|
489
|
+
.l-ui-page__contained {
|
|
490
|
+
@apply w-full max-w-[var(--l-ui-contained-width)]
|
|
491
|
+
mx-auto;
|
|
492
|
+
}
|
|
493
|
+
|
|
480
494
|
/* Header */
|
|
481
495
|
|
|
482
496
|
.l-ui-header-container {
|
|
@@ -490,6 +504,12 @@
|
|
|
490
504
|
@apply hidden;
|
|
491
505
|
}
|
|
492
506
|
|
|
507
|
+
.l-ui-body--glass-header .l-ui-header-container {
|
|
508
|
+
@apply bg-background/60
|
|
509
|
+
border-b-transparent
|
|
510
|
+
backdrop-blur-xl;
|
|
511
|
+
}
|
|
512
|
+
|
|
493
513
|
.l-ui-header {
|
|
494
514
|
@apply flex items-center justify-between
|
|
495
515
|
h-[var(--header-height)]
|
|
@@ -497,7 +517,7 @@
|
|
|
497
517
|
}
|
|
498
518
|
|
|
499
519
|
.l-ui-body--header-contained .l-ui-header {
|
|
500
|
-
@apply w-full max-w-
|
|
520
|
+
@apply w-full max-w-[var(--l-ui-contained-width)]
|
|
501
521
|
mx-auto;
|
|
502
522
|
}
|
|
503
523
|
|
|
@@ -747,8 +767,16 @@
|
|
|
747
767
|
mask-size: contain;
|
|
748
768
|
}
|
|
749
769
|
|
|
770
|
+
/* transition-none is intentional: the after-change style determines the
|
|
771
|
+
transition, so this gives "animate open, snap shut" - opening matches the
|
|
772
|
+
base rule (animates), closing matches this rule (snaps instantly). */
|
|
750
773
|
.l-ui-navigation__section-toggle[aria-expanded="false"] .l-ui-navigation__section-chevron {
|
|
751
|
-
@apply -rotate-90
|
|
774
|
+
@apply -rotate-90
|
|
775
|
+
transition-none;
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
.l-ui-navigation__section-toggle--no-transition .l-ui-navigation__section-chevron {
|
|
779
|
+
@apply transition-none;
|
|
752
780
|
}
|
|
753
781
|
|
|
754
782
|
.l-ui-navigation__section-items {
|
|
@@ -771,6 +799,19 @@
|
|
|
771
799
|
gap-0.5 px-3 py-3;
|
|
772
800
|
}
|
|
773
801
|
|
|
802
|
+
/* Height-expand slide applied while the section is opening; the controller
|
|
803
|
+
measures the natural height and transitions from 0 to reveal the items. */
|
|
804
|
+
.l-ui-navigation__section-items--opening {
|
|
805
|
+
@apply overflow-hidden
|
|
806
|
+
transition-[height] duration-200 ease-out;
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
@media (prefers-reduced-motion: reduce) {
|
|
810
|
+
.l-ui-navigation__section-items--opening {
|
|
811
|
+
@apply transition-none;
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
|
|
774
815
|
.l-ui-navigation__user {
|
|
775
816
|
@apply shrink-0
|
|
776
817
|
p-4;
|
|
@@ -897,6 +938,11 @@
|
|
|
897
938
|
text-xs;
|
|
898
939
|
}
|
|
899
940
|
|
|
941
|
+
.l-ui-button--icon.l-ui-button--small {
|
|
942
|
+
@apply min-w-[32px]
|
|
943
|
+
p-1.5;
|
|
944
|
+
}
|
|
945
|
+
|
|
900
946
|
.l-ui-button:disabled {
|
|
901
947
|
@apply opacity-50
|
|
902
948
|
cursor-not-allowed;
|
|
@@ -21,12 +21,21 @@ export default class extends Controller {
|
|
|
21
21
|
if (stored === null) return
|
|
22
22
|
|
|
23
23
|
const isOpen = stored === "true"
|
|
24
|
-
|
|
24
|
+
// Apply the restored state without animating the chevron on load; it should
|
|
25
|
+
// simply render in the correct position.
|
|
26
|
+
const toggle = this.toggleTarget
|
|
27
|
+
toggle.classList.add("l-ui-navigation__section-toggle--no-transition")
|
|
28
|
+
toggle.setAttribute("aria-expanded", isOpen ? "true" : "false")
|
|
25
29
|
if (isOpen) {
|
|
26
30
|
this.panelTarget.removeAttribute("hidden")
|
|
27
31
|
} else {
|
|
28
32
|
this.panelTarget.setAttribute("hidden", "")
|
|
29
33
|
}
|
|
34
|
+
requestAnimationFrame(() => {
|
|
35
|
+
requestAnimationFrame(() => {
|
|
36
|
+
toggle.classList.remove("l-ui-navigation__section-toggle--no-transition")
|
|
37
|
+
})
|
|
38
|
+
})
|
|
30
39
|
}
|
|
31
40
|
|
|
32
41
|
toggle() {
|
|
@@ -34,6 +43,7 @@ export default class extends Controller {
|
|
|
34
43
|
this.toggleTarget.setAttribute("aria-expanded", isOpen ? "true" : "false")
|
|
35
44
|
if (isOpen) {
|
|
36
45
|
this.panelTarget.removeAttribute("hidden")
|
|
46
|
+
this.#animateOpen()
|
|
37
47
|
} else {
|
|
38
48
|
this.panelTarget.setAttribute("hidden", "")
|
|
39
49
|
}
|
|
@@ -45,4 +55,25 @@ export default class extends Controller {
|
|
|
45
55
|
// ignore
|
|
46
56
|
}
|
|
47
57
|
}
|
|
58
|
+
|
|
59
|
+
// Slide the panel open by transitioning its height from 0 to its natural
|
|
60
|
+
// height, only on user-initiated open, not on initial page load.
|
|
61
|
+
#animateOpen() {
|
|
62
|
+
const panel = this.panelTarget
|
|
63
|
+
const CLASS = "l-ui-navigation__section-items--opening"
|
|
64
|
+
|
|
65
|
+
if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) return
|
|
66
|
+
|
|
67
|
+
const endHeight = panel.scrollHeight
|
|
68
|
+
panel.classList.add(CLASS)
|
|
69
|
+
panel.style.height = "0px"
|
|
70
|
+
panel.offsetHeight // Force a reflow so the starting height is applied.
|
|
71
|
+
panel.style.height = `${endHeight}px`
|
|
72
|
+
|
|
73
|
+
panel.addEventListener("transitionend", (event) => {
|
|
74
|
+
if (event.propertyName !== "height") return
|
|
75
|
+
panel.classList.remove(CLASS)
|
|
76
|
+
panel.style.height = ""
|
|
77
|
+
}, { once: true })
|
|
78
|
+
}
|
|
48
79
|
}
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
|
|
36
36
|
<div class="l-ui-panel__header-actions">
|
|
37
37
|
<button type="button"
|
|
38
|
-
class="l-ui-button l-ui-button--primary l-ui-button--icon"
|
|
38
|
+
class="l-ui-button l-ui-button--primary l-ui-button--icon l-ui-button--small"
|
|
39
39
|
aria-label="Hide panel"
|
|
40
40
|
aria-controls="panel"
|
|
41
41
|
title="Toggle panel (Ctrl+i / ⌘i)"
|
data/lib/layered/ui/version.rb
CHANGED