layered-ui-rails 0.8.0 → 0.10.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d5abac205810e228d971ffa9c13f2c069c56be3f143c0f25612f02c2d8b6e598
4
- data.tar.gz: 3dbb48d42029cce15cb92dcbd6cc014046032cb7449f145e63854b4898936d14
3
+ metadata.gz: b9fe544e3cfcfc260bba58a205ada774ffcb1154b48b86fb406d25c189da5b77
4
+ data.tar.gz: 8d66cf1556700d9c26867ab59aa2d989bfd17f924ea1b7ec7be9cc3feb4626c5
5
5
  SHA512:
6
- metadata.gz: e39b4bbc08bb0f198320dccf181de35003e0ab2927eabe3666e0f39743e652a3e9ca45dc295d40b3ce22e1a85381392d86bd202dda9d905f1b500f2c837fdb6d
7
- data.tar.gz: 8efb4d5a510c8ef52541b0d9f1286e13dce5b8d64c90d7f984e4d4eccfc7153f91eafa83c5beadf54911b2a8a405e5b8c54383ea3884dc2ed6c0169c70f283ca
6
+ metadata.gz: 5a2aa85d87740cdf751526101c052155edf16f04136fbb164a70d30a1535bd1b68f82ccec6b333f7c001d8a8a7174868cd5f594d1613d0dd470d5f876bf28d2f
7
+ data.tar.gz: 33f34e0b13bd3ba0494722ffe41142cbfb3c94cd0da47a6a79bc423a9926928e332586def1e1d669b5c806f6f4fb828da14778197f0349d55e79c626071ec9c2
@@ -103,9 +103,11 @@ Quick reference:
103
103
 
104
104
  | Helper | Purpose |
105
105
  |---|---|
106
- | `l_ui_navigation_item(label, path, active: nil, &block)` | Sidebar nav link with optional nesting |
106
+ | `l_ui_navigation_item(label, path, ...)` | Sidebar nav link (supports `icon:`, `match: :starts_with`, `expandable:`) |
107
+ | `l_ui_navigation_section(heading = nil, ...)` | Group nav items; supports `collapsible:`, `storage_key:`, `separated:` |
107
108
  | `l_ui_breadcrumbs(&block)` | Breadcrumb nav wrapper |
108
109
  | `l_ui_breadcrumb_item(label, path = nil)` | Individual breadcrumb |
110
+ | `l_ui_title_bar(title:, breadcrumbs: [], actions: nil, &block)` | Responsive page title bar with breadcrumbs and actions |
109
111
  | `l_ui_pagy(pagy)` | Styled pagination (requires pagy gem) |
110
112
  | `l_ui_search_form(query, url:, fields:, ...)` | Search form (requires ransack gem) |
111
113
  | `l_ui_sort_link(query, attribute, label = nil, ...)` | Sortable table header (requires ransack gem) |
@@ -141,6 +143,7 @@ All controllers use the `l-ui--` namespace and are auto-registered via importmap
141
143
  |---|---|---|
142
144
  | Theme | `l-ui--theme` | Dark/light mode toggle with localStorage |
143
145
  | Navigation | `l-ui--navigation` | Responsive sidebar with backdrop |
146
+ | Navigation section | `l-ui--navigation-section` | Collapsible nav section with localStorage persistence |
144
147
  | Panel | `l-ui--panel` | Resizable side panel (Cmd/Ctrl+I toggle) |
145
148
  | Panel button | `l-ui--panel-button` | Draggable floating action button |
146
149
  | Panel resize | `l-ui--panel-resize` | Panel width drag handle |
@@ -28,9 +28,19 @@ Responsive sidebar navigation with backdrop overlay on mobile.
28
28
  **Targets:** `navigation`, `backdrop`, `toggleButton`, `openIcon`, `closeIcon`
29
29
  **Actions:** `toggle`, `close`
30
30
  **Keyboard:** Escape to close
31
+ **Behaviour:** Locks body scroll while the mobile overlay is open
31
32
 
32
33
  The layout wires this up automatically. Navigation items are populated via `content_for :l_ui_navigation_items`.
33
34
 
35
+ ## Navigation section (`l-ui--navigation-section`)
36
+
37
+ Collapsible navigation section with optional localStorage persistence. Wired up automatically by `l_ui_navigation_section` when `collapsible: true`.
38
+
39
+ **Targets:** `toggle`, `panel`
40
+ **Values:** `storageKey` (String, localStorage key), `forceOpen` (Boolean)
41
+ **Actions:** `toggle`
42
+ **Behaviour:** The server renders the section's default open/closed state (and forces it open when it contains the active item). On `connect`, if `forceOpen` is set the stored preference is ignored; otherwise the controller restores the user's stored preference. Clicking the toggle always works (so a force-opened section can still be collapsed) and writes the new state to localStorage.
43
+
34
44
  ## Modal (`l-ui--modal`)
35
45
 
36
46
  Native `<dialog>` wrapper with focus trap, scroll lock, and focus restoration.
@@ -170,6 +170,15 @@ WCAG 2.2 AA table pattern:
170
170
  .l-ui-breadcrumbs__link Breadcrumb link
171
171
  ```
172
172
 
173
+ ## Title bar
174
+
175
+ ```
176
+ .l-ui-title-bar Title bar wrapper used with .l-ui-container--spread
177
+ .l-ui-title-bar__content Breadcrumbs and title column
178
+ .l-ui-title-bar__title Page title
179
+ .l-ui-title-bar__actions Action area
180
+ ```
181
+
173
182
  ## Pagination
174
183
 
175
184
  ```
@@ -190,8 +199,18 @@ WCAG 2.2 AA table pattern:
190
199
  .l-ui-navigation Nav flexbox
191
200
  .l-ui-navigation__links Nav links list
192
201
  .l-ui-navigation__item Nav item
193
- .l-ui-navigation__item--active Active nav item (with arrow)
194
- .l-ui-navigation__secondary Nested nav list
202
+ .l-ui-navigation__item--active Active nav item (highlighted bg)
203
+ .l-ui-navigation__item-icon Icon image inside a nav item
204
+ .l-ui-navigation__item-icon-slot Wrapper for caller-supplied icon HTML (e.g. icon fonts)
205
+ .l-ui-navigation__item-label Label span inside a nav item
206
+ .l-ui-navigation__section Section group (li)
207
+ .l-ui-navigation__section--has-heading Section that has a visible heading
208
+ .l-ui-navigation__section--separated Section with top border separator
209
+ .l-ui-navigation__section--collapsible Section with toggle button heading
210
+ .l-ui-navigation__section-heading Plain section label (small, uppercase, muted)
211
+ .l-ui-navigation__section-toggle Collapsible section heading (item-row sized button)
212
+ .l-ui-navigation__section-chevron Toggle chevron (rotates when closed)
213
+ .l-ui-navigation__section-items Section body (ul, hidden when closed)
195
214
  .l-ui-navigation__user User info section
196
215
  .l-ui-navigation__user-name User name text
197
216
  .l-ui-navigation__user-email User email text
@@ -275,6 +294,7 @@ WCAG 2.2 AA table pattern:
275
294
  .l-ui-sr-only Visually hidden, screen reader only
276
295
  .l-ui-skip-link Accessibility skip link
277
296
  .l-ui-list Styled list
297
+ .l-ui-hr Horizontal rule with theme border and vertical spacing
278
298
  .l-ui-container--grid 1-col mobile, 2-col desktop grid
279
299
  .l-ui-container--spread Flex row with space-between
280
300
  .l-ui-container--pagy Pagination wrapper
@@ -5,19 +5,39 @@ All helpers are prefixed `l_ui_` and are available in all views automatically.
5
5
  ## Navigation
6
6
 
7
7
  ```ruby
8
- l_ui_navigation_item(label, path, active: nil, &block)
8
+ l_ui_navigation_item(label, path, active: nil, match: :exact, icon: nil, icon_path: nil, icon_html: nil)
9
+ l_ui_navigation_section(heading = nil, icon: nil, icon_path: nil, icon_html: nil, collapsible: false, expanded: true, storage_key: nil, separated: false, &block)
9
10
  ```
10
11
 
12
+ `l_ui_navigation_item`:
11
13
  - `label` (String) - link text
12
14
  - `path` (String) - URL
13
- - `active` (Boolean, optional) - force active state; defaults to `current_page?` check
14
- - `&block` - optional nested navigation items
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
+ - `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`
18
+ - `icon_path` (String, optional) - explicit asset path; takes precedence over `icon:`
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
+
21
+ `l_ui_navigation_section`:
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:`
24
+ - `icon_path` (String, optional) - explicit asset path next to the heading; takes precedence over `icon:`
25
+ - `icon_html` (String, optional) - pre-rendered icon markup; takes precedence over `icon:` and `icon_path:`
26
+ - `collapsible` (Boolean) - when `true` and a heading is given, renders a toggle button with a chevron and wires `aria-controls` to the panel
27
+ - `expanded` (Boolean) - default open state for collapsible sections; a section that contains an active descendant always opens
28
+ - `storage_key` (String, optional) - localStorage key to persist the open/closed preference
29
+ - `separated` (Boolean) - draw a horizontal rule above the section, useful for footer-style groups
30
+ - `&block` - section body, typically `l_ui_navigation_item`s
15
31
 
16
32
  ```erb
17
33
  <% content_for :l_ui_navigation_items do %>
18
- <%= l_ui_navigation_item("Home", root_path) %>
19
- <%= l_ui_navigation_item("Products", products_path, active: true) do %>
20
- <%= l_ui_navigation_item("Electronics", electronics_path) %>
34
+ <%= l_ui_navigation_section do %>
35
+ <%= l_ui_navigation_item("Home", root_path, icon: "home") %>
36
+ <% end %>
37
+
38
+ <%= l_ui_navigation_section("Products", collapsible: true, storage_key: "products") do %>
39
+ <%= l_ui_navigation_item("Overview", products_path) %>
40
+ <%= l_ui_navigation_item("Electronics", electronics_path, match: :starts_with) %>
21
41
  <%= l_ui_navigation_item("Clothing", clothing_path) %>
22
42
  <% end %>
23
43
  <% end %>
@@ -41,6 +61,29 @@ l_ui_breadcrumb_item(label, path = nil)
41
61
  <% end %>
42
62
  ```
43
63
 
64
+ ## Title bar
65
+
66
+ ```ruby
67
+ l_ui_title_bar(title:, breadcrumbs: [], actions: nil, &block)
68
+ ```
69
+
70
+ - `title` (String) - page title rendered as the `<h1>`
71
+ - `breadcrumbs` (Array, optional) - breadcrumb items as `[label, path]` arrays or `{ label:, path: }` hashes
72
+ - `actions` (String|Array, optional) - HTML-safe action content; omit when using a block
73
+ - `&block` - optional action markup, usually buttons or links
74
+
75
+ ```erb
76
+ <%= l_ui_title_bar(
77
+ title: "Users",
78
+ breadcrumbs: [
79
+ ["Home", root_path],
80
+ ["Admin", admin_path]
81
+ ]
82
+ ) do %>
83
+ <%= link_to "New user", new_user_path, class: "l-ui-button--primary" %>
84
+ <% end %>
85
+ ```
86
+
44
87
  ## Pagination (requires pagy gem)
45
88
 
46
89
  ```ruby
@@ -124,7 +167,8 @@ Returns a `<th>` element with sort link and ARIA sort attributes.
124
167
 
125
168
  ```ruby
126
169
  l_ui_table(records, columns:, caption: nil, actions: nil,
127
- actions_label: "Actions", query: nil, url: nil, turbo_frame: nil)
170
+ actions_label: "Actions", query: nil, url: nil,
171
+ turbo_frame: nil, row_id: nil)
128
172
  ```
129
173
 
130
174
  - `records` (ActiveRecord::Relation or Array) - the collection to render
@@ -135,6 +179,7 @@ l_ui_table(records, columns:, caption: nil, actions: nil,
135
179
  - `query` (Ransack::Search, optional) - enables sortable column headers
136
180
  - `url` (String, optional) - sort link URL (passed to `l_ui_sort_link`)
137
181
  - `turbo_frame` (String, optional) - turbo frame target for sort links
182
+ - `row_id` (Proc, optional) - receives (record), returns the `<tr>` id. Defaults to `dom_id(record)` for records that respond to `to_key` (ActiveRecord). Return `nil` to omit the id.
138
183
 
139
184
  Column options:
140
185
  - `attribute` (Symbol) - used for label generation and sort links
data/CHANGELOG.md CHANGED
@@ -2,7 +2,32 @@
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
- ## [Unreleased]
5
+ ## [0.10.0] - 2026-05-03
6
+
7
+ ### Added
8
+
9
+ - Nested navigation: `l_ui_navigation_section` helper for collapsible sidebar sections, backed by an `l-ui--navigation-section` Stimulus controller and `l-ui-navigation__section`/`__toggle`/`__items` styles
10
+ - Optional `icon:` / `icon_path:` / `icon_html:` arguments on `l_ui_navigation_item` and `l_ui_navigation_section` to render an icon alongside the label; ships with a default `icon_home.svg` as an example
11
+ - `row_id:` option on `l_ui_table` to set a stable DOM id on each row (useful for Turbo Stream targeting)
12
+
13
+ ### Changed
14
+
15
+ - **Breaking:** Minimum Ruby version raised to 3.3.0 (Ruby 3.2 is EOL). CI matrix now tests against Ruby 3.3, 3.4, and 4.0
16
+
17
+ ## [0.9.0] - 2026-04-26
18
+
19
+ ### Added
20
+
21
+ - `l_ui_title_bar` helper for page title bars with optional actions
22
+ - `l-ui-hr` class for styled horizontal rules
23
+ - `color-scheme` CSS property set to match the active theme so native form controls (date pickers, scrollbars, etc.) render with matching light/dark styling
24
+
25
+ ### Changed
26
+
27
+ - Navigation and panel controllers share an extracted `scroll_lock` utility module
28
+ - Checkbox and radio inputs use a pointer cursor
29
+ - Scroll-to-bottom button is centred and has a faint shadow
30
+ - Form submit label shortened from "Save changes" to "Save"
6
31
 
7
32
  ## [0.8.0] - 2026-04-24
8
33
 
data/README.md CHANGED
@@ -31,31 +31,22 @@ An open source, Rails 8+ engine that provides WCAG 2.2 AA compliant design token
31
31
  </tr>
32
32
  </table>
33
33
 
34
- ## Getting started
35
-
36
- Add to your Gemfile and install:
37
-
38
- ```bash
39
- bundle add layered-ui-rails
40
- bin/rails generate layered:ui:install
41
- ```
42
-
43
- The install generator will:
44
- - Copy `layered_ui.css` to `app/assets/tailwind/`
45
- - Add `@import "./layered_ui";` to your `application.css`
46
- - Add `import "layered_ui"` to your `application.js`
47
-
48
- Then update your application layout to render the engine layout. Place any `content_for` blocks **above** the render call - the engine layout reads them when it renders, so they must be defined first:
34
+ ## Features
49
35
 
50
- ```erb
51
- <% content_for :l_ui_body_class, "l-ui-body--always-show-navigation" %>
36
+ - **Dark/light theme** - system preference detection with localStorage persistence and manual toggle
37
+ - **Responsive layout** - header, sidebar navigation, main content area, and optional resizable panel
38
+ - **WCAG 2.2 AA compliant** - skip links, focus indicators, ARIA attributes, and 4.5:1 contrast ratios
39
+ - **Components** - buttons, forms, surfaces, tables, tabs, notices, badges, conversations, modals, and pagination
40
+ - **Optional integrations** - Devise authentication and Pagy pagination with styled views
41
+ - **Customisable branding** - Override the default logos and icons and colors
42
+ - **Google Lighthouse** - `layered-ui-rails` scores a [perfect 100](https://github.com/layered-ai-public/layered-ui-rails/raw/refs/heads/main/test/dummy/app/assets/images/lighthouse.webp) across all four Google Lighthouse categories - performance, accessibility, best practices, and SEO
52
43
 
53
- <% content_for :l_ui_navigation_items do %>
54
- <%= l_ui_navigation_item "Home", root_path %>
55
- <% end %>
44
+ ## Requirements
56
45
 
57
- <%= render template: "layouts/layered_ui/application" %>
58
- ```
46
+ - Ruby on Rails >= 8.0
47
+ - Tailwind CSS Rails >= 4.0
48
+ - Importmap Rails >= 2.0
49
+ - Stimulus Rails >= 1.0
59
50
 
60
51
  ## Agent skill
61
52
 
@@ -75,22 +66,31 @@ bin/rails generate layered:ui:install_agent_skill
75
66
  curl -fsSL https://raw.githubusercontent.com/layered-ai-public/layered-ui-rails/main/install-skill.sh | sh
76
67
  ```
77
68
 
78
- ## Requirements
69
+ ## Installation
79
70
 
80
- - Ruby on Rails >= 8.0
81
- - Tailwind CSS Rails >= 4.0
82
- - Importmap Rails >= 2.0
83
- - Stimulus Rails >= 1.0
71
+ Add to your Gemfile and install:
84
72
 
85
- ## Features
73
+ ```bash
74
+ bundle add layered-ui-rails
75
+ bin/rails generate layered:ui:install
76
+ ```
86
77
 
87
- - **Dark/light theme** - system preference detection with localStorage persistence and manual toggle
88
- - **Responsive layout** - header, sidebar navigation, main content area, and optional resizable panel
89
- - **WCAG 2.2 AA compliant** - skip links, focus indicators, ARIA attributes, and 4.5:1 contrast ratios
90
- - **Components** - buttons, forms, surfaces, tables, tabs, notices, badges, conversations, modals, and pagination
91
- - **Optional integrations** - Devise authentication and Pagy pagination with styled views
92
- - **Customisable branding** - Override the default logos and icons and colors
93
- - **Google Lighthouse** - `layered-ui-rails` scores a [perfect 100](https://github.com/layered-ai-public/layered-ui-rails/raw/refs/heads/main/test/dummy/app/assets/images/lighthouse.webp) across all four Google Lighthouse categories - performance, accessibility, best practices, and SEO
78
+ The install generator will:
79
+ - Copy `layered_ui.css` to `app/assets/tailwind/`
80
+ - Add `@import "./layered_ui";` to your `application.css`
81
+ - Add `import "layered_ui"` to your `application.js`
82
+
83
+ Then update your application layout to render the engine layout. Place any `content_for` blocks **above** the render call - the engine layout reads them when it renders, so they must be defined first:
84
+
85
+ ```erb
86
+ <% content_for :l_ui_body_class, "l-ui-body--always-show-navigation" %>
87
+
88
+ <% content_for :l_ui_navigation_items do %>
89
+ <%= l_ui_navigation_item "Home", root_path %>
90
+ <% end %>
91
+
92
+ <%= render template: "layouts/layered_ui/application" %>
93
+ ```
94
94
 
95
95
  ## Customising theme tokens
96
96
 
@@ -223,4 +223,4 @@ Copyright 2026 LAYERED AI LIMITED (UK company number: 17056830). See [NOTICE](NO
223
223
 
224
224
  ## Trademarks
225
225
 
226
- 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.
226
+ 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.
@@ -0,0 +1,3 @@
1
+ <svg fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
2
+ <path stroke-linecap="round" stroke-linejoin="round" d="m2.25 12 8.954-8.955c.44-.439 1.152-.439 1.591 0L21.75 12M4.5 9.75v10.125c0 .621.504 1.125 1.125 1.125H9.75v-4.875c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125V21h4.125c.621 0 1.125-.504 1.125-1.125V9.75M8.25 21h8.25" />
3
+ </svg>
@@ -47,6 +47,7 @@
47
47
 
48
48
  @layer base {
49
49
  :root {
50
+ color-scheme: light;
50
51
  /* Tier 1 - Accent */
51
52
  --accent: oklch(0.2044 0 0);
52
53
  --accent-foreground: oklch(1 0 0);
@@ -76,6 +77,7 @@
76
77
  }
77
78
 
78
79
  .dark {
80
+ color-scheme: dark;
79
81
  /* Tier 1 - Accent */
80
82
  --accent: oklch(1 0 0);
81
83
  --accent-foreground: oklch(0.2044 0 0);
@@ -294,6 +296,13 @@
294
296
  space-y-1;
295
297
  }
296
298
 
299
+ /* Horizontal rule */
300
+
301
+ .l-ui-hr {
302
+ @apply my-4
303
+ border-0 border-t border-border;
304
+ }
305
+
297
306
  /* Markdown */
298
307
 
299
308
  .l-ui-markdown > *:first-child {
@@ -635,8 +644,8 @@
635
644
  }
636
645
 
637
646
  @utility navigation__item {
638
- @apply flex items-center
639
- w-full min-h-[44px]
647
+ @apply flex items-center gap-3
648
+ w-full min-h-[40px] px-3
640
649
  text-sm text-foreground-muted font-medium
641
650
  rounded-sm
642
651
  focus-ring
@@ -644,50 +653,96 @@
644
653
  }
645
654
 
646
655
  .l-ui-navigation__item {
647
- @apply navigation__item
656
+ @apply navigation__item;
648
657
  }
649
658
 
650
659
  .l-ui-navigation__item--active {
651
660
  @apply navigation__item
652
- font-bold
653
- text-accent;
661
+ bg-surface-highlighted
662
+ text-foreground;
663
+ }
654
664
 
655
- &::after {
656
- content: "▸";
657
- @apply ml-auto
658
- text-2xl font-bold;
659
- }
665
+ .l-ui-navigation__item-icon {
666
+ @apply w-4 h-4 shrink-0
667
+ dark:invert;
668
+ }
669
+
670
+ .l-ui-navigation__item-icon-slot {
671
+ @apply inline-flex items-center justify-center
672
+ w-4 h-4 shrink-0;
673
+ }
660
674
 
661
- &:has(~ .l-ui-navigation__secondary .l-ui-navigation__item--active) {
662
- @apply pr-3;
675
+ .l-ui-navigation__item-label {
676
+ @apply truncate;
677
+ }
678
+
679
+ .l-ui-navigation__section {
680
+ @apply flex flex-col;
681
+
682
+ & + .l-ui-navigation__section {
683
+ @apply mt-2;
684
+ }
663
685
 
664
- &::after {
665
- content: none;
666
- }
686
+ & + .l-ui-navigation__section--separated {
687
+ @apply mt-3 pt-3
688
+ border-t border-border;
667
689
  }
668
690
  }
669
691
 
670
- .l-ui-navigation__secondary {
692
+ .l-ui-navigation__section-heading {
693
+ @apply flex items-center gap-2
694
+ w-full px-3 pt-4 pb-1
695
+ text-xs text-foreground-muted font-medium uppercase tracking-wider;
696
+ }
697
+
698
+ .l-ui-navigation__section-toggle {
699
+ @apply flex items-center gap-3
700
+ w-full min-h-[40px] px-3
701
+ text-sm text-foreground-muted font-medium
702
+ rounded-sm
703
+ focus-ring
704
+ cursor-pointer
705
+ transition-colors;
706
+ }
707
+
708
+ .l-ui-navigation__section-chevron {
709
+ @apply ml-auto inline-block
710
+ w-3 h-3
711
+ transition-transform duration-150;
712
+
713
+ background-color: currentColor;
714
+ -webkit-mask-image: url('layered_ui/icon_chevron_down.svg');
715
+ mask-image: url('layered_ui/icon_chevron_down.svg');
716
+ -webkit-mask-repeat: no-repeat;
717
+ mask-repeat: no-repeat;
718
+ -webkit-mask-position: center;
719
+ mask-position: center;
720
+ -webkit-mask-size: contain;
721
+ mask-size: contain;
722
+ }
723
+
724
+ .l-ui-navigation__section-toggle[aria-expanded="false"] .l-ui-navigation__section-chevron {
725
+ @apply -rotate-90;
726
+ }
727
+
728
+ .l-ui-navigation__section-items {
671
729
  @apply flex flex-col
672
- pl-4
673
- border-l-4 border-border;
674
-
675
- .l-ui-navigation__item--active {
676
- @apply bg-transparent
677
- font-bold
678
- text-accent;
679
-
680
- &::after {
681
- content: "▸";
682
- @apply ml-auto
683
- text-2xl font-bold;
684
- }
685
- }
730
+ gap-2;
731
+ }
732
+
733
+ .l-ui-navigation__section-items[hidden] {
734
+ display: none;
735
+ }
736
+
737
+ .l-ui-navigation__section--has-heading .l-ui-navigation__section-items {
738
+ @apply mt-2 ml-5 pl-3
739
+ border-l border-border;
686
740
  }
687
741
 
688
742
  .l-ui-navigation__links {
689
743
  @apply flex flex-col flex-1
690
- px-4 py-2 min-h-0
744
+ gap-0.5
745
+ px-3 py-3 min-h-0
691
746
  overflow-y-auto;
692
747
  }
693
748
 
@@ -740,6 +795,26 @@
740
795
  focus-ring rounded-sm;
741
796
  }
742
797
 
798
+ /* Title bar */
799
+
800
+ .l-ui-title-bar {
801
+ @apply w-full;
802
+ }
803
+
804
+ .l-ui-title-bar__content {
805
+ @apply min-w-0;
806
+ }
807
+
808
+ .l-ui-title-bar__title {
809
+ @apply mt-0;
810
+ }
811
+
812
+ .l-ui-title-bar__actions {
813
+ @apply flex flex-wrap items-center justify-end
814
+ gap-2
815
+ shrink-0;
816
+ }
817
+
743
818
  /* Buttons */
744
819
 
745
820
  @utility button {
@@ -1017,7 +1092,8 @@ pre.l-ui-surface {
1017
1092
  @apply w-6 h-6
1018
1093
  accent-foreground
1019
1094
  focus-ring
1020
- rounded-sm;
1095
+ rounded-sm
1096
+ cursor-pointer;
1021
1097
  }
1022
1098
 
1023
1099
  /* Search */
@@ -1124,11 +1200,12 @@ pre.l-ui-surface {
1124
1200
  @apply w-6 h-6
1125
1201
  mr-2
1126
1202
  accent-foreground
1127
- focus-ring;
1203
+ focus-ring
1204
+ cursor-pointer;
1128
1205
  }
1129
1206
 
1130
1207
  .l-ui-radio__label {
1131
- @apply text-sm;
1208
+ @apply text-sm cursor-pointer;
1132
1209
  }
1133
1210
 
1134
1211
  /* Tabs */
@@ -1659,10 +1736,11 @@ pre.l-ui-surface {
1659
1736
  .l-ui-scroll-to-bottom {
1660
1737
  @apply
1661
1738
  sticky bottom-2 flex items-center justify-center
1662
- ml-auto mr-0 -mt-9 h-9 w-9
1739
+ mx-auto -mt-9 h-9 w-9
1663
1740
  rounded-full
1664
1741
  cursor-pointer
1665
1742
  bg-button-primary-bg text-button-primary-icon
1743
+ shadow-sm
1666
1744
  focus-ring
1667
1745
  opacity-0 pointer-events-none
1668
1746
  transition-opacity duration-200;
@@ -1,24 +1,127 @@
1
+ require "securerandom"
2
+
1
3
  module Layered
2
4
  module Ui
3
5
  module NavigationHelper
4
- def l_ui_navigation_item(label, path, active: nil, &block)
5
- if block_given?
6
- children = capture(&block)
7
- is_active = active.nil? ? (current_page?(path) || children.include?("l-ui-navigation__item--active")) : active
8
- css_class = is_active ? "l-ui-navigation__item--active" : "l-ui-navigation__item"
9
- options = { class: css_class }
10
- options["aria-current"] = "page" if is_active && current_page?(path)
11
- content_tag(:li) do
12
- link_to(label, path, options) +
13
- content_tag(:ul, children, class: "l-ui-navigation__secondary", role: "list")
6
+ def l_ui_navigation_item(label, path, active: nil, match: :exact, icon: nil, icon_path: nil, icon_html: nil)
7
+ is_active = active.nil? ? l_ui_navigation_item_self_active?(path, match) : active
8
+
9
+ css_class = is_active ? "l-ui-navigation__item--active" : "l-ui-navigation__item"
10
+ options = { class: css_class }
11
+ options["aria-current"] = "page" if current_page?(path)
12
+
13
+ l_ui_navigation_register_active if is_active
14
+
15
+ link_content = l_ui_navigation_item_inner(label, icon, icon_path, icon_html)
16
+
17
+ content_tag(:li) { link_to(link_content, path, options) }
18
+ end
19
+
20
+ def l_ui_navigation_section(heading = nil, icon: nil, icon_path: nil, icon_html: nil, collapsible: false, expanded: true, storage_key: nil, separated: false, &block)
21
+ stack = (Thread.current[:l_ui_navigation_section_stack] ||= [])
22
+ frame = { active: false }
23
+ stack.push(frame)
24
+ begin
25
+ children = block_given? ? capture(&block) : "".html_safe
26
+ ensure
27
+ stack.pop
28
+ end
29
+
30
+ has_active_descendant = frame[:active]
31
+
32
+ if (parent = stack.last) && has_active_descendant
33
+ parent[:active] = true
34
+ end
35
+
36
+ is_open = has_active_descendant || expanded
37
+ is_collapsible = collapsible && heading.present?
38
+ panel_id = is_collapsible ? "l-ui-navigation-section-#{SecureRandom.hex(4)}" : nil
39
+
40
+ section_classes = ["l-ui-navigation__section"]
41
+ section_classes << "l-ui-navigation__section--has-heading" if heading.present?
42
+ section_classes << "l-ui-navigation__section--collapsible" if is_collapsible
43
+ section_classes << "l-ui-navigation__section--separated" if separated
44
+
45
+ data = {}
46
+ if is_collapsible
47
+ data["controller"] = "l-ui--navigation-section"
48
+ data["l-ui--navigation-section-storage-key-value"] = storage_key.to_s if storage_key.present?
49
+ data["l-ui--navigation-section-force-open-value"] = "true" if has_active_descendant
50
+ end
51
+
52
+ content_tag(:li, class: section_classes.join(" "), data: data) do
53
+ parts = []
54
+
55
+ if heading.present?
56
+ heading_inner = l_ui_navigation_item_inner(heading, icon, icon_path, icon_html)
57
+
58
+ if is_collapsible
59
+ chevron = content_tag(:span, "", class: "l-ui-navigation__section-chevron", "aria-hidden": "true")
60
+ parts << content_tag(:button,
61
+ heading_inner + chevron,
62
+ type: "button",
63
+ class: "l-ui-navigation__section-toggle",
64
+ "aria-expanded": is_open.to_s,
65
+ "aria-controls": panel_id,
66
+ data: { "l-ui--navigation-section-target": "toggle", action: "click->l-ui--navigation-section#toggle" }
67
+ )
68
+ else
69
+ parts << content_tag(:div, heading_inner, class: "l-ui-navigation__section-heading")
70
+ end
71
+ end
72
+
73
+ panel_attrs = { class: "l-ui-navigation__section-items", role: "list" }
74
+ if is_collapsible
75
+ panel_attrs[:id] = panel_id
76
+ panel_attrs[:data] = { "l-ui--navigation-section-target": "panel" }
77
+ panel_attrs[:hidden] = true unless is_open
14
78
  end
79
+ parts << content_tag(:ul, children, **panel_attrs)
80
+
81
+ safe_join(parts)
82
+ end
83
+ end
84
+
85
+ private
86
+
87
+ def l_ui_navigation_register_active
88
+ stack = Thread.current[:l_ui_navigation_section_stack]
89
+ return unless stack
90
+ stack.each { |frame| frame[:active] = true }
91
+ end
92
+
93
+ def l_ui_navigation_item_self_active?(path, match)
94
+ case match
95
+ when :starts_with
96
+ normalized = path.to_s.chomp("/")
97
+ return current_page?(path) if normalized.empty?
98
+ request.path == path || request.path.start_with?("#{normalized}/")
99
+ else
100
+ current_page?(path)
101
+ end
102
+ end
103
+
104
+ def l_ui_navigation_item_inner(label, icon, icon_path, icon_html)
105
+ parts = []
106
+
107
+ if icon_html.present?
108
+ parts << content_tag(:span, icon_html, class: "l-ui-navigation__item-icon-slot", aria: { hidden: true })
15
109
  else
16
- is_active = active.nil? ? current_page?(path) : active
17
- css_class = is_active ? "l-ui-navigation__item--active" : "l-ui-navigation__item"
18
- options = { class: css_class }
19
- options["aria-current"] = "page" if is_active
20
- content_tag(:li) { link_to label, path, options }
110
+ resolved_icon_path =
111
+ if icon_path.present?
112
+ icon_path
113
+ elsif icon.present?
114
+ "layered_ui/icon_#{icon}.svg"
115
+ end
116
+
117
+ if resolved_icon_path
118
+ parts << image_tag(resolved_icon_path, alt: "", class: "l-ui-navigation__item-icon", aria: { hidden: true })
119
+ end
21
120
  end
121
+
122
+ parts << content_tag(:span, label, class: "l-ui-navigation__item-label")
123
+
124
+ safe_join(parts)
22
125
  end
23
126
  end
24
127
  end
@@ -33,7 +33,12 @@ module Layered
33
33
  #
34
34
  # Pass +query:+ (a Ransack search object) and +turbo_frame:+ to enable
35
35
  # sortable column headers via +l_ui_sort_link+.
36
- def l_ui_table(records, columns:, caption: nil, actions: nil, actions_label: "Actions", query: nil, url: nil, turbo_frame: nil)
36
+ #
37
+ # Each <tr> is given +id: dom_id(record)+ when the record responds to
38
+ # +to_key+ (i.e. ActiveRecord), so individual rows can be targeted by
39
+ # Turbo Streams. Pass +row_id:+ as a proc to override (return +nil+ to
40
+ # omit the id).
41
+ def l_ui_table(records, columns:, caption: nil, actions: nil, actions_label: "Actions", query: nil, url: nil, turbo_frame: nil, row_id: nil)
37
42
  columns = normalise_table_columns(columns)
38
43
  col_count = columns.size + (actions ? 1 : 0)
39
44
 
@@ -58,7 +63,7 @@ module Layered
58
63
  end
59
64
  else
60
65
  safe_join(records.map do |record|
61
- tag.tr do
66
+ tag.tr(id: table_row_id(record, row_id)) do
62
67
  cells = columns.map do |col|
63
68
  table_cell(record, col)
64
69
  end
@@ -95,6 +100,11 @@ module Layered
95
100
  end
96
101
  end
97
102
 
103
+ def table_row_id(record, row_id)
104
+ return row_id.call(record) if row_id
105
+ ActionView::RecordIdentifier.dom_id(record) if record.respond_to?(:to_key) && record.to_key
106
+ end
107
+
98
108
  def table_cell(record, col)
99
109
  value = col[:render].call(record)
100
110
 
@@ -0,0 +1,52 @@
1
+ module Layered
2
+ module Ui
3
+ module TitleBarHelper
4
+ include Layered::Ui::BreadcrumbsHelper
5
+
6
+ def l_ui_title_bar(title:, breadcrumbs: [], actions: nil, &block)
7
+ action_content = block_given? ? capture(&block) : actions
8
+
9
+ content_tag(:header, class: "l-ui-title-bar l-ui-container--spread") do
10
+ safe_join([
11
+ content_tag(:div, class: "l-ui-title-bar__content") do
12
+ safe_join([
13
+ l_ui_title_bar_breadcrumbs(breadcrumbs),
14
+ content_tag(:h1, title, class: "l-ui-title-bar__title")
15
+ ].compact)
16
+ end,
17
+ l_ui_title_bar_actions(action_content)
18
+ ].compact)
19
+ end
20
+ end
21
+
22
+ private
23
+
24
+ def l_ui_title_bar_breadcrumbs(breadcrumbs)
25
+ return if breadcrumbs.blank?
26
+
27
+ l_ui_breadcrumbs do
28
+ safe_join(breadcrumbs.map { |breadcrumb| l_ui_title_bar_breadcrumb_item(breadcrumb) })
29
+ end
30
+ end
31
+
32
+ def l_ui_title_bar_breadcrumb_item(breadcrumb)
33
+ case breadcrumb
34
+ when Hash
35
+ l_ui_breadcrumb_item(breadcrumb.fetch(:label), breadcrumb[:path])
36
+ when Array
37
+ l_ui_breadcrumb_item(breadcrumb[0], breadcrumb[1])
38
+ else
39
+ l_ui_breadcrumb_item(breadcrumb)
40
+ end
41
+ end
42
+
43
+ def l_ui_title_bar_actions(action_content)
44
+ return if action_content.blank?
45
+
46
+ content = action_content.is_a?(Array) ? safe_join(action_content) : action_content
47
+
48
+ content_tag(:div, content, class: "l-ui-title-bar__actions")
49
+ end
50
+ end
51
+ end
52
+ end
@@ -1,6 +1,7 @@
1
1
  import { Controller } from "@hotwired/stimulus"
2
2
  import { announce, clearAnnounceTimeout } from "layered_ui/utilities/announce"
3
3
  import { isMobile } from "layered_ui/utilities/layout"
4
+ import { lockBodyScroll, unlockBodyScroll } from "layered_ui/utilities/scroll_lock"
4
5
 
5
6
  export default class extends Controller {
6
7
  static targets = ["navigation", "backdrop", "toggleButton", "openIcon", "closeIcon"]
@@ -8,6 +9,7 @@ export default class extends Controller {
8
9
  connect() {
9
10
  this.previousActiveElement = null
10
11
  this.isOpen = false
12
+ this.isScrollLocked = false
11
13
  this._resizeFrame = null
12
14
  this.boundHandleResize = () => {
13
15
  if (this._resizeFrame) return
@@ -54,6 +56,7 @@ export default class extends Controller {
54
56
  this.navigationTarget.classList.add("open")
55
57
  this.backdropTarget.classList.add("open")
56
58
  this.setNavigationInteractivity(true)
59
+ this.updateScrollLock()
57
60
 
58
61
  // Update ARIA attributes and swap icons
59
62
  if (this.hasToggleButtonTarget) {
@@ -89,6 +92,7 @@ export default class extends Controller {
89
92
  this.navigationTarget.classList.remove("open")
90
93
  this.backdropTarget.classList.remove("open")
91
94
  this.setNavigationInteractivity(false)
95
+ this.unlockScroll()
92
96
 
93
97
  // Update ARIA attributes and swap icons
94
98
  if (this.hasToggleButtonTarget) {
@@ -116,6 +120,7 @@ export default class extends Controller {
116
120
  clearAnnounceTimeout(this)
117
121
  cancelAnimationFrame(this._resizeFrame)
118
122
  window.removeEventListener("resize", this.boundHandleResize)
123
+ this.unlockScroll()
119
124
  this.previousActiveElement = null
120
125
  }
121
126
 
@@ -125,6 +130,7 @@ export default class extends Controller {
125
130
  // In overlay mode (default), always respect isOpen state regardless of viewport
126
131
  if (isMobile() || !this.alwaysShow) {
127
132
  this.setNavigationInteractivity(this.isOpen)
133
+ this.updateScrollLock()
128
134
  return
129
135
  }
130
136
 
@@ -133,6 +139,7 @@ export default class extends Controller {
133
139
  this.navigationTarget.classList.remove("open")
134
140
  this.backdropTarget.classList.remove("open")
135
141
  this.setNavigationInteractivity(true)
142
+ this.unlockScroll()
136
143
 
137
144
  if (this.hasToggleButtonTarget) {
138
145
  this.toggleButtonTarget.setAttribute("aria-expanded", "false")
@@ -157,6 +164,28 @@ export default class extends Controller {
157
164
  this.navigationTarget.removeAttribute("aria-hidden")
158
165
  }
159
166
 
167
+ updateScrollLock() {
168
+ if (this.isOpen && isMobile()) {
169
+ this.lockScroll()
170
+ } else {
171
+ this.unlockScroll()
172
+ }
173
+ }
174
+
175
+ lockScroll() {
176
+ if (this.isScrollLocked) return
177
+
178
+ lockBodyScroll()
179
+ this.isScrollLocked = true
180
+ }
181
+
182
+ unlockScroll() {
183
+ if (!this.isScrollLocked) return
184
+
185
+ unlockBodyScroll()
186
+ this.isScrollLocked = false
187
+ }
188
+
160
189
  get alwaysShow() {
161
190
  return this.element.classList.contains("l-ui-body--always-show-navigation")
162
191
  }
@@ -0,0 +1,48 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ const STORAGE_PREFIX = "l-ui-navigation-section:"
4
+
5
+ export default class extends Controller {
6
+ static targets = ["toggle", "panel"]
7
+ static values = {
8
+ storageKey: String,
9
+ forceOpen: Boolean
10
+ }
11
+
12
+ connect() {
13
+ if (this.forceOpenValue) return
14
+ if (!this.hasStorageKeyValue || !this.storageKeyValue) return
15
+ let stored
16
+ try {
17
+ stored = window.localStorage.getItem(STORAGE_PREFIX + this.storageKeyValue)
18
+ } catch (_e) {
19
+ return
20
+ }
21
+ if (stored === null) return
22
+
23
+ const isOpen = stored === "true"
24
+ this.toggleTarget.setAttribute("aria-expanded", isOpen ? "true" : "false")
25
+ if (isOpen) {
26
+ this.panelTarget.removeAttribute("hidden")
27
+ } else {
28
+ this.panelTarget.setAttribute("hidden", "")
29
+ }
30
+ }
31
+
32
+ toggle() {
33
+ const isOpen = this.toggleTarget.getAttribute("aria-expanded") !== "true"
34
+ this.toggleTarget.setAttribute("aria-expanded", isOpen ? "true" : "false")
35
+ if (isOpen) {
36
+ this.panelTarget.removeAttribute("hidden")
37
+ } else {
38
+ this.panelTarget.setAttribute("hidden", "")
39
+ }
40
+
41
+ if (!this.hasStorageKeyValue || !this.storageKeyValue) return
42
+ try {
43
+ window.localStorage.setItem(STORAGE_PREFIX + this.storageKeyValue, isOpen ? "true" : "false")
44
+ } catch (_e) {
45
+ // ignore
46
+ }
47
+ }
48
+ }
@@ -2,6 +2,7 @@ import { Controller } from "@hotwired/stimulus"
2
2
  import { announce, clearAnnounceTimeout } from "layered_ui/utilities/announce"
3
3
  import { storageGet, storageSet } from "layered_ui/utilities/storage"
4
4
  import { isMobile } from "layered_ui/utilities/layout"
5
+ import { lockBodyScroll, unlockBodyScroll } from "layered_ui/utilities/scroll_lock"
5
6
 
6
7
  export default class extends Controller {
7
8
  static targets = ["container", "hideButton", "actionButton"]
@@ -9,6 +10,7 @@ export default class extends Controller {
9
10
  connect() {
10
11
  this.previousActiveElement = null
11
12
  this.isOpen = false
13
+ this.isScrollLocked = false
12
14
  this.boundKeyboardShortcut = this.handleKeyboardShortcut.bind(this)
13
15
  this.boundCloseOnNavigate = this.closeOnMobileNavigate.bind(this)
14
16
  const page = document.querySelector(".l-ui-page")
@@ -34,6 +36,7 @@ export default class extends Controller {
34
36
  clearAnnounceTimeout(this)
35
37
  document.removeEventListener('keydown', this.boundKeyboardShortcut)
36
38
  document.removeEventListener('turbo:visit', this.boundCloseOnNavigate)
39
+ this.unlockScroll()
37
40
  this.previousActiveElement = null
38
41
  }
39
42
 
@@ -90,9 +93,7 @@ export default class extends Controller {
90
93
  if (isMobile()) {
91
94
  const main = document.querySelector("main")
92
95
  if (main) main.setAttribute("inert", "")
93
- this.savedScrollY = window.scrollY
94
- document.body.style.top = `-${this.savedScrollY}px`
95
- document.body.classList.add("l-ui-scroll-lock")
96
+ this.lockScroll()
96
97
  }
97
98
 
98
99
  storageSet("panelOpen", "true")
@@ -124,11 +125,7 @@ export default class extends Controller {
124
125
 
125
126
  const main = document.querySelector("main")
126
127
  if (main) main.removeAttribute("inert")
127
- document.body.classList.remove("l-ui-scroll-lock")
128
- document.body.style.top = ""
129
- if (this.savedScrollY !== undefined) {
130
- window.scrollTo(0, this.savedScrollY)
131
- }
128
+ this.unlockScroll()
132
129
 
133
130
  storageSet("panelOpen", "false")
134
131
  this.updatePageMargin()
@@ -161,4 +158,18 @@ export default class extends Controller {
161
158
  page.style.marginRight = ""
162
159
  }
163
160
  }
161
+
162
+ lockScroll() {
163
+ if (this.isScrollLocked) return
164
+
165
+ lockBodyScroll()
166
+ this.isScrollLocked = true
167
+ }
168
+
169
+ unlockScroll() {
170
+ if (!this.isScrollLocked) return
171
+
172
+ unlockBodyScroll()
173
+ this.isScrollLocked = false
174
+ }
164
175
  }
@@ -1,6 +1,7 @@
1
1
  import { application } from "controllers/application"
2
2
  import ThemeController from "layered_ui/controllers/l_ui/theme_controller"
3
3
  import NavigationController from "layered_ui/controllers/l_ui/navigation_controller"
4
+ import NavigationSectionController from "layered_ui/controllers/l_ui/navigation_section_controller"
4
5
  import PanelController from "layered_ui/controllers/l_ui/panel_controller"
5
6
  import PanelResizeController from "layered_ui/controllers/l_ui/panel_resize_controller"
6
7
  import PanelButtonController from "layered_ui/controllers/l_ui/panel_button_controller"
@@ -11,6 +12,7 @@ import TabsController from "layered_ui/controllers/l_ui/tabs_controller"
11
12
  application.register("l-ui--search-form", SearchFormController)
12
13
  application.register("l-ui--theme", ThemeController)
13
14
  application.register("l-ui--navigation", NavigationController)
15
+ application.register("l-ui--navigation-section", NavigationSectionController)
14
16
  application.register("l-ui--panel", PanelController)
15
17
  application.register("l-ui--panel-resize", PanelResizeController)
16
18
  application.register("l-ui--panel-button", PanelButtonController)
@@ -0,0 +1,29 @@
1
+ let lockCount = 0
2
+ let savedScrollY = null
3
+
4
+ export function lockBodyScroll() {
5
+ if (lockCount === 0) {
6
+ savedScrollY = window.scrollY
7
+ document.body.style.top = `-${savedScrollY}px`
8
+ document.body.classList.add("l-ui-scroll-lock")
9
+ }
10
+
11
+ lockCount++
12
+ }
13
+
14
+ export function unlockBodyScroll() {
15
+ if (lockCount === 0) return
16
+
17
+ lockCount--
18
+
19
+ if (lockCount === 0) {
20
+ document.body.classList.remove("l-ui-scroll-lock")
21
+ document.body.style.top = ""
22
+
23
+ if (savedScrollY !== null) {
24
+ window.scrollTo(0, savedScrollY)
25
+ }
26
+
27
+ savedScrollY = null
28
+ }
29
+ }
@@ -10,7 +10,7 @@
10
10
  <% end %>
11
11
 
12
12
  <div class="l-ui-form__actions">
13
- <%= f.submit(record.new_record? ? "Create" : "Save changes",
13
+ <%= f.submit(record.new_record? ? "Create" : "Save",
14
14
  class: "l-ui-button l-ui-button--primary") %>
15
15
  </div>
16
16
  <% end %>
data/config/importmap.rb CHANGED
@@ -3,11 +3,13 @@ pin "layered_ui", to: "layered_ui/index.js"
3
3
  # Utilities
4
4
  pin "layered_ui/utilities/announce", to: "layered_ui/utilities/announce.js"
5
5
  pin "layered_ui/utilities/layout", to: "layered_ui/utilities/layout.js"
6
+ pin "layered_ui/utilities/scroll_lock", to: "layered_ui/utilities/scroll_lock.js"
6
7
  pin "layered_ui/utilities/storage", to: "layered_ui/utilities/storage.js"
7
8
 
8
9
  # Controllers
9
10
  pin "layered_ui/controllers/l_ui/modal_controller", to: "layered_ui/controllers/l_ui/modal_controller.js"
10
11
  pin "layered_ui/controllers/l_ui/navigation_controller", to: "layered_ui/controllers/l_ui/navigation_controller.js"
12
+ pin "layered_ui/controllers/l_ui/navigation_section_controller", to: "layered_ui/controllers/l_ui/navigation_section_controller.js"
11
13
  pin "layered_ui/controllers/l_ui/panel_controller", to: "layered_ui/controllers/l_ui/panel_controller.js"
12
14
  pin "layered_ui/controllers/l_ui/panel_resize_controller", to: "layered_ui/controllers/l_ui/panel_resize_controller.js"
13
15
  pin "layered_ui/controllers/l_ui/panel_button_controller", to: "layered_ui/controllers/l_ui/panel_button_controller.js"
@@ -31,6 +31,7 @@ module Layered
31
31
  helper Layered::Ui::NavigationHelper
32
32
  helper Layered::Ui::PagyHelper
33
33
  helper Layered::Ui::TableHelper
34
+ helper Layered::Ui::TitleBarHelper
34
35
  helper Layered::Ui::FormHelper
35
36
  helper Layered::Ui::RansackHelper
36
37
  end
@@ -1,5 +1,5 @@
1
1
  module Layered
2
2
  module Ui
3
- VERSION = "0.8.0"
3
+ VERSION = "0.10.0"
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.8.0
4
+ version: 0.10.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - layered.ai
@@ -196,6 +196,7 @@ files:
196
196
  - app/assets/images/layered_ui/icon_github.svg
197
197
  - app/assets/images/layered_ui/icon_globe.svg
198
198
  - app/assets/images/layered_ui/icon_hamburger.svg
199
+ - app/assets/images/layered_ui/icon_home.svg
199
200
  - app/assets/images/layered_ui/icon_light.svg
200
201
  - app/assets/images/layered_ui/icon_linkedin.svg
201
202
  - app/assets/images/layered_ui/icon_mail.svg
@@ -215,8 +216,10 @@ files:
215
216
  - app/helpers/layered/ui/pagy_helper.rb
216
217
  - app/helpers/layered/ui/ransack_helper.rb
217
218
  - app/helpers/layered/ui/table_helper.rb
219
+ - app/helpers/layered/ui/title_bar_helper.rb
218
220
  - app/javascript/layered_ui/controllers/l_ui/modal_controller.js
219
221
  - app/javascript/layered_ui/controllers/l_ui/navigation_controller.js
222
+ - app/javascript/layered_ui/controllers/l_ui/navigation_section_controller.js
220
223
  - app/javascript/layered_ui/controllers/l_ui/panel_button_controller.js
221
224
  - app/javascript/layered_ui/controllers/l_ui/panel_controller.js
222
225
  - app/javascript/layered_ui/controllers/l_ui/panel_resize_controller.js
@@ -226,6 +229,7 @@ files:
226
229
  - app/javascript/layered_ui/index.js
227
230
  - app/javascript/layered_ui/utilities/announce.js
228
231
  - app/javascript/layered_ui/utilities/layout.js
232
+ - app/javascript/layered_ui/utilities/scroll_lock.js
229
233
  - app/javascript/layered_ui/utilities/storage.js
230
234
  - app/views/devise/confirmations/new.html.erb
231
235
  - app/views/devise/mailer/confirmation_instructions.html.erb
@@ -271,7 +275,7 @@ metadata:
271
275
  source_code_uri: https://github.com/layered-ai-public/layered-ui-rails
272
276
  bug_tracker_uri: https://github.com/layered-ai-public/layered-ui-rails/issues
273
277
  changelog_uri: https://github.com/layered-ai-public/layered-ui-rails/blob/main/CHANGELOG.md
274
- documentation_uri: https://www.layered.ai
278
+ documentation_uri: https://layered-ui-rails.layered.ai/
275
279
  discord_uri: https://discord.gg/aCGqz9Bx
276
280
  rubygems_mfa_required: 'true'
277
281
  post_install_message: |
@@ -293,7 +297,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
293
297
  requirements:
294
298
  - - ">="
295
299
  - !ruby/object:Gem::Version
296
- version: 3.2.0
300
+ version: 3.3.0
297
301
  required_rubygems_version: !ruby/object:Gem::Requirement
298
302
  requirements:
299
303
  - - ">="