layered-ui-rails 0.18.0 → 0.18.1
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/CSS.md +2 -1
- data/.claude/skills/layered-ui-rails/references/HELPERS.md +16 -2
- data/AGENTS.md +1 -0
- data/CHANGELOG.md +15 -0
- data/app/assets/tailwind/layered_ui/engine.css +27 -1
- 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: 2acf474942e7d6fa669959fcf0c78675d57e9ae065f18347ddbb3a9f1a48b7fc
|
|
4
|
+
data.tar.gz: a9a2c5123723a239582523090095f2539c01ba7439335bcffc464d51bd9ffabe
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 805599d0bc604bdde66d6092b283ae078024b5b0a1761b2dcd8795926f240439861fe63c591dbd8c06357cf16d39c3b46111e4356f2051b3b662e9a664334821
|
|
7
|
+
data.tar.gz: d84bad400743d3e09478ba82d1e735037051758d6b5e1a65dcd93613c6a24569823c1d578b8fddcb5b88b53fab2225a5c0c2abc4e42afb17099920099c031822
|
|
@@ -153,7 +153,7 @@ Quick reference:
|
|
|
153
153
|
|
|
154
154
|
| Helper | Purpose |
|
|
155
155
|
|---|---|
|
|
156
|
-
| `l_ui_navigation_item(label, path, ...)` | Sidebar nav link (supports `icon:`, `match: :starts_with`, `expandable:`) |
|
|
156
|
+
| `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
157
|
| `l_ui_navigation_section(heading = nil, ...)` | Group nav items; supports `collapsible:`, `storage_key:`, `separated:` |
|
|
158
158
|
| `l_ui_breadcrumbs(&block)` | Breadcrumb nav wrapper |
|
|
159
159
|
| `l_ui_breadcrumb_item(label, path = nil)` | Individual breadcrumb |
|
|
@@ -60,7 +60,8 @@ Always combine the `l-ui-button` base class with a colour modifier (e.g. `l-ui-b
|
|
|
60
60
|
Icon variants (combine with the `l-ui-button` base):
|
|
61
61
|
|
|
62
62
|
```
|
|
63
|
-
.l-ui-button--icon Icon-only button (
|
|
63
|
+
.l-ui-button--icon Icon-only button (44px square, no text)
|
|
64
|
+
Add --small (l-ui-button--icon l-ui-button--small) for a 32px square icon button
|
|
64
65
|
.l-ui-button--navigation-toggle Mobile navigation toggle
|
|
65
66
|
```
|
|
66
67
|
|
|
@@ -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,21 @@
|
|
|
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.1] - 2026-06-07
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- 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.
|
|
10
|
+
- 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:`).
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
- 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`.
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
|
|
18
|
+
- Navigation section chevron no longer animates when restoring its saved open/closed state on page load; it now renders in position.
|
|
19
|
+
|
|
5
20
|
## [0.18.0] - 2026-06-07
|
|
6
21
|
|
|
7
22
|
### Fixed
|
|
@@ -747,8 +747,16 @@
|
|
|
747
747
|
mask-size: contain;
|
|
748
748
|
}
|
|
749
749
|
|
|
750
|
+
/* transition-none is intentional: the after-change style determines the
|
|
751
|
+
transition, so this gives "animate open, snap shut" - opening matches the
|
|
752
|
+
base rule (animates), closing matches this rule (snaps instantly). */
|
|
750
753
|
.l-ui-navigation__section-toggle[aria-expanded="false"] .l-ui-navigation__section-chevron {
|
|
751
|
-
@apply -rotate-90
|
|
754
|
+
@apply -rotate-90
|
|
755
|
+
transition-none;
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
.l-ui-navigation__section-toggle--no-transition .l-ui-navigation__section-chevron {
|
|
759
|
+
@apply transition-none;
|
|
752
760
|
}
|
|
753
761
|
|
|
754
762
|
.l-ui-navigation__section-items {
|
|
@@ -771,6 +779,19 @@
|
|
|
771
779
|
gap-0.5 px-3 py-3;
|
|
772
780
|
}
|
|
773
781
|
|
|
782
|
+
/* Height-expand slide applied while the section is opening; the controller
|
|
783
|
+
measures the natural height and transitions from 0 to reveal the items. */
|
|
784
|
+
.l-ui-navigation__section-items--opening {
|
|
785
|
+
@apply overflow-hidden
|
|
786
|
+
transition-[height] duration-200 ease-out;
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
@media (prefers-reduced-motion: reduce) {
|
|
790
|
+
.l-ui-navigation__section-items--opening {
|
|
791
|
+
@apply transition-none;
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
|
|
774
795
|
.l-ui-navigation__user {
|
|
775
796
|
@apply shrink-0
|
|
776
797
|
p-4;
|
|
@@ -897,6 +918,11 @@
|
|
|
897
918
|
text-xs;
|
|
898
919
|
}
|
|
899
920
|
|
|
921
|
+
.l-ui-button--icon.l-ui-button--small {
|
|
922
|
+
@apply min-w-[32px]
|
|
923
|
+
p-1.5;
|
|
924
|
+
}
|
|
925
|
+
|
|
900
926
|
.l-ui-button:disabled {
|
|
901
927
|
@apply opacity-50
|
|
902
928
|
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