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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 45a2361df4b5409267f1619b3ba0de294c2e90a56e610492e9587dc40c9a79be
4
- data.tar.gz: e812a5155ed72c2d0c9fe6668a74215fd4600decf3326d08190a76936b43de2f
3
+ metadata.gz: 2acf474942e7d6fa669959fcf0c78675d57e9ae065f18347ddbb3a9f1a48b7fc
4
+ data.tar.gz: a9a2c5123723a239582523090095f2539c01ba7439335bcffc464d51bd9ffabe
5
5
  SHA512:
6
- metadata.gz: ef9f4e14b361fcc2a6d3d4db9e4629a97533d24d479f3fba123daa58278d0a53be18c1cd3526ed4a70cf4ef9fd7cf65a243a303249bb8eb81bfa2c7c55efbb1e
7
- data.tar.gz: d1f369d793b03173d8ba0c44616b4452841f07391c846744738b426f5c6f7af87ffd85250ffabf1a1248f59da74947a4df32288c5c63c63232ea97f8efb63885
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 (fixed size, no text)
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; `"home"` ships with the gem. For others, supply the SVG in the host app at `app/assets/images/layered_ui/icon_NAME.svg`
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) - host-app icon name next to the heading; resolves the same way as `l_ui_navigation_item`'s `icon:`
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
- this.toggleTarget.setAttribute("aria-expanded", isOpen ? "true" : "false")
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)"
@@ -1,5 +1,5 @@
1
1
  module Layered
2
2
  module Ui
3
- VERSION = "0.18.0"
3
+ VERSION = "0.18.1"
4
4
  end
5
5
  end
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.18.0
4
+ version: 0.18.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - layered.ai