layered-ui-rails 0.1.3 → 0.1.4

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: 417904020be6218b684bb12df4c6089c04d79334ec1cd65a207de6b21e3cda15
4
- data.tar.gz: 67b5030949e4b2722d2fc341d7fed3dcfd3f8d48dc4b90e6905e651dc6ecc6b8
3
+ metadata.gz: a2e3c4b089f447a7d40452272925b236cfb43da5d8c8414dff4187c80505840a
4
+ data.tar.gz: 72e580c90993476ed99f68bee967928ef31241c936582bf02b3816a2616028ab
5
5
  SHA512:
6
- metadata.gz: '05296252d95be8c3a4971351ba9a23b9481d026e27bd931ff4b69a0c53a53574d30174162c69895a6ecc4294e0bf8db5b84e3478b7b44ed3c9e6d9329efda56c'
7
- data.tar.gz: cd87a0ef43243f98ddcb19d05cbe7d0d1f79ed52f0156e00924725eac9619ddc2c4a27bbb951d1b07df7c8e865cf8b74f93f80e5ef96ae36311e96686fa99b82
6
+ metadata.gz: 6a6d460c812709f9063dcfa85644a0bf65b5b0c172a79bd2cc0a31e7775bb7f6f58c6e0a1090b4d20013ab142647b5836238c7726dd32325637202445f50f84c
7
+ data.tar.gz: e15e51d28fc6ba387589acf87f4e5c725c2973e7360bc84d67fc7541c09d094c1258343a643ca343352eaeb8f0959d027e4214f59a496bab9172f74bf76992e2
data/AGENTS.md CHANGED
@@ -13,8 +13,9 @@ Guidance for AI agents working in this repository.
13
13
  - **CSS** `app/assets/tailwind/layered/ui/styles.css`: HSL tokens, `.dark` on `<html>`, `@theme` utilities (`bg-background`, etc.), BEM components (`.l-ui-button--primary`, etc.). Layout: 63px header, 240px sidebar, 320px panel. WCAG 2.2 AA.
14
14
  - **CSS `@apply`:** Multi-line with grouping (layout → spacing → typography → colors → effects). Single utilities may stay on one line.
15
15
  - **Generators:** `bin/rails generate layered:ui:install` (copy CSS, import CSS, import JS)
16
- - **JS** `app/javascript/layered_ui/`: Stimulus controllers registered as `l-ui--theme`, `l-ui--mobile-navigation`, `l-ui--panel`, `l-ui--modal`, `l-ui--tabs`
17
- - **Layout yields** (prefixed `l_ui_`): `:l_ui_navigation_items`, `:l_ui_panel_heading`, `:l_ui_panel_body`
16
+ - **JS** `app/javascript/layered_ui/`: Stimulus controllers registered as `l-ui--theme`, `l-ui--navigation`, `l-ui--panel`, `l-ui--modal`, `l-ui--tabs`
17
+ - **Layout yields** (prefixed `l_ui_`): `:l_ui_navigation_items`, `:l_ui_panel_heading`, `:l_ui_panel_body`, `:l_ui_body_class`
18
+ - `:l_ui_body_class` modifiers: `l-ui-body--always-show-navigation` (pins nav as sidebar on desktop), `l-ui-body--hide-header` (hides header and collapses its space)
18
19
 
19
20
  ## Testing
20
21
 
data/CHANGELOG.md CHANGED
@@ -4,6 +4,21 @@ All notable changes to this project will be documented in this file. This projec
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.1.4] - 2026-03-25
8
+
9
+ ### Added
10
+
11
+ - `yield :l_ui_body_class` in the engine layout body tag, allowing host apps to pass modifier classes via `content_for`
12
+ - `l-ui-body--always-show-navigation` modifier pins the navigation as a persistent sidebar on desktop (suitable for admin layouts)
13
+ - `l-ui-body--hide-header` modifier hides the header and collapses its reserved space
14
+ - Disabled state styles for all button variants
15
+
16
+ ### Changed
17
+
18
+ - New default: Navigation is now off-canvas on all screen sizes, toggled via the header button - use `l-ui-body--always-show-navigation` to restore the pinned sidebar behaviour
19
+ - Renamed Stimulus controller from `l-ui--mobile-navigation` to `l-ui--navigation` as it now has utility on both desktop and mobile
20
+ - Renamed `l-ui-button--mobile-navigation` to `l-ui-button--navigation-toggle`
21
+
7
22
  ## [0.1.3] - 2026-03-14
8
23
 
9
24
  ### Added
data/README.md CHANGED
@@ -100,7 +100,14 @@ This project is still in its early days. We welcome issues, feedback, and ideas
100
100
 
101
101
  ## License
102
102
 
103
- Released under the [Apache 2.0 License](LICENSE). The source code is fully open, but the layered.ai name and brand are trademarks of LAYERED AI LIMITED (UK company number: 17056830).
103
+ Released under the [Apache 2.0 License](LICENSE).
104
+
105
+ Copyright 2026 LAYERED AI LIMITED (UK company number: 17056830). See [NOTICE](NOTICE) for attribution details.
106
+
107
+ ## Trademarks
108
+
109
+ The source code is fully open, but the layered.ai name, logo, and brand assets are trademarks of LAYERED AI LIMITED. The Apache 2.0 license does not grant rights to use the layered.ai branding. Forks and redistributions must use a distinct name. See [TRADEMARK.md](TRADEMARK.md) for the full policy.
110
+
111
+ ## Contributing
104
112
 
105
- - [TRADEMARK.md](TRADEMARK.md) - trademark and brand policy
106
113
  - [CLA.md](CLA.md) - contributor license agreement
@@ -424,6 +424,10 @@
424
424
  text-foreground;
425
425
  }
426
426
 
427
+ .l-ui-body--hide-header {
428
+ --header-height: 0px;
429
+ }
430
+
427
431
  /* Page */
428
432
 
429
433
  .l-ui-page {
@@ -437,7 +441,11 @@
437
441
  }
438
442
 
439
443
  .l-ui-page--with-navigation {
440
- @apply page md:ml-[240px];
444
+ @apply page;
445
+ }
446
+
447
+ .l-ui-body--always-show-navigation .l-ui-page--with-navigation {
448
+ @apply md:ml-[240px];
441
449
  }
442
450
 
443
451
  .l-ui-page--vertically-centered {
@@ -459,6 +467,10 @@
459
467
  bg-background;
460
468
  }
461
469
 
470
+ .l-ui-body--hide-header .l-ui-container--header {
471
+ @apply hidden;
472
+ }
473
+
462
474
  .l-ui-header {
463
475
  @apply flex items-center justify-between
464
476
  h-[var(--header-height)] px-4 py-3;
@@ -561,8 +573,8 @@
561
573
  z-50
562
574
  border-r border-t border-border
563
575
  bg-background
564
- invisible md:visible
565
- transform -translate-x-full md:translate-x-0
576
+ invisible
577
+ -translate-x-full
566
578
  transition-transform duration-200;
567
579
  }
568
580
 
@@ -570,19 +582,26 @@
570
582
  @apply visible translate-x-0;
571
583
  }
572
584
 
585
+ .l-ui-body--always-show-navigation .l-ui-container--navigation {
586
+ @apply md:visible md:translate-x-0;
587
+ }
588
+
573
589
  .l-ui-backdrop--navigation {
574
590
  @apply fixed top-[var(--header-height)] left-0 right-0 bottom-0
575
591
  z-[45]
576
592
  bg-backdrop/50 backdrop-blur-xs
577
593
  opacity-0 pointer-events-none
578
- transition-opacity duration-200
579
- md:hidden;
594
+ transition-opacity duration-200;
580
595
  }
581
596
 
582
597
  .l-ui-backdrop--navigation.open {
583
598
  @apply opacity-100 pointer-events-auto;
584
599
  }
585
600
 
601
+ .l-ui-body--always-show-navigation .l-ui-backdrop--navigation {
602
+ @apply md:hidden;
603
+ }
604
+
586
605
  .l-ui-navigation {
587
606
  @apply flex flex-col
588
607
  h-full;
@@ -703,15 +722,18 @@
703
722
  text-danger;
704
723
  }
705
724
 
706
- .l-ui-button--mobile-navigation {
725
+ .l-ui-button--navigation-toggle {
707
726
  @apply flex items-center justify-center
708
727
  min-w-[44px] min-h-[44px] p-2
709
728
  cursor-pointer
710
729
  text-foreground
711
730
  rounded-sm
712
731
  focus-ring
713
- transition-colors
714
- md:hidden;
732
+ transition-colors;
733
+ }
734
+
735
+ .l-ui-body--always-show-navigation .l-ui-button--navigation-toggle {
736
+ @apply md:hidden;
715
737
  }
716
738
 
717
739
  .l-ui-button--icon {
@@ -724,6 +746,11 @@
724
746
  transition-colors;
725
747
  }
726
748
 
749
+ .l-ui-button--disabled {
750
+ @apply opacity-50
751
+ cursor-not-allowed;
752
+ }
753
+
727
754
  /* Notice */
728
755
 
729
756
  @utility notice {
@@ -33,7 +33,7 @@ export default class extends Controller {
33
33
  }
34
34
 
35
35
  close(event) {
36
- // Close menu when clicking outside on mobile or pressing Escape
36
+ // Close menu when clicking outside or pressing Escape
37
37
  if (event.type === "keydown" && event.key !== "Escape") return
38
38
 
39
39
  if (this.hasNavigationTarget && this.isOpen) {
@@ -122,11 +122,13 @@ export default class extends Controller {
122
122
  handleResize() {
123
123
  if (!this.hasNavigationTarget) return
124
124
 
125
- if (isMobile()) {
125
+ // In overlay mode (default), always respect isOpen state regardless of viewport
126
+ if (isMobile() || !this.alwaysShow) {
126
127
  this.setNavigationInteractivity(this.isOpen)
127
128
  return
128
129
  }
129
130
 
131
+ // Desktop in always-show mode: force nav visible
130
132
  this.isOpen = false
131
133
  this.navigationTarget.classList.remove("open")
132
134
  this.backdropTarget.classList.remove("open")
@@ -145,7 +147,7 @@ export default class extends Controller {
145
147
  }
146
148
 
147
149
  setNavigationInteractivity(isOpen) {
148
- if (isMobile() && !isOpen) {
150
+ if (!this.alwaysShow && !isOpen) {
149
151
  this.navigationTarget.setAttribute("inert", "")
150
152
  this.navigationTarget.setAttribute("aria-hidden", "true")
151
153
  return
@@ -154,4 +156,8 @@ export default class extends Controller {
154
156
  this.navigationTarget.removeAttribute("inert")
155
157
  this.navigationTarget.removeAttribute("aria-hidden")
156
158
  }
159
+
160
+ get alwaysShow() {
161
+ return this.element.classList.contains("l-ui-body--always-show-navigation")
162
+ }
157
163
  }
@@ -1,16 +1,15 @@
1
1
  import { Controller } from "@hotwired/stimulus"
2
2
  import { storageGetJSON, storageSet } from "layered_ui/utilities/storage"
3
- import { isMobile, hasNavigation, HEADER_HEIGHT, NAV_WIDTH, getPadding, getLeftEdge } from "layered_ui/utilities/layout"
3
+ import { getHeaderHeight, getPadding, getLeftEdge } from "layered_ui/utilities/layout"
4
4
 
5
5
  const BUTTON_SIZE = 56
6
6
  const DRAG_THRESHOLD = 5
7
7
  const SNAP_TIMEOUT = 400
8
- const TOGGLE_DELAY = 220
8
+ const TOGGLE_DELAY = 160
9
9
  const TOP_LEFT = "top-left"
10
10
  const TOP_RIGHT = "top-right"
11
11
  const BOTTOM_LEFT = "bottom-left"
12
12
  const BOTTOM_RIGHT = "bottom-right"
13
-
14
13
  export default class extends Controller {
15
14
  connect() {
16
15
  this.isDragging = false
@@ -95,7 +94,7 @@ export default class extends Controller {
95
94
  ? parseFloat(this.element.style.top)
96
95
  : this.element.getBoundingClientRect().top
97
96
 
98
- const topEdge = this.clampTop(HEADER_HEIGHT + getPadding())
97
+ const topEdge = this.clampTop(getHeaderHeight() + getPadding())
99
98
  const bottomEdge = this.clampTop(window.innerHeight - BUTTON_SIZE - getPadding())
100
99
  const midY = (topEdge + bottomEdge) / 2
101
100
  const isTop = topPx <= midY
@@ -151,7 +150,7 @@ export default class extends Controller {
151
150
  moveToCorner(corner) {
152
151
  const leftEdge = getLeftEdge()
153
152
  const rightEdge = getPadding()
154
- const topEdge = this.clampTop(HEADER_HEIGHT + getPadding())
153
+ const topEdge = this.clampTop(getHeaderHeight() + getPadding())
155
154
  const bottomEdge = this.clampTop(window.innerHeight - BUTTON_SIZE - getPadding())
156
155
 
157
156
  switch (corner) {
@@ -343,7 +342,7 @@ export default class extends Controller {
343
342
  // Clamp a top value within viewport bounds, below the header
344
343
  clampTop(topPx) {
345
344
  const padding = getPadding()
346
- const minTop = HEADER_HEIGHT + padding
345
+ const minTop = getHeaderHeight() + padding
347
346
  const maxTop = window.innerHeight - BUTTON_SIZE - padding
348
347
  return Math.min(Math.max(topPx, minTop), maxTop)
349
348
  }
@@ -1,6 +1,6 @@
1
1
  import { application } from "controllers/application"
2
2
  import ThemeController from "layered_ui/controllers/l_ui/theme_controller"
3
- import MobileNavigationController from "layered_ui/controllers/l_ui/mobile_navigation_controller"
3
+ import NavigationController from "layered_ui/controllers/l_ui/navigation_controller"
4
4
  import PanelController from "layered_ui/controllers/l_ui/panel_controller"
5
5
  import PanelResizeController from "layered_ui/controllers/l_ui/panel_resize_controller"
6
6
  import PanelButtonController from "layered_ui/controllers/l_ui/panel_button_controller"
@@ -8,7 +8,7 @@ import ModalController from "layered_ui/controllers/l_ui/modal_controller"
8
8
  import TabsController from "layered_ui/controllers/l_ui/tabs_controller"
9
9
 
10
10
  application.register("l-ui--theme", ThemeController)
11
- application.register("l-ui--mobile-navigation", MobileNavigationController)
11
+ application.register("l-ui--navigation", NavigationController)
12
12
  application.register("l-ui--panel", PanelController)
13
13
  application.register("l-ui--panel-resize", PanelResizeController)
14
14
  application.register("l-ui--panel-button", PanelButtonController)
@@ -1,6 +1,5 @@
1
1
  export const MOBILE_BREAKPOINT = 768
2
2
  export const NAV_WIDTH = 240
3
- export const HEADER_HEIGHT = 63
4
3
 
5
4
  export function isMobile() {
6
5
  return window.innerWidth < MOBILE_BREAKPOINT
@@ -8,13 +7,18 @@ export function isMobile() {
8
7
 
9
8
  export function hasNavigation() {
10
9
  const page = document.querySelector(".l-ui-page")
11
- return page && page.classList.contains("l-ui-page--with-navigation") && !isMobile()
10
+ const alwaysShow = document.body.classList.contains("l-ui-body--always-show-navigation")
11
+ return page && page.classList.contains("l-ui-page--with-navigation") && !isMobile() && alwaysShow
12
12
  }
13
13
 
14
14
  export function getPadding() {
15
15
  return isMobile() ? 16 : 32
16
16
  }
17
17
 
18
+ export function getHeaderHeight() {
19
+ return parseFloat(getComputedStyle(document.body).getPropertyValue("--header-height")) || 0
20
+ }
21
+
18
22
  export function getLeftEdge() {
19
23
  const navWidth = hasNavigation() ? NAV_WIDTH : 0
20
24
  return navWidth + getPadding()
@@ -34,15 +34,15 @@
34
34
  <% if yield(:l_ui_navigation_items).present? || l_ui_user_signed_in? %>
35
35
  <button
36
36
  type="button"
37
- class="l-ui-button--mobile-navigation"
38
- data-action="click->l-ui--mobile-navigation#toggle"
39
- data-l-ui--mobile-navigation-target="toggleButton"
37
+ class="l-ui-button--navigation-toggle"
38
+ data-action="click->l-ui--navigation#toggle"
39
+ data-l-ui--navigation-target="toggleButton"
40
40
  aria-label="Toggle navigation menu"
41
41
  aria-expanded="false"
42
- aria-controls="mobile-navigation"
42
+ aria-controls="l-ui-navigation"
43
43
  >
44
- <%= image_tag "layered_ui/icon_hamburger.svg", alt: "", class: "l-ui-icon--md", data: { "l-ui--mobile-navigation-target": "openIcon" }, aria: { hidden: true } %>
45
- <%= image_tag "layered_ui/icon_close.svg", alt: "", class: "l-ui-icon--md", style: "display: none;", data: { "l-ui--mobile-navigation-target": "closeIcon" }, aria: { hidden: true } %>
44
+ <%= image_tag "layered_ui/icon_hamburger.svg", alt: "", class: "l-ui-icon--md", data: { "l-ui--navigation-target": "openIcon" }, aria: { hidden: true } %>
45
+ <%= image_tag "layered_ui/icon_close.svg", alt: "", class: "l-ui-icon--md", style: "display: none;", data: { "l-ui--navigation-target": "closeIcon" }, aria: { hidden: true } %>
46
46
  </button>
47
47
  <% end %>
48
48
  </nav>
@@ -1,5 +1,5 @@
1
1
  <% if yield(:l_ui_navigation_items).present? || l_ui_user_signed_in? %>
2
- <nav id="mobile-navigation" class="l-ui-container--navigation" aria-label="Main navigation" data-l-ui--mobile-navigation-target="navigation">
2
+ <nav id="l-ui-navigation" class="l-ui-container--navigation" aria-label="Main navigation" data-l-ui--navigation-target="navigation">
3
3
  <div class="l-ui-navigation">
4
4
  <ul class="l-ui-navigation__links" role="list">
5
5
  <%= yield :l_ui_navigation_items %>
@@ -22,12 +22,12 @@
22
22
  <%= javascript_importmap_tags %>
23
23
  </head>
24
24
 
25
- <body class="l-ui-body" data-controller="l-ui--mobile-navigation" data-action="click@window->l-ui--mobile-navigation#close keydown.esc@window->l-ui--mobile-navigation#close">
25
+ <body class="l-ui-body <%= yield :l_ui_body_class %>" data-controller="l-ui--navigation" data-action="click@window->l-ui--navigation#close keydown.esc@window->l-ui--navigation#close">
26
26
  <a href="#main-content" class="l-ui-skip-link">Skip to main content</a>
27
27
  <div id="l-ui-live-region" class="l-ui-sr-only" aria-live="polite" aria-atomic="true"></div>
28
28
  <%= render "layouts/layered_ui/header" %>
29
29
  <%= render "layouts/layered_ui/navigation" %>
30
- <div class="l-ui-backdrop--navigation" data-l-ui--mobile-navigation-target="backdrop" data-action="click->l-ui--mobile-navigation#close"></div>
30
+ <div class="l-ui-backdrop--navigation" data-l-ui--navigation-target="backdrop" data-action="click->l-ui--navigation#close"></div>
31
31
  <% if yield(:l_ui_panel_body).present? %>
32
32
  <%= render "layouts/layered_ui/panel" %>
33
33
  <% end %>
data/config/importmap.rb CHANGED
@@ -7,7 +7,7 @@ pin "layered_ui/utilities/storage", to: "layered_ui/utilities/storage.js"
7
7
 
8
8
  # Controllers
9
9
  pin "layered_ui/controllers/l_ui/modal_controller", to: "layered_ui/controllers/l_ui/modal_controller.js"
10
- pin "layered_ui/controllers/l_ui/mobile_navigation_controller", to: "layered_ui/controllers/l_ui/mobile_navigation_controller.js"
10
+ pin "layered_ui/controllers/l_ui/navigation_controller", to: "layered_ui/controllers/l_ui/navigation_controller.js"
11
11
  pin "layered_ui/controllers/l_ui/panel_controller", to: "layered_ui/controllers/l_ui/panel_controller.js"
12
12
  pin "layered_ui/controllers/l_ui/panel_resize_controller", to: "layered_ui/controllers/l_ui/panel_resize_controller.js"
13
13
  pin "layered_ui/controllers/l_ui/panel_button_controller", to: "layered_ui/controllers/l_ui/panel_button_controller.js"
@@ -1,5 +1,5 @@
1
1
  module Layered
2
2
  module Ui
3
- VERSION = "0.1.3"
3
+ VERSION = "0.1.4"
4
4
  end
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: layered-ui-rails
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.3
4
+ version: 0.1.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - layered.ai
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-03-14 00:00:00.000000000 Z
11
+ date: 2026-03-25 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -187,8 +187,8 @@ files:
187
187
  - app/helpers/layered/ui/authentication_helper.rb
188
188
  - app/helpers/layered/ui/navigation_helper.rb
189
189
  - app/helpers/layered/ui/pagination_helper.rb
190
- - app/javascript/layered_ui/controllers/l_ui/mobile_navigation_controller.js
191
190
  - app/javascript/layered_ui/controllers/l_ui/modal_controller.js
191
+ - app/javascript/layered_ui/controllers/l_ui/navigation_controller.js
192
192
  - app/javascript/layered_ui/controllers/l_ui/panel_button_controller.js
193
193
  - app/javascript/layered_ui/controllers/l_ui/panel_controller.js
194
194
  - app/javascript/layered_ui/controllers/l_ui/panel_resize_controller.js
@@ -269,5 +269,5 @@ rubygems_version: 3.5.11
269
269
  signing_key:
270
270
  specification_version: 4
271
271
  summary: Open source, minimalist, responsive, accessible UI system with light and
272
- dark theme support - and a touch of glass.
272
+ dark theme support.
273
273
  test_files: []