layered-ui-rails 0.5.0 → 0.6.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: 0f1a66524b7aad7bb6cca544c4936735e286f2a1a2bccf620a47bd08c3c6b3ef
4
- data.tar.gz: 779bb09a82b63891245d917c25c40f82e2e1c505291c3967daf0f1db4a4cbb94
3
+ metadata.gz: 8b3fa30f64b14c50db43ea23c4fbdb699903a7f4c2f957a8b22cd85772868a7b
4
+ data.tar.gz: 215e0d57d2403591bf31dd9d31ea05c281e5f36f83ebe822a4ae3aa902eabf0b
5
5
  SHA512:
6
- metadata.gz: cbac626bf832dc7599b37a4234aac65e1f8cbd1282d1c2bda20e9df3c97319ed468adde98af047affa463895d0bdcf433bd6512af4ceef59c843eba21caf443d
7
- data.tar.gz: f62ab50c0825ee0b152fd48b1c508750cbe04bff880217aa0a7ffa4663d54db3ab285d08546fcdc52117cac2817c2e66196d3f8b96bfff4a6f584c0671e52508
6
+ metadata.gz: 890b8e734f1b573143bf40deda9d3d5c64f1eb358f075a0a5e6608475eaeeb6dd0470675e68b90ac72f55f28f7a688bb19410bc99f5f77e3c590c2025c90ab50
7
+ data.tar.gz: 73c86751d4a39d80879c254fa35bae780c74ae4c7a5e45fb9c41c5634a6b69ce60632750e035176439c038646415a98e0b6988f389e8368cf1354348363a4553
@@ -22,9 +22,16 @@ bin/rails generate layered:ui:install
22
22
 
23
23
  The generator copies `layered_ui.css` into `app/assets/tailwind/`, adds the CSS import to `application.css`, and adds the JS import to `application.js`.
24
24
 
25
- Then render the engine layout from your application layout:
25
+ Then render the engine layout from your application layout. Place all `content_for` blocks **above** the render call - the engine layout reads them when it renders, so they must be defined first:
26
26
 
27
27
  ```erb
28
+ <% content_for :l_ui_body_class, "l-ui-body--always-show-navigation" %>
29
+
30
+ <% content_for :l_ui_navigation_items do %>
31
+ <%= l_ui_navigation_item("Dashboard", dashboard_path) %>
32
+ <%= l_ui_navigation_item("Users", users_path) %>
33
+ <% end %>
34
+
28
35
  <%= render template: "layouts/layered_ui/application" %>
29
36
  ```
30
37
 
@@ -34,7 +41,7 @@ The engine layout provides a fixed header (63px), optional sidebar navigation (2
34
41
 
35
42
  ### Content blocks
36
43
 
37
- Populate layout regions with `content_for`:
44
+ Populate layout regions with `content_for` (always above the render call):
38
45
 
39
46
  ```erb
40
47
  <%# Navigation sidebar items %>
@@ -139,7 +146,7 @@ All controllers use the `l-ui--` namespace and are auto-registered via importmap
139
146
  | Panel resize | `l-ui--panel-resize` | Panel width drag handle |
140
147
  | Modal | `l-ui--modal` | Native `<dialog>` with focus trap |
141
148
  | Tabs | `l-ui--tabs` | Accessible tabbed interface |
142
- | Search form | `l-ui--search-form` | Multi-scope search with Turbo support |
149
+ | Search form | `l-ui--search-form` | Multi-scope search with Turbo support and pagination param preservation |
143
150
 
144
151
  ## Theming
145
152
 
@@ -157,7 +164,7 @@ Override CSS custom properties after the engine import. Values are space-separat
157
164
  }
158
165
  ```
159
166
 
160
- Key tokens: `--accent`, `--accent-foreground`, `--background`, `--foreground`, `--foreground-muted`, `--border`, `--border-control`, `--surface`, `--surface-active`, `--danger`, `--header-height`.
167
+ Key tokens: `--accent`, `--accent-foreground`, `--background`, `--foreground`, `--foreground-muted`, `--border`, `--border-control`, `--surface`, `--surface-highlighted`, `--danger`, `--header-height`.
161
168
 
162
169
  ## Asset overrides
163
170
 
@@ -167,7 +174,7 @@ Place files in `app/assets/images/layered_ui/` to replace engine defaults:
167
174
 
168
175
  ## Optional integrations
169
176
 
170
- - **Devise** - auto-detected. Provides styled auth views, header login/register buttons, sidebar user info and logout.
177
+ - **Devise** - auto-detected. Provides styled auth views, header login/register buttons, sidebar user info and logout. Setup: `bundle add devise`, run `devise:install` and `devise User` generators, add `devise_for :users` to routes. Configure `Layered::Ui.current_user_method` if not using `:current_user`. Helpers: `l_ui_devise_installed?`, `l_ui_user_signed_in?`.
171
178
  - **Pagy** - auto-detected. Use `l_ui_pagy(@pagy)` for styled pagination.
172
179
  - **Ransack** - auto-detected. Use `l_ui_search_form` and `l_ui_sort_link` for styled search and sortable tables.
173
180
 
@@ -139,7 +139,7 @@ Drag handle for resizing the panel width on desktop.
139
139
  Manages multi-scope search forms with parameter preservation and Turbo frame support.
140
140
 
141
141
  **Values:** `scope` (String, default `"q"`)
142
- **Actions:** `preserve`, `clear`
142
+ **Actions:** `preserve`, `clear`, `rewriteLink`
143
143
 
144
144
  ```html
145
145
  <form data-controller="l-ui--search-form"
@@ -153,4 +153,17 @@ Manages multi-scope search forms with parameter preservation and Turbo frame sup
153
153
  </form>
154
154
  ```
155
155
 
156
- When multiple search forms exist on one page (each with a different `scope` value), submitting one form automatically preserves the other forms' query parameters.
156
+ When multiple search forms exist on one page (each with a different `scope` value), submitting one form automatically preserves the other forms' query parameters. The `page` param and any scoped page param matching the scope (e.g. `users_page` for scope `users_q`) are reset on submit so pagination returns to page 1.
157
+
158
+ **`rewriteLink`** - merges current URL params into a clicked link's href. Useful for pagination links inside Turbo Frames where the server-rendered href may be missing params from other scopes. Attach to a parent element (e.g. the Turbo Frame):
159
+
160
+ ```html
161
+ &lt;%= turbo_frame_tag "users_collection", data: { turbo_action: "advance",
162
+ controller: "l-ui--search-form", l_ui__search_form_scope_value: "users_q",
163
+ action: "click->l-ui--search-form#rewriteLink" } do %&gt;
164
+ &lt;%= l_ui_search_form(@users_q, url: users_path, fields: [:name, :email],
165
+ clear: true, turbo_frame: "users_collection") %&gt;
166
+ &lt;%= l_ui_table(@users, ..., query: @users_q, turbo_frame: "users_collection") %&gt;
167
+ &lt;%= l_ui_pagy(@users_pagy) %&gt;
168
+ &lt;% end %&gt;
169
+ ```
@@ -16,14 +16,14 @@ All classes use the `l-ui-` prefix with BEM naming. Use these in host app views.
16
16
  Standalone variants (use one of these, not combined with each other):
17
17
 
18
18
  ```
19
- .l-ui-button Plain button with padding and focus ring
20
- .l-ui-button--primary Accent-coloured solid button
21
- .l-ui-button--danger Solid red button (for destructive actions)
22
- .l-ui-button--outline Bordered button
23
- .l-ui-button--outline-danger Red bordered button (for destructive actions)
24
- .l-ui-button--icon Icon-only button (fixed size, no text)
19
+ .l-ui-button Plain button with padding and focus ring
20
+ .l-ui-button--primary Accent-coloured solid button
21
+ .l-ui-button--danger Solid red button (for destructive actions)
22
+ .l-ui-button--outline Bordered button
23
+ .l-ui-button--outline-danger Red bordered button (for destructive actions)
24
+ .l-ui-button--icon Icon-only button (fixed size, no text)
25
25
  .l-ui-button--navigation-toggle Mobile navigation toggle
26
- .l-ui-button--panel-close Panel close button
26
+ .l-ui-button--panel-close Panel close button
27
27
  ```
28
28
 
29
29
  Modifiers (combine with a standalone variant above):
@@ -39,64 +39,64 @@ For destructive actions use `l-ui-button--danger` (solid) or `l-ui-button--outli
39
39
  ## Surfaces
40
40
 
41
41
  ```
42
- .l-ui-surface Rounded, padded container
43
- .l-ui-surface--active Darker background variant
44
- .l-ui-surface--sm Smaller padding
45
- .l-ui-surface--collapsible Wraps a <details> element
46
- .l-ui-surface--collapsible-active Open state
47
- .l-ui-surface__summary Collapsible toggle (on <summary>)
48
- .l-ui-surface__chevron Chevron indicator (rotates on open)
49
- .l-ui-surface__content Collapsible content area
42
+ .l-ui-surface Rounded, padded container
43
+ .l-ui-surface--highlighted Darker background variant
44
+ .l-ui-surface--sm Smaller padding
45
+ .l-ui-surface--collapsible Wraps a <details> element
46
+ .l-ui-surface--collapsible-highlighted Highlighted variant
47
+ .l-ui-surface__summary Collapsible toggle (on <summary>)
48
+ .l-ui-surface__chevron Chevron indicator (rotates on open)
49
+ .l-ui-surface__content Collapsible content area
50
50
  ```
51
51
 
52
52
  ## Forms
53
53
 
54
54
  ```
55
- .l-ui-form Form container
56
- .l-ui-form__group Vertical field group with spacing
55
+ .l-ui-form Form container
56
+ .l-ui-form__group Vertical field group with spacing
57
57
  .l-ui-form__group--large-gap Larger spacing variant
58
- .l-ui-form__field Input/textarea styling
59
- .l-ui-form__errors Error summary box
60
- .l-ui-form__errors-list Bulleted error list
61
- .l-ui-form__actions Right-aligned action button container (full-width buttons on mobile)
62
- .l-ui-form__field-error Individual field error message
63
- .l-ui-form__hint Field hint text
64
- .l-ui-form__required Required indicator (*)
58
+ .l-ui-form__field Input/textarea styling
59
+ .l-ui-form__errors Error summary box
60
+ .l-ui-form__errors-list Bulleted error list
61
+ .l-ui-form__actions Right-aligned action button container (full-width buttons on mobile)
62
+ .l-ui-form__field-error Individual field error message
63
+ .l-ui-form__hint Field hint text
64
+ .l-ui-form__required Required indicator (*)
65
65
 
66
- .l-ui-label Form label
67
- .l-ui-label--checkbox Checkbox label variant
66
+ .l-ui-label Form label
67
+ .l-ui-label--checkbox Checkbox label variant
68
68
 
69
- .l-ui-select Select dropdown
70
- .l-ui-select-wrapper Select wrapper (custom arrow)
69
+ .l-ui-select Select dropdown
70
+ .l-ui-select-wrapper Select wrapper (custom arrow)
71
71
 
72
- .l-ui-search__inline Inline search form layout
72
+ .l-ui-search__inline Inline search form layout
73
73
 
74
- .l-ui-container--checkbox Checkbox container
75
- .l-ui-radio__group Radio button group
76
- .l-ui-radio__item Radio item wrapper
77
- .l-ui-radio__input Radio input element
78
- .l-ui-radio__label Radio label
74
+ .l-ui-container--checkbox Checkbox container
75
+ .l-ui-radio__group Radio button group
76
+ .l-ui-radio__item Radio item wrapper
77
+ .l-ui-radio__input Radio input element
78
+ .l-ui-radio__label Radio label
79
79
 
80
- .l-ui-switch Toggle switch container
81
- .l-ui-switch__input Hidden checkbox input
82
- .l-ui-switch__track Visual track element
80
+ .l-ui-switch Toggle switch container
81
+ .l-ui-switch__input Hidden checkbox input
82
+ .l-ui-switch__track Visual track element
83
83
  ```
84
84
 
85
85
  ## Tables
86
86
 
87
87
  ```
88
- .l-ui-table Table element
89
- .l-ui-table__header <thead> row
90
- .l-ui-table__header-cell <th> cell
91
- .l-ui-table__header-cell--action Right-aligned action header
92
- .l-ui-table__sort-link Sortable header link
93
- .l-ui-table__sort-indicator Sort direction indicator (arrow)
94
- .l-ui-table__body <tbody>
95
- .l-ui-table__cell Regular <td> cell
96
- .l-ui-table__cell--primary Bold cell (typically first column, use <th scope="row">)
97
- .l-ui-table__cell--action Right-aligned action cell
98
- .l-ui-table__action--danger Danger action link
99
- .l-ui-container--table Overflow wrapper for responsive tables
88
+ .l-ui-table Table element
89
+ .l-ui-table__header <thead> row
90
+ .l-ui-table__header-cell <th> cell
91
+ .l-ui-table__header-cell--action Right-aligned action header
92
+ .l-ui-table__sort-link Sortable header link
93
+ .l-ui-table__sort-indicator Sort direction indicator (arrow)
94
+ .l-ui-table__body <tbody>
95
+ .l-ui-table__cell Regular <td> cell
96
+ .l-ui-table__cell--primary Bold cell (typically first column, use <th scope="row">)
97
+ .l-ui-table__cell--action Right-aligned action cell
98
+ .l-ui-table__action--danger Danger action link
99
+ .l-ui-container--table Overflow wrapper for responsive tables
100
100
  ```
101
101
 
102
102
  WCAG 2.2 AA table pattern:
@@ -128,126 +128,126 @@ WCAG 2.2 AA table pattern:
128
128
  ## Notices
129
129
 
130
130
  ```
131
- .l-ui-notice--success Green success message
132
- .l-ui-notice--warning Yellow warning message
133
- .l-ui-notice--error Red error message
131
+ .l-ui-notice--success Green success message
132
+ .l-ui-notice--warning Yellow warning message
133
+ .l-ui-notice--error Red error message
134
134
  ```
135
135
 
136
136
  ## Badges
137
137
 
138
138
  ```
139
- .l-ui-badge Base badge
140
- .l-ui-badge--rounded Pill shape
141
- .l-ui-badge--default Grey
142
- .l-ui-badge--success Green
143
- .l-ui-badge--warning Yellow
144
- .l-ui-badge--danger Red
139
+ .l-ui-badge Base badge
140
+ .l-ui-badge--rounded Pill shape
141
+ .l-ui-badge--default Grey
142
+ .l-ui-badge--success Green
143
+ .l-ui-badge--warning Yellow
144
+ .l-ui-badge--danger Red
145
145
  ```
146
146
 
147
147
  ## Tabs
148
148
 
149
149
  ```
150
- .l-ui-tabs__list Tab list container (role="tablist")
151
- .l-ui-tabs__tab Tab button
152
- .l-ui-tabs__tab--active Active tab with accent border
153
- .l-ui-tabs__panel Tab panel content
150
+ .l-ui-tabs__list Tab list container (role="tablist")
151
+ .l-ui-tabs__tab Tab button
152
+ .l-ui-tabs__tab--active Active tab with accent border
153
+ .l-ui-tabs__panel Tab panel content
154
154
  ```
155
155
 
156
156
  ## Modal
157
157
 
158
158
  ```
159
- .l-ui-modal <dialog> element
160
- .l-ui-modal__header Modal header
161
- .l-ui-modal__body Scrollable modal content
159
+ .l-ui-modal <dialog> element
160
+ .l-ui-modal__header Modal header
161
+ .l-ui-modal__body Scrollable modal content
162
162
  ```
163
163
 
164
164
  ## Breadcrumbs
165
165
 
166
166
  ```
167
- .l-ui-breadcrumbs <nav> container
168
- .l-ui-breadcrumbs__list <ol> list
169
- .l-ui-breadcrumbs__item <li> item
170
- .l-ui-breadcrumbs__link Breadcrumb link
167
+ .l-ui-breadcrumbs <nav> container
168
+ .l-ui-breadcrumbs__list <ol> list
169
+ .l-ui-breadcrumbs__item <li> item
170
+ .l-ui-breadcrumbs__link Breadcrumb link
171
171
  ```
172
172
 
173
173
  ## Pagination
174
174
 
175
175
  ```
176
- .l-ui-pagination Pagination container
177
- .l-ui-pagination__item Page link or span
178
- .l-ui-pagination__item--active Current page
179
- .l-ui-pagination__item--disabled Disabled navigation
180
- .l-ui-pagination__gap Gap indicator (...)
176
+ .l-ui-pagination Pagination container
177
+ .l-ui-pagination__item Page link or span
178
+ .l-ui-pagination__item--active Current page
179
+ .l-ui-pagination__item--disabled Disabled navigation
180
+ .l-ui-pagination__gap Gap indicator (...)
181
181
  ```
182
182
 
183
183
  ## Navigation
184
184
 
185
185
  ```
186
- .l-ui-container--navigation Sidebar container
187
- .l-ui-container--navigation.open Visible sidebar
188
- .l-ui-backdrop--navigation Overlay backdrop
189
- .l-ui-backdrop--navigation.open Visible backdrop
190
- .l-ui-navigation Nav flexbox
191
- .l-ui-navigation__links Nav links list
192
- .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
195
- .l-ui-navigation__user User info section
196
- .l-ui-navigation__user-name User name text
197
- .l-ui-navigation__user-email User email text
186
+ .l-ui-container--navigation Sidebar container
187
+ .l-ui-container--navigation.open Visible sidebar
188
+ .l-ui-backdrop--navigation Overlay backdrop
189
+ .l-ui-backdrop--navigation.open Visible backdrop
190
+ .l-ui-navigation Nav flexbox
191
+ .l-ui-navigation__links Nav links list
192
+ .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
195
+ .l-ui-navigation__user User info section
196
+ .l-ui-navigation__user-name User name text
197
+ .l-ui-navigation__user-email User email text
198
198
  ```
199
199
 
200
200
  ## Header
201
201
 
202
202
  ```
203
- .l-ui-container--header Fixed header container
204
- .l-ui-header Header flexbox
205
- .l-ui-header__icon Header icon (responsive)
206
- .l-ui-header__icon--light Light theme icon
207
- .l-ui-header__icon--dark Dark theme icon
208
- .l-ui-header__logo Header logo (responsive)
209
- .l-ui-header__logo--light Light theme logo
210
- .l-ui-header__logo--dark Dark theme logo
211
- .l-ui-theme-toggle Theme toggle button
212
- .l-ui-theme-toggle__icon--light Sun icon (shown in dark mode)
213
- .l-ui-theme-toggle__icon--dark Moon icon (shown in light mode)
203
+ .l-ui-container--header Fixed header container
204
+ .l-ui-header Header flexbox
205
+ .l-ui-header__icon Header icon (responsive)
206
+ .l-ui-header__icon--light Light theme icon
207
+ .l-ui-header__icon--dark Dark theme icon
208
+ .l-ui-header__logo Header logo (responsive)
209
+ .l-ui-header__logo--light Light theme logo
210
+ .l-ui-header__logo--dark Dark theme logo
211
+ .l-ui-theme-toggle Theme toggle button
212
+ .l-ui-theme-toggle__icon--light Sun icon (shown in dark mode)
213
+ .l-ui-theme-toggle__icon--dark Moon icon (shown in light mode)
214
214
  ```
215
215
 
216
216
  ## Panel
217
217
 
218
218
  ```
219
- .l-ui-container--panel Side panel container
220
- .l-ui-container--panel.open Visible panel
221
- .l-ui-panel Panel flexbox
222
- .l-ui-panel__button Floating action button
223
- .l-ui-panel__button--dragging During drag
224
- .l-ui-panel__button--snapping Snapping to edge
225
- .l-ui-panel__icon--light Panel button icon (light)
226
- .l-ui-panel__icon--dark Panel button icon (dark)
227
- .l-ui-panel__resize-handle Desktop resize handle
228
- .l-ui-panel__header Panel header
229
- .l-ui-panel__header-heading Panel title
230
- .l-ui-panel__body Scrollable panel content
231
- .l-ui-panel__input Panel input area (footer)
219
+ .l-ui-container--panel Side panel container
220
+ .l-ui-container--panel.open Visible panel
221
+ .l-ui-panel Panel flexbox
222
+ .l-ui-panel__button Floating action button
223
+ .l-ui-panel__button--dragging During drag
224
+ .l-ui-panel__button--snapping Snapping to edge
225
+ .l-ui-panel__icon--light Panel button icon (light)
226
+ .l-ui-panel__icon--dark Panel button icon (dark)
227
+ .l-ui-panel__resize-handle Desktop resize handle
228
+ .l-ui-panel__header Panel header
229
+ .l-ui-panel__header-heading Panel title
230
+ .l-ui-panel__body Scrollable panel content
231
+ .l-ui-panel__input Panel input area (footer)
232
232
  ```
233
233
 
234
234
  ## Conversation
235
235
 
236
236
  ```
237
- .l-ui-conversation Conversation wrapper
238
- .l-ui-conversation__messages Scrollable messages area
239
- .l-ui-conversation__composer Message input area
237
+ .l-ui-conversation Conversation wrapper
238
+ .l-ui-conversation__messages Scrollable messages area
239
+ .l-ui-conversation__composer Message input area
240
240
  .l-ui-conversation__composer-input Textarea
241
- .l-ui-conversation__separator Date separator
241
+ .l-ui-conversation__separator Date separator
242
242
 
243
- .l-ui-message Message wrapper
244
- .l-ui-message--sent Sent message (right-aligned)
245
- .l-ui-message__avatar User avatar
246
- .l-ui-message__bubble Message bubble
247
- .l-ui-message__author Author name
248
- .l-ui-message__body Message content
249
- .l-ui-message__footer Metadata footer
250
- .l-ui-message__timestamp Timestamp
243
+ .l-ui-message Message wrapper
244
+ .l-ui-message--sent Sent message (right-aligned)
245
+ .l-ui-message__avatar User avatar
246
+ .l-ui-message__bubble Message bubble
247
+ .l-ui-message__author Author name
248
+ .l-ui-message__body Message content
249
+ .l-ui-message__footer Metadata footer
250
+ .l-ui-message__timestamp Timestamp
251
251
  ```
252
252
 
253
253
  ## Markdown
@@ -268,36 +268,43 @@ WCAG 2.2 AA table pattern:
268
268
  ## Utility classes
269
269
 
270
270
  ```
271
- .l-ui-utility--mt-0 through --mt-8 Margin top (fixed scale)
272
- .l-ui-utility--mt-sm/md/lg/xl/2xl Responsive margin top
273
- .l-ui-utility--mb-0 Margin bottom zero
274
- .l-ui-sr-only Visually hidden, screen reader only
275
- .l-ui-skip-link Accessibility skip link
276
- .l-ui-list Styled list
277
- .l-ui-container--grid 1-col mobile, 2-col desktop grid
278
- .l-ui-container--spread Flex row with space-between
279
- .l-ui-container--pagy Pagination wrapper
280
- .l-ui-scroll-lock Prevent body scroll (mobile panels/modals)
271
+ .l-ui-utility--mt-0 through --mt-8 Margin top (fixed scale)
272
+ .l-ui-utility--mt-sm/md/lg/xl/2xl Responsive margin top
273
+ .l-ui-utility--mb-0 Margin bottom zero
274
+ .l-ui-sr-only Visually hidden, screen reader only
275
+ .l-ui-skip-link Accessibility skip link
276
+ .l-ui-list Styled list
277
+ .l-ui-container--grid 1-col mobile, 2-col desktop grid
278
+ .l-ui-container--spread Flex row with space-between
279
+ .l-ui-container--pagy Pagination wrapper
280
+ .l-ui-scroll-lock Prevent body scroll (mobile panels/modals)
281
281
  ```
282
282
 
283
283
  ## Theming tokens
284
284
 
285
- All colour values are space-separated HSL channels (e.g. `220 80% 55%`). Override after importing the engine CSS.
285
+ All color values are space-separated HSL channels (e.g. `220 80% 55%`). Override after importing the engine CSS.
286
286
 
287
+ Tier 1 - Accent (quick branding):
288
+ ```
289
+ --accent Primary action color
290
+ --accent-foreground Text on accent backgrounds
291
+ ```
292
+
293
+ Tier 2 - Full palette (override individually as needed):
287
294
  ```
288
- --accent Primary action colour
289
- --accent-foreground Text on accent backgrounds
290
295
  --background Page background
291
- --foreground Primary text colour
296
+ --foreground Primary text color
292
297
  --foreground-muted Secondary/muted text
293
- --border Default border colour
298
+ --border Default border color
294
299
  --border-control Form control border
295
- --ring Focus ring colour
300
+ --ring Focus ring color
296
301
  --surface Card/surface background
297
- --surface-active Active/selected surface
298
- --danger Danger/error colour
302
+ --surface-highlighted Highlighted surface
303
+ --button-primary-bg Primary button background (defaults to --accent)
304
+ --button-primary-text Primary button and floating icon text (defaults to --accent-foreground)
305
+ --danger Danger/error color
299
306
  --danger-light Light danger background
300
- --danger-text Danger text colour
307
+ --danger-text Danger text color
301
308
  --success-bg Success background
302
309
  --success-text Success text
303
310
  --switch-track-checked Checked switch track
@@ -305,6 +312,7 @@ All colour values are space-separated HSL channels (e.g. `220 80% 55%`). Overrid
305
312
  --warning-text Warning text
306
313
  --error-bg Error background
307
314
  --error-text Error text
308
- --backdrop Backdrop overlay colour
309
315
  --header-height Header height (default 63px)
310
316
  ```
317
+
318
+ Override --button-primary-text when your accent color needs a different text/icon color on buttons (e.g. a pink accent with white button text in dark mode).
data/AGENTS.md CHANGED
@@ -5,7 +5,7 @@ Guidance for AI agents working in this repository.
5
5
  ## Architecture
6
6
 
7
7
  - **Entry:** `require "layered-ui-rails"` → `lib/layered/ui.rb` → `lib/layered/ui/engine.rb`
8
- - **Engine:** importmap, assets, Pagy helpers when present; helpers: `AuthenticationHelper`, `NavigationHelper`, `PaginationHelper`
8
+ - **Engine:** importmap, assets, Pagy helpers when present; helpers: `AuthenticationHelper`, `NavigationHelper`, `PagyHelper`
9
9
  - **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.
10
10
  - **CSS `@apply`:** Multi-line with grouping (layout → spacing → typography → colors → effects). Single utilities may stay on one line.
11
11
  - **Generators:** `bin/rails generate layered:ui:install` (copy CSS, import CSS, import JS)
data/CHANGELOG.md CHANGED
@@ -2,6 +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
+ ## [0.6.0] - 2026-04-19
6
+
7
+ ### Added
8
+
9
+ - `rewriteLink` action on `l-ui--search-form` controller to preserve URL params in pagination links across Turbo Frames
10
+ - Paginated Ransack demo in the dummy app with scoped `page_key` params
11
+ - Thin scrollbars on all scrollable elements within `l-ui-body` (for Windows compatibility)
12
+ - `--button-primary-bg` and `--button-primary-text` tokens documented as independently overridable (e.g. pink accent with white button text in dark mode)
13
+
14
+ ### Changed
15
+
16
+ - Renamed `PaginationHelper` to `PagyHelper` to match the underlying gem
17
+ - Renamed `--surface-active` to `--surface-highlighted` (and all related CSS classes: `l-ui-surface--active` to `l-ui-surface--highlighted`, `l-ui-surface--collapsible-active` to `l-ui-surface--collapsible-highlighted`)
18
+ - Scoped page params (e.g. `users_page`) are now reset when a Ransack search or sort submits, returning to page 1
19
+ - Floating icon buttons (`l-ui-panel__button`, `l-ui-scroll-to-bottom`) now use solid `bg-button-primary-bg` instead of semi-transparent with shadow
20
+ - Navigation backdrop uses blur-only effect instead of a color overlay
21
+ - Navigation secondary border changed from `border-surface` to `border-border`
22
+ - Dummy app uses default Devise routes instead of custom path names
23
+ - Agent skill section moved higher in README and dummy app home page
24
+ - Documentation clarifies that `content_for` blocks must appear above the layout render call
25
+ - Color token documentation reorganised into Tier 1 (accent) and Tier 2 (full palette)
26
+
27
+ ### Removed
28
+
29
+ - `--backdrop` CSS custom property - backdrops now use blur-only styling
30
+
5
31
  ## [0.5.0] - 2026-04-19
6
32
 
7
33
  ### Added
data/README.md CHANGED
@@ -1,11 +1,14 @@
1
1
  # layered-ui-rails
2
2
 
3
+ [![License: Apache 2.0](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)
3
4
  [![CI](https://github.com/layered-ai-public/layered-ui-rails/actions/workflows/ci.yml/badge.svg)](https://github.com/layered-ai-public/layered-ui-rails/actions/workflows/ci.yml)
4
5
  [![WCAG 2.2 AA](https://img.shields.io/badge/WCAG_2.2-AA-green)](https://www.w3.org/WAI/WCAG22/quickref/)
5
- [![License: Apache 2.0](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)
6
6
  [![Website](https://img.shields.io/badge/Website-layered.ai-purple)](https://www.layered.ai/)
7
7
  [![GitHub](https://img.shields.io/badge/GitHub-layered--ui--rails-black)](https://github.com/layered-ai-public/layered-ui-rails)
8
8
  [![Discord](https://img.shields.io/badge/Discord-join-5865F2)](https://discord.gg/aCGqz9Bx)
9
+ [![YouTube](https://img.shields.io/badge/YouTube-subscribe-FF0000)](https://www.youtube.com/@UseLayeredAi)
10
+ [![X](https://img.shields.io/badge/X-follow-000000)](https://x.com/UseLayeredAi)
11
+ [![LinkedIn](https://img.shields.io/badge/LinkedIn-follow-0A66C2)](https://www.linkedin.com/company/uselayeredai/)
9
12
 
10
13
  An open source, Rails 8+ engine that provides WCAG 2.2 AA compliant design tokens, Tailwind CSS utilities, and Stimulus controllers for theme switching, mobile navigation, slide-out panels, modals, and tabs. See the [live demo](https://layered-ui-rails.layered.ai).
11
14
 
@@ -42,12 +45,36 @@ The install generator will:
42
45
  - Add `@import "./layered_ui";` to your `application.css`
43
46
  - Add `import "layered_ui"` to your `application.js`
44
47
 
45
- Then update your application layout to render the engine layout:
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:
46
49
 
47
50
  ```erb
51
+ <% content_for :l_ui_body_class, "l-ui-body--always-show-navigation" %>
52
+
53
+ <% content_for :l_ui_navigation_items do %>
54
+ <%= l_ui_navigation_item "Home", root_path %>
55
+ <% end %>
56
+
48
57
  <%= render template: "layouts/layered_ui/application" %>
49
58
  ```
50
59
 
60
+ ## Agent skill
61
+
62
+ An [agent skill](https://agentskills.io) is included so AI coding agents can work with `layered-ui-rails` in your project. Once installed, the agent can handle the full setup - just ask it to add `layered-ui-rails` to your app and it will install the gem, run the generator, and configure your layout.
63
+
64
+ **Project install** - scoped to a single repo, available to all contributors:
65
+
66
+ ```bash
67
+ bin/rails generate layered:ui:install_agent_skill
68
+ ```
69
+
70
+ **Global install** - available across all your projects:
71
+
72
+ ```bash
73
+ ./install-skill.sh
74
+ # or install remotely without cloning the repo:
75
+ curl -fsSL https://raw.githubusercontent.com/layered-ai-public/layered-ui-rails/main/install-skill.sh | sh
76
+ ```
77
+
51
78
  ## Requirements
52
79
 
53
80
  - Ruby on Rails >= 8.0
@@ -67,7 +94,10 @@ Then update your application layout to render the engine layout:
67
94
 
68
95
  ## Customising theme tokens
69
96
 
70
- All colors are CSS custom properties on `:root`. Override any token in your stylesheet (after importing the engine CSS):
97
+ All colors are CSS custom properties on `:root` using a two-tier system:
98
+
99
+ - **Tier 1 - Accent:** Set `--accent` and `--accent-foreground` for quick branding.
100
+ - **Tier 2 - Full palette:** Override any individual token. Includes `--button-primary-bg` and `--button-primary-text` which default to the accent pair but can be overridden independently (e.g. a pink accent that needs white button text in dark mode).
71
101
 
72
102
  ```css
73
103
  /* app/assets/tailwind/application.css */
@@ -81,6 +111,7 @@ All colors are CSS custom properties on `:root`. Override any token in your styl
81
111
  .dark {
82
112
  --accent: 220 80% 65%;
83
113
  --accent-foreground: 0 0% 9%;
114
+ --button-primary-text: 0 0% 100%; /* white icons/text on colored buttons */
84
115
  }
85
116
  ```
86
117
 
@@ -153,24 +184,6 @@ For per-request icons, set instance variables - the engine renders `<link>` and
153
184
 
154
185
  > **Security:** Rails HTML-escapes URL values, so XSS via attribute injection is mitigated. However, if values are tenant-controlled, validate that they are legitimate URLs - reject `javascript:` schemes and ensure values point to expected origins.
155
186
 
156
- ## Agent skill
157
-
158
- An [agent skill](https://agentskills.io) is included so AI coding agents like Claude Code can work with layered-ui-rails in your project.
159
-
160
- **Project install** - scoped to a single repo, available to all contributors:
161
-
162
- ```bash
163
- bin/rails generate layered:ui:install_agent_skill
164
- ```
165
-
166
- **Global install** - available across all your projects:
167
-
168
- ```bash
169
- ./install-skill.sh
170
- # or install remotely without cloning the repo:
171
- curl -fsSL https://raw.githubusercontent.com/layered-ai-public/layered-ui-rails/main/install-skill.sh | sh
172
- ```
173
-
174
187
  ## Documentation
175
188
 
176
189
  An online version of the documentation is available at **[layered-ui-rails.layered.ai](https://layered-ui-rails.layered.ai)**.
@@ -215,4 +228,3 @@ Copyright 2026 LAYERED AI LIMITED (UK company number: 17056830). See [NOTICE](NO
215
228
  ## Trademarks
216
229
 
217
230
  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.
218
-
@@ -32,12 +32,25 @@
32
32
 
33
33
  @variant dark (&:where(.dark, .dark *));
34
34
 
35
- /* Colors */
35
+ /*
36
+ * Color tokens (HSL channels, e.g. "220 80% 55%")
37
+ *
38
+ * Tier 1 - Accent: --accent, --accent-foreground
39
+ * Tier 2 - Full palette: background, foreground, border, surface, etc.
40
+ * Also includes --button-primary-bg and --button-primary-text which
41
+ * default to the accent pair but can be overridden independently
42
+ * (e.g. a pink accent that needs white button text in dark mode).
43
+ *
44
+ * See the generator (bin/rails generate layered:ui:create_overrides) for a
45
+ * ready-made overrides file with all tokens commented out.
46
+ */
36
47
 
37
48
  @layer base {
38
49
  :root {
50
+ /* Tier 1 - Accent */
39
51
  --accent: 0 0% 9%;
40
52
  --accent-foreground: 0 0% 100%;
53
+ /* Tier 2 - Full palette */
41
54
  --background: 0 0% 100%;
42
55
  --foreground: 0 0% 13%;
43
56
  --foreground-muted: 0 0% 29%;
@@ -45,7 +58,7 @@
45
58
  --border-control: 0 0% 55%;
46
59
  --ring: 0 0% 13%;
47
60
  --surface: 0 0% 96%;
48
- --surface-active: 0 0% 91%;
61
+ --surface-highlighted: 0 0% 91%;
49
62
  --button-primary-bg: var(--accent);
50
63
  --button-primary-text: var(--accent-foreground);
51
64
  --danger: 0 72% 38%;
@@ -58,13 +71,14 @@
58
71
  --warning-text: 48 96% 15%;
59
72
  --error-bg: 0 84% 75%;
60
73
  --error-text: 0 93% 12%;
61
- --backdrop: 0 0% 0%;
62
74
  --header-height: 63px;
63
75
  }
64
76
 
65
77
  .dark {
78
+ /* Tier 1 - Accent */
66
79
  --accent: 0 0% 100%;
67
80
  --accent-foreground: 0 0% 9%;
81
+ /* Tier 2 - Full palette */
68
82
  --background: 0 0% 0%;
69
83
  --foreground: 0 0% 89%;
70
84
  --foreground-muted: 0 0% 71%;
@@ -72,7 +86,7 @@
72
86
  --border-control: 0 0% 40%;
73
87
  --ring: 0 0% 89%;
74
88
  --surface: 0 0% 8%;
75
- --surface-active: 0 0% 16%;
89
+ --surface-highlighted: 0 0% 16%;
76
90
  --button-primary-bg: var(--accent);
77
91
  --button-primary-text: var(--accent-foreground);
78
92
  --danger: 0 85% 60%;
@@ -85,7 +99,6 @@
85
99
  --warning-text: 48 96% 80%;
86
100
  --error-bg: 0 93% 12%;
87
101
  --error-text: 0 84% 75%;
88
- --backdrop: 0 0% 0%;
89
102
  }
90
103
 
91
104
  /* Typography */
@@ -127,6 +140,14 @@
127
140
  @apply whitespace-pre overflow-x-auto
128
141
  text-foreground-muted;
129
142
  }
143
+
144
+ /* Scrollbars */
145
+
146
+ .l-ui-body,
147
+ .l-ui-body * {
148
+ scrollbar-width: thin;
149
+ scrollbar-color: hsl(var(--foreground-muted) / 0.3) transparent;
150
+ }
130
151
  }
131
152
 
132
153
  /* Theme */
@@ -142,7 +163,7 @@
142
163
  --color-border: hsl(var(--border));
143
164
  --color-border-control: hsl(var(--border-control));
144
165
  --color-surface: hsl(var(--surface));
145
- --color-surface-active: hsl(var(--surface-active));
166
+ --color-surface-highlighted: hsl(var(--surface-highlighted));
146
167
  --color-button-primary-bg: hsl(var(--button-primary-bg));
147
168
  --color-button-primary-text: hsl(var(--button-primary-text));
148
169
  --color-danger: hsl(var(--danger));
@@ -155,7 +176,6 @@
155
176
  --color-warning-text: hsl(var(--warning-text));
156
177
  --color-error-bg: hsl(var(--error-bg));
157
178
  --color-error-text: hsl(var(--error-text));
158
- --color-backdrop: hsl(var(--backdrop));
159
179
  --color-ring: hsl(var(--ring));
160
180
  }
161
181
 
@@ -346,7 +366,7 @@
346
366
  .l-ui-markdown code {
347
367
  @apply px-1.5 py-0.5
348
368
  text-xs
349
- bg-surface-active
369
+ bg-surface-highlighted
350
370
  rounded;
351
371
  }
352
372
 
@@ -593,7 +613,7 @@
593
613
  .l-ui-backdrop--navigation {
594
614
  @apply fixed top-[var(--header-height)] left-0 right-0 bottom-0
595
615
  z-[45]
596
- bg-backdrop/50 backdrop-blur-xs
616
+ backdrop-blur-sm
597
617
  opacity-0 pointer-events-none
598
618
  transition-opacity duration-200;
599
619
  }
@@ -647,7 +667,7 @@
647
667
  .l-ui-navigation__secondary {
648
668
  @apply flex flex-col
649
669
  pl-4
650
- border-l-4 border-surface;
670
+ border-l-4 border-border;
651
671
 
652
672
  .l-ui-navigation__item--active {
653
673
  @apply bg-transparent
@@ -854,9 +874,9 @@ pre.l-ui-surface {
854
874
  }
855
875
  }
856
876
 
857
- .l-ui-surface--active {
877
+ .l-ui-surface--highlighted {
858
878
  @apply surface
859
- bg-surface-active;
879
+ bg-surface-highlighted;
860
880
  }
861
881
 
862
882
  .l-ui-surface--collapsible {
@@ -865,14 +885,14 @@ pre.l-ui-surface {
865
885
  bg-surface;
866
886
  }
867
887
 
868
- .l-ui-surface--collapsible-active {
888
+ .l-ui-surface--collapsible-highlighted {
869
889
  @apply surface
870
890
  p-0
871
- bg-surface-active;
891
+ bg-surface-highlighted;
872
892
  }
873
893
 
874
894
  .l-ui-surface--collapsible,
875
- .l-ui-surface--collapsible-active {
895
+ .l-ui-surface--collapsible-highlighted {
876
896
  &[open] > .l-ui-surface__summary .l-ui-surface__chevron {
877
897
  @apply rotate-90;
878
898
  }
@@ -1252,7 +1272,7 @@ pre.l-ui-surface {
1252
1272
 
1253
1273
  @utility pagination__item--active {
1254
1274
  @apply font-bold
1255
- bg-surface-active
1275
+ bg-surface-highlighted
1256
1276
  text-foreground
1257
1277
  pointer-events-none;
1258
1278
  }
@@ -1334,7 +1354,7 @@ pre.l-ui-surface {
1334
1354
 
1335
1355
  .l-ui-badge--default {
1336
1356
  @apply badge
1337
- bg-surface-active
1357
+ bg-surface-highlighted
1338
1358
  text-foreground-muted;
1339
1359
  }
1340
1360
 
@@ -1363,9 +1383,9 @@ pre.l-ui-surface {
1363
1383
  w-14 h-14
1364
1384
  z-40
1365
1385
  cursor-pointer
1366
- bg-button-primary-bg/60
1386
+ bg-button-primary-bg
1367
1387
  text-button-primary-text
1368
- rounded-full shadow-lg backdrop-blur-xs
1388
+ rounded-full
1369
1389
  focus-ring
1370
1390
  transition-all;
1371
1391
  touch-action: none;
@@ -1632,10 +1652,10 @@ pre.l-ui-surface {
1632
1652
  .l-ui-scroll-to-bottom {
1633
1653
  @apply
1634
1654
  sticky bottom-2 flex items-center justify-center
1635
- mx-auto -mt-9 h-9 w-9
1655
+ ml-auto mr-0 -mt-9 h-9 w-9
1636
1656
  rounded-full
1637
1657
  cursor-pointer
1638
- bg-button-primary-bg/60 text-button-primary-text shadow-lg backdrop-blur-xs
1658
+ bg-button-primary-bg text-button-primary-text
1639
1659
  focus-ring
1640
1660
  opacity-0 pointer-events-none
1641
1661
  transition-opacity duration-200;
@@ -1704,7 +1724,7 @@ pre.l-ui-surface {
1704
1724
  }
1705
1725
 
1706
1726
  .l-ui-modal::backdrop {
1707
- @apply bg-backdrop/50 backdrop-blur-sm;
1727
+ @apply backdrop-blur-sm;
1708
1728
  }
1709
1729
 
1710
1730
  .l-ui-modal__header {
@@ -1,6 +1,6 @@
1
1
  module Layered
2
2
  module Ui
3
- module PaginationHelper
3
+ module PagyHelper
4
4
  def l_ui_pagy(pagy)
5
5
  unless defined?(Pagy)
6
6
  return tag.p("Pagination requires the pagy gem. Add `gem \"pagy\"` to your Gemfile.", class: "l-ui-notice--warning") if Rails.env.development?
@@ -5,7 +5,6 @@ import { getHeaderHeight, getPadding, getLeftEdge } from "layered_ui/utilities/l
5
5
  const BUTTON_SIZE = 56
6
6
  const DRAG_THRESHOLD = 5
7
7
  const SNAP_TIMEOUT = 400
8
- const TOGGLE_DELAY = 160
9
8
  const TOP_LEFT = "top-left"
10
9
  const TOP_RIGHT = "top-right"
11
10
  const BOTTOM_LEFT = "bottom-left"
@@ -25,7 +24,6 @@ export default class extends Controller {
25
24
  })
26
25
  }
27
26
  this.boundKeyboardShortcuts = this.handleKeyboardShortcuts.bind(this)
28
- this.toggleTimeout = null
29
27
 
30
28
  this.restorePosition()
31
29
  window.addEventListener('resize', this.boundWindowResize)
@@ -33,7 +31,6 @@ export default class extends Controller {
33
31
  }
34
32
 
35
33
  disconnect() {
36
- clearTimeout(this.toggleTimeout)
37
34
  cancelAnimationFrame(this._resizeFrame)
38
35
  window.removeEventListener('resize', this.boundWindowResize)
39
36
  document.removeEventListener('keydown', this.boundKeyboardShortcuts)
@@ -49,11 +46,13 @@ export default class extends Controller {
49
46
  }
50
47
  }
51
48
 
52
- // Restore position from localStorage or apply default
53
49
  restorePosition() {
54
50
  const position = storageGetJSON("panelButtonPosition")
55
51
 
56
- if (position && position.edge && typeof position.top === "number") {
52
+ if (position && position.corner) {
53
+ this.moveToCorner(position.corner)
54
+ } else if (position && position.edge && typeof position.top === "number") {
55
+ this.corner = null
57
56
  const topPx = this.clampTop(position.top / 100 * window.innerHeight)
58
57
  this.element.style.top = `${topPx}px`
59
58
  if (position.edge === "left") {
@@ -68,56 +67,8 @@ export default class extends Controller {
68
67
  }
69
68
  }
70
69
 
71
- // Delay toggle slightly so double-click can trigger corner cycling instead
72
- queueToggle(event) {
73
- if (event.detail > 1) return
74
-
75
- clearTimeout(this.toggleTimeout)
76
- this.toggleTimeout = setTimeout(() => {
77
- this.dispatch("toggle", { bubbles: true })
78
- }, TOGGLE_DELAY)
79
- }
80
-
81
- // Double-click cycles the floating button around screen corners
82
- cycleCorner(event) {
83
- event.preventDefault()
84
- event.stopPropagation()
85
- clearTimeout(this.toggleTimeout)
86
-
87
- const current = this.currentCorner()
88
- const next = this.nextCorner(current)
89
- this.moveToCorner(next)
90
- }
91
-
92
- currentCorner() {
93
- const topPx = this.element.style.top
94
- ? parseFloat(this.element.style.top)
95
- : this.element.getBoundingClientRect().top
96
-
97
- const topEdge = this.clampTop(getHeaderHeight() + getPadding())
98
- const bottomEdge = this.clampTop(window.innerHeight - BUTTON_SIZE - getPadding())
99
- const midY = (topEdge + bottomEdge) / 2
100
- const isTop = topPx <= midY
101
-
102
- const isRight = this.element.style.right && this.element.style.right !== "auto"
103
- if (isTop && isRight) return TOP_RIGHT
104
- if (!isTop && isRight) return BOTTOM_RIGHT
105
- if (isTop) return TOP_LEFT
106
- return BOTTOM_LEFT
107
- }
108
-
109
- nextCorner(corner) {
110
- switch (corner) {
111
- case TOP_LEFT:
112
- return TOP_RIGHT
113
- case TOP_RIGHT:
114
- return BOTTOM_RIGHT
115
- case BOTTOM_RIGHT:
116
- return BOTTOM_LEFT
117
- case BOTTOM_LEFT:
118
- default:
119
- return TOP_LEFT
120
- }
70
+ toggle() {
71
+ this.dispatch("toggle", { bubbles: true })
121
72
  }
122
73
 
123
74
  // Move the button directly to a corner via keyboard shortcut
@@ -148,6 +99,7 @@ export default class extends Controller {
148
99
  }
149
100
 
150
101
  moveToCorner(corner) {
102
+ this.corner = corner
151
103
  const leftEdge = getLeftEdge()
152
104
  const rightEdge = getPadding()
153
105
  const topEdge = this.clampTop(getHeaderHeight() + getPadding())
@@ -158,40 +110,38 @@ export default class extends Controller {
158
110
  this.element.style.left = `${leftEdge}px`
159
111
  this.element.style.right = "auto"
160
112
  this.element.style.top = `${topEdge}px`
161
- this.savePosition("left", topEdge)
162
113
  break
163
114
  case TOP_RIGHT:
164
115
  this.element.style.left = "auto"
165
116
  this.element.style.right = `${rightEdge}px`
166
117
  this.element.style.top = `${topEdge}px`
167
- this.savePosition("right", topEdge)
168
118
  break
169
119
  case BOTTOM_LEFT:
170
120
  this.element.style.left = `${leftEdge}px`
171
121
  this.element.style.right = "auto"
172
122
  this.element.style.top = `${bottomEdge}px`
173
- this.savePosition("left", bottomEdge)
174
123
  break
175
124
  case BOTTOM_RIGHT:
176
125
  this.element.style.left = "auto"
177
126
  this.element.style.right = `${rightEdge}px`
178
127
  this.element.style.top = `${bottomEdge}px`
179
- this.savePosition("right", bottomEdge)
180
128
  break
181
129
  }
130
+
131
+ this.saveCornerPosition(corner)
132
+ }
133
+
134
+ saveCornerPosition(corner) {
135
+ storageSet("panelButtonPosition", JSON.stringify({ corner }))
182
136
  }
183
137
 
184
- savePosition(edge, topPx) {
185
- const topPercent = topPx / window.innerHeight * 100
186
- storageSet("panelButtonPosition", JSON.stringify({ edge, top: topPercent }))
138
+ saveDragPosition(edge) {
139
+ const top = parseFloat(this.element.style.top) / window.innerHeight * 100
140
+ storageSet("panelButtonPosition", JSON.stringify({ edge, top }))
187
141
  }
188
142
 
189
- // Apply the default bottom-right position
190
143
  applyDefault() {
191
- const padding = getPadding()
192
- this.element.style.right = `${padding}px`
193
- this.element.style.left = "auto"
194
- this.element.style.top = `${window.innerHeight - BUTTON_SIZE - padding}px`
144
+ this.moveToCorner(BOTTOM_RIGHT)
195
145
  }
196
146
 
197
147
  // Start tracking a potential drag
@@ -274,7 +224,8 @@ export default class extends Controller {
274
224
  this.element.style.left = `${targetLeft}px`
275
225
  this.element.style.right = "auto"
276
226
 
277
- this.savePosition(edge, rect.top)
227
+ this.corner = null
228
+ this.saveDragPosition(edge)
278
229
 
279
230
  const onTransitionEnd = () => {
280
231
  this.element.classList.remove("l-ui-panel__button--snapping")
@@ -316,8 +267,12 @@ export default class extends Controller {
316
267
  }, 0)
317
268
  }
318
269
 
319
- // Constrain position to viewport bounds after resize
320
270
  constrainPosition() {
271
+ if (this.corner) {
272
+ this.moveToCorner(this.corner)
273
+ return
274
+ }
275
+
321
276
  if (!this.element.style.top) return
322
277
 
323
278
  const topPx = this.clampTop(parseFloat(this.element.style.top))
@@ -1,13 +1,8 @@
1
1
  import { Controller } from "@hotwired/stimulus"
2
2
 
3
- // Preserves query params from other search scopes when a scoped form submits
4
- // or its clear link is clicked.
5
- //
6
- // When multiple Ransack collections share one page, each form only knows about
7
- // its own fields. This controller reads the current URL at submit time and
8
- // injects hidden inputs for params that belong to other scopes, so the
9
- // resulting URL reflects the full state of all collections. The clear action
10
- // rewrites the clear link's href to include the same preserved params.
3
+ // When multiple scoped Ransack collections share one page, preserves other
4
+ // scopes' params across form submits (preserve), clear links (clear), and
5
+ // pagination clicks (rewriteLink).
11
6
  export default class extends Controller {
12
7
  static values = { scope: String }
13
8
 
@@ -32,16 +27,35 @@ export default class extends Controller {
32
27
  link.href = qs ? `${base}?${qs}` : base
33
28
  }
34
29
 
30
+ rewriteLink(event) {
31
+ const link = event.target.closest(".l-ui-container--pagy a[href]")
32
+ if (!link) return
33
+
34
+ const url = new URL(link.href, window.location.origin)
35
+ const linkKeys = new Set(url.searchParams.keys())
36
+
37
+ for (const [key, value] of this.#otherParams()) {
38
+ if (!linkKeys.has(key)) url.searchParams.append(key, value)
39
+ }
40
+
41
+ link.href = url.pathname + url.search
42
+ }
43
+
35
44
  #otherParams() {
36
45
  const currentParams = new URLSearchParams(window.location.search)
37
46
  const scope = this.scopeValue
38
47
  const result = new URLSearchParams()
39
48
 
40
49
  for (const [key, value] of currentParams) {
41
- if (key === scope || key.startsWith(scope + "[") || key === "commit") continue
50
+ if (key === scope || key.startsWith(scope + "[") || key === "commit" || key === "page" || key === this.#pageParam) continue
42
51
  result.append(key, value)
43
52
  }
44
53
 
45
54
  return result
46
55
  }
56
+
57
+ get #pageParam() {
58
+ const scope = this.scopeValue
59
+ return scope.endsWith("_q") ? scope.slice(0, -2) + "_page" : null
60
+ }
47
61
  }
@@ -60,9 +60,9 @@
60
60
  aria-controls="panel"
61
61
  aria-keyshortcuts="Control+I Meta+I Control+Alt+1 Meta+Alt+1 Control+Alt+2 Meta+Alt+2 Control+Alt+3 Meta+Alt+3 Control+Alt+4 Meta+Alt+4"
62
62
  tabindex="-1"
63
- title="Toggle panel (Ctrl/⌘+i). Move to corners (Ctrl/⌘ + Alt/⌥ + 1/2/3/4), double-click, or drag."
63
+ title="Toggle panel (Ctrl/⌘+i). Move to corners (Ctrl/⌘ + Alt/⌥ + 1/2/3/4) or drag."
64
64
  data-controller="l-ui--panel-button"
65
- data-action="click->l-ui--panel-button#queueToggle dblclick->l-ui--panel-button#cycleCorner mousedown->l-ui--panel-button#startDrag touchstart->l-ui--panel-button#startDrag"
65
+ data-action="click->l-ui--panel-button#toggle mousedown->l-ui--panel-button#startDrag touchstart->l-ui--panel-button#startDrag"
66
66
  data-l-ui--panel-target="actionButton"
67
67
  >
68
68
  <%= image_tag(@l_ui_panel_icon_light_url.presence || "layered_ui/panel_icon_light.svg", alt: "", class: "l-ui-icon--lg l-ui-panel__icon--light", aria: { hidden: true }) %>
@@ -35,6 +35,10 @@ module Layered
35
35
  *
36
36
  * Set a single brand color. Primary buttons, active tabs, and
37
37
  * active navigation items all inherit from these two variables.
38
+ *
39
+ * If your accent color needs a different text/icon color on
40
+ * buttons (e.g. a pink accent with white button text in dark
41
+ * mode), override --button-primary-text in Tier 2 below.
38
42
  * ---------------------------------------------------------------- */
39
43
 
40
44
  :root {
@@ -62,7 +66,7 @@ module Layered
62
66
  --border-control: 0 0% 55%;
63
67
  --ring: 0 0% 13%;
64
68
  --surface: 0 0% 96%;
65
- --surface-active: 0 0% 91%;
69
+ --surface-highlighted: 0 0% 91%;
66
70
  --button-primary-bg: var(--accent);
67
71
  --button-primary-text: var(--accent-foreground);
68
72
  --danger: 0 72% 38%;
@@ -74,7 +78,6 @@ module Layered
74
78
  --warning-text: 48 96% 15%;
75
79
  --error-bg: 0 84% 75%;
76
80
  --error-text: 0 93% 12%;
77
- --backdrop: 0 0% 0%;
78
81
  }
79
82
 
80
83
  .dark {
@@ -85,7 +88,7 @@ module Layered
85
88
  --border-control: 0 0% 40%;
86
89
  --ring: 0 0% 89%;
87
90
  --surface: 0 0% 8%;
88
- --surface-active: 0 0% 16%;
91
+ --surface-highlighted: 0 0% 16%;
89
92
  --button-primary-bg: var(--accent);
90
93
  --button-primary-text: var(--accent-foreground);
91
94
  --danger: 0 85% 60%;
@@ -97,7 +100,6 @@ module Layered
97
100
  --warning-text: 48 96% 80%;
98
101
  --error-bg: 0 93% 12%;
99
102
  --error-text: 0 84% 75%;
100
- --backdrop: 0 0% 0%;
101
103
  }
102
104
  */
103
105
 
@@ -29,7 +29,7 @@ module Layered
29
29
  helper Layered::Ui::AuthenticationHelper
30
30
  helper Layered::Ui::BreadcrumbsHelper
31
31
  helper Layered::Ui::NavigationHelper
32
- helper Layered::Ui::PaginationHelper
32
+ helper Layered::Ui::PagyHelper
33
33
  helper Layered::Ui::TableHelper
34
34
  helper Layered::Ui::FormHelper
35
35
  helper Layered::Ui::RansackHelper
@@ -1,5 +1,5 @@
1
1
  module Layered
2
2
  module Ui
3
- VERSION = "0.5.0"
3
+ VERSION = "0.6.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.5.0
4
+ version: 0.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - layered.ai
@@ -212,7 +212,7 @@ files:
212
212
  - app/helpers/layered/ui/breadcrumbs_helper.rb
213
213
  - app/helpers/layered/ui/form_helper.rb
214
214
  - app/helpers/layered/ui/navigation_helper.rb
215
- - app/helpers/layered/ui/pagination_helper.rb
215
+ - app/helpers/layered/ui/pagy_helper.rb
216
216
  - app/helpers/layered/ui/ransack_helper.rb
217
217
  - app/helpers/layered/ui/table_helper.rb
218
218
  - app/javascript/layered_ui/controllers/l_ui/modal_controller.js