layered-ui-rails 0.5.0 → 0.7.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: b538d1b28b8718c6bcbaa3e9e21b8294b2e0556ca869433245d1e587ac7679a9
4
+ data.tar.gz: 775ec4083eb6e729eee0aa2c83c3ed5001fe258dd79c78ed476e103910797dda
5
5
  SHA512:
6
- metadata.gz: cbac626bf832dc7599b37a4234aac65e1f8cbd1282d1c2bda20e9df3c97319ed468adde98af047affa463895d0bdcf433bd6512af4ceef59c843eba21caf443d
7
- data.tar.gz: f62ab50c0825ee0b152fd48b1c508750cbe04bff880217aa0a7ffa4663d54db3ab285d08546fcdc52117cac2817c2e66196d3f8b96bfff4a6f584c0671e52508
6
+ metadata.gz: bfd719cd045e9ceb2a065c8ab8de722f6bc9187e541580dd39e7f11bd3672aae14628b04252fd0dc0de44dcc0656cb61df01954c210227a611a6bf4c5d187e1e
7
+ data.tar.gz: 4ac05f4a41d0cb312d67f0337136f77a3eecfb944089ce65be0c78fe15fe7a326edd7f06fa790c0b5cec1cb57cb2275c99d14d39015be3288ef35da7dffd706e
@@ -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,11 +146,11 @@ 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
 
146
- Override CSS custom properties after the engine import. Values are space-separated HSL channels (no `hsl()` wrapper).
153
+ Override CSS custom properties after the engine import. Values are space-separated HSL channels (e.g. `220 80% 55%`, or `0 0% 100%` for white). Keywords and hex/rgb won't work - tokens are wrapped in `hsl()` when consumed. A converter such as https://colorpicker.dev/ can help translate hex/rgb values.
147
154
 
148
155
  ```css
149
156
  @import "./layered_ui";
@@ -157,17 +164,19 @@ 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
 
164
171
  Place files in `app/assets/images/layered_ui/` to replace engine defaults:
165
172
 
166
- `logo_light.svg`, `logo_dark.svg`, `icon_light.svg`, `icon_dark.svg`, `apple_touch_icon.png`, `panel_icon_light.svg`, `panel_icon_dark.svg`.
173
+ `logo_light.svg`, `logo_dark.svg`, `icon_light.svg`, `icon_dark.svg`, `apple_touch_icon.png`.
174
+
175
+ The panel toggle button uses an inline SVG that inherits `currentColor`. Recolor it by overriding the `--button-primary-icon` Tier 2 token, or replace the image by setting both `@l_ui_panel_icon_light_url` and `@l_ui_panel_icon_dark_url` (per-request).
167
176
 
168
177
  ## Optional integrations
169
178
 
170
- - **Devise** - auto-detected. Provides styled auth views, header login/register buttons, sidebar user info and logout.
179
+ - **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
180
  - **Pagy** - auto-detected. Use `l_ui_pagy(@pagy)` for styled pagination.
172
181
  - **Ransack** - auto-detected. Use `l_ui_search_form` and `l_ui_sort_link` for styled search and sortable tables.
173
182
 
@@ -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,127 @@ 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 Panel button inline SVG icon
226
+ .l-ui-panel__icon--light Panel button icon (light, for custom image override)
227
+ .l-ui-panel__icon--dark Panel button icon (dark, for custom image override)
228
+ .l-ui-panel__resize-handle Desktop resize handle
229
+ .l-ui-panel__header Panel header
230
+ .l-ui-panel__header-heading Panel title
231
+ .l-ui-panel__body Scrollable panel content
232
+ .l-ui-panel__input Panel input area (footer)
232
233
  ```
233
234
 
234
235
  ## Conversation
235
236
 
236
237
  ```
237
- .l-ui-conversation Conversation wrapper
238
- .l-ui-conversation__messages Scrollable messages area
239
- .l-ui-conversation__composer Message input area
238
+ .l-ui-conversation Conversation wrapper
239
+ .l-ui-conversation__messages Scrollable messages area
240
+ .l-ui-conversation__composer Message input area
240
241
  .l-ui-conversation__composer-input Textarea
241
- .l-ui-conversation__separator Date separator
242
+ .l-ui-conversation__separator Date separator
242
243
 
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
244
+ .l-ui-message Message wrapper
245
+ .l-ui-message--sent Sent message (right-aligned)
246
+ .l-ui-message__avatar User avatar
247
+ .l-ui-message__bubble Message bubble
248
+ .l-ui-message__author Author name
249
+ .l-ui-message__body Message content
250
+ .l-ui-message__footer Metadata footer
251
+ .l-ui-message__timestamp Timestamp
251
252
  ```
252
253
 
253
254
  ## Markdown
@@ -268,36 +269,44 @@ WCAG 2.2 AA table pattern:
268
269
  ## Utility classes
269
270
 
270
271
  ```
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)
272
+ .l-ui-utility--mt-0 through --mt-8 Margin top (fixed scale)
273
+ .l-ui-utility--mt-sm/md/lg/xl/2xl Responsive margin top
274
+ .l-ui-utility--mb-0 Margin bottom zero
275
+ .l-ui-sr-only Visually hidden, screen reader only
276
+ .l-ui-skip-link Accessibility skip link
277
+ .l-ui-list Styled list
278
+ .l-ui-container--grid 1-col mobile, 2-col desktop grid
279
+ .l-ui-container--spread Flex row with space-between
280
+ .l-ui-container--pagy Pagination wrapper
281
+ .l-ui-scroll-lock Prevent body scroll (mobile panels/modals)
281
282
  ```
282
283
 
283
284
  ## Theming tokens
284
285
 
285
- All colour values are space-separated HSL channels (e.g. `220 80% 55%`). Override after importing the engine CSS.
286
+ All color values are space-separated HSL channels (e.g. `220 80% 55%`, or `0 0% 100%` for white). Override after importing the engine CSS. Keywords and hex/rgb won't work - tokens are wrapped in `hsl()` when consumed. A converter such as https://colorpicker.dev/ can help translate hex/rgb values.
286
287
 
288
+ Tier 1 - Accent (quick branding):
289
+ ```
290
+ --accent Primary action color
291
+ --accent-foreground Text on accent backgrounds
292
+ ```
293
+
294
+ Tier 2 - Full palette (override individually as needed):
287
295
  ```
288
- --accent Primary action colour
289
- --accent-foreground Text on accent backgrounds
290
296
  --background Page background
291
- --foreground Primary text colour
297
+ --foreground Primary text color
292
298
  --foreground-muted Secondary/muted text
293
- --border Default border colour
299
+ --border Default border color
294
300
  --border-control Form control border
295
- --ring Focus ring colour
301
+ --ring Focus ring color
296
302
  --surface Card/surface background
297
- --surface-active Active/selected surface
298
- --danger Danger/error colour
303
+ --surface-highlighted Highlighted surface
304
+ --button-primary-bg Primary button background (defaults to --accent)
305
+ --button-primary-text Primary button text (defaults to --accent-foreground)
306
+ --button-primary-icon Icon color on filled icon buttons (defaults to --button-primary-text)
307
+ --danger Danger/error color
299
308
  --danger-light Light danger background
300
- --danger-text Danger text colour
309
+ --danger-text Danger text color
301
310
  --success-bg Success background
302
311
  --success-text Success text
303
312
  --switch-track-checked Checked switch track
@@ -305,6 +314,7 @@ All colour values are space-separated HSL channels (e.g. `220 80% 55%`). Overrid
305
314
  --warning-text Warning text
306
315
  --error-bg Error background
307
316
  --error-text Error text
308
- --backdrop Backdrop overlay colour
309
317
  --header-height Header height (default 63px)
310
318
  ```
319
+
320
+ 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). Override --button-primary-icon instead when only the icon color should change.
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,48 @@
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.7.0] - 2026-04-23
6
+
7
+ ### Added
8
+
9
+ - `--button-primary-icon` Tier 2 token for filled icon buttons (e.g. `l-ui-panel__button`, `l-ui-scroll-to-bottom`). Defaults to `--button-primary-text`; override to recolor icons independently of button text.
10
+ - Inline SVG default for the panel toggle button icon - inherits `currentColor` so it picks up `--button-primary-icon`. Per-request `@l_ui_panel_icon_light_url` / `@l_ui_panel_icon_dark_url` still replace the default image, and each theme falls back to the inline SVG independently when its variable is unset.
11
+ - Documentation of the HSL-channel format for theme tokens (space-separated channels such as `220 80% 55%`) in `SKILL.md`, `references/CSS.md`, and the overrides generator, with a pointer to a hex/rgb converter.
12
+
13
+ ### Changed
14
+
15
+ - `l-ui-scroll-to-bottom` now bakes the chevron-down icon into CSS via `mask-image` (with `-webkit-mask` prefixes and a URL-encoded SVG for Safari compatibility) and colours it with `--button-primary-icon`. Host markup is a plain empty `<button class="l-ui-scroll-to-bottom" aria-label="...">` - no inner `<svg>`/`<img>` required.
16
+
17
+ ### Removed
18
+
19
+ - Static-file panel icon overrides (`panel_icon_light.svg`, `panel_icon_dark.svg` in `app/assets/images/layered_ui/`). Use `@l_ui_panel_icon_light_url` / `@l_ui_panel_icon_dark_url` instance variables to supply a custom image.
20
+
21
+ ## [0.6.0] - 2026-04-19
22
+
23
+ ### Added
24
+
25
+ - `rewriteLink` action on `l-ui--search-form` controller to preserve URL params in pagination links across Turbo Frames
26
+ - Paginated Ransack demo in the dummy app with scoped `page_key` params
27
+ - Thin scrollbars on all scrollable elements within `l-ui-body` (for Windows compatibility)
28
+ - `--button-primary-bg` and `--button-primary-text` tokens documented as independently overridable (e.g. pink accent with white button text in dark mode)
29
+
30
+ ### Changed
31
+
32
+ - Renamed `PaginationHelper` to `PagyHelper` to match the underlying gem
33
+ - 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`)
34
+ - Scoped page params (e.g. `users_page`) are now reset when a Ransack search or sort submits, returning to page 1
35
+ - 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
36
+ - Navigation backdrop uses blur-only effect instead of a color overlay
37
+ - Navigation secondary border changed from `border-surface` to `border-border`
38
+ - Dummy app uses default Devise routes instead of custom path names
39
+ - Agent skill section moved higher in README and dummy app home page
40
+ - Documentation clarifies that `content_for` blocks must appear above the layout render call
41
+ - Color token documentation reorganised into Tier 1 (accent) and Tier 2 (full palette)
42
+
43
+ ### Removed
44
+
45
+ - `--backdrop` CSS custom property - backdrops now use blur-only styling
46
+
5
47
  ## [0.5.0] - 2026-04-19
6
48
 
7
49
  ### 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
 
@@ -119,8 +150,6 @@ Replace the defaults by placing files with the same names in `app/assets/images/
119
150
  | `icon_light.svg` | Favicon and header icon (light theme) |
120
151
  | `icon_dark.svg` | Favicon and header icon (dark theme) |
121
152
  | `apple_touch_icon.png` | Apple touch icon |
122
- | `panel_icon_light.svg` | Panel toggle button (light theme) |
123
- | `panel_icon_dark.svg` | Panel toggle button (dark theme) |
124
153
 
125
154
  layered-ui-rails uses two patterns for per-request overrides:
126
155
 
@@ -153,24 +182,6 @@ For per-request icons, set instance variables - the engine renders `<link>` and
153
182
 
154
183
  > **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
184
 
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
185
  ## Documentation
175
186
 
176
187
  An online version of the documentation is available at **[layered-ui-rails.layered.ai](https://layered-ui-rails.layered.ai)**.
@@ -215,4 +226,3 @@ Copyright 2026 LAYERED AI LIMITED (UK company number: 17056830). See [NOTICE](NO
215
226
  ## Trademarks
216
227
 
217
228
  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,9 +58,10 @@
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);
64
+ --button-primary-icon: var(--button-primary-text);
51
65
  --danger: 0 72% 38%;
52
66
  --danger-light: 0 100% 97%;
53
67
  --danger-text: 0 72% 35%;
@@ -58,13 +72,14 @@
58
72
  --warning-text: 48 96% 15%;
59
73
  --error-bg: 0 84% 75%;
60
74
  --error-text: 0 93% 12%;
61
- --backdrop: 0 0% 0%;
62
75
  --header-height: 63px;
63
76
  }
64
77
 
65
78
  .dark {
79
+ /* Tier 1 - Accent */
66
80
  --accent: 0 0% 100%;
67
81
  --accent-foreground: 0 0% 9%;
82
+ /* Tier 2 - Full palette */
68
83
  --background: 0 0% 0%;
69
84
  --foreground: 0 0% 89%;
70
85
  --foreground-muted: 0 0% 71%;
@@ -72,9 +87,10 @@
72
87
  --border-control: 0 0% 40%;
73
88
  --ring: 0 0% 89%;
74
89
  --surface: 0 0% 8%;
75
- --surface-active: 0 0% 16%;
90
+ --surface-highlighted: 0 0% 16%;
76
91
  --button-primary-bg: var(--accent);
77
92
  --button-primary-text: var(--accent-foreground);
93
+ --button-primary-icon: var(--button-primary-text);
78
94
  --danger: 0 85% 60%;
79
95
  --danger-light: 0 93% 15%;
80
96
  --danger-text: 0 85% 64%;
@@ -85,7 +101,6 @@
85
101
  --warning-text: 48 96% 80%;
86
102
  --error-bg: 0 93% 12%;
87
103
  --error-text: 0 84% 75%;
88
- --backdrop: 0 0% 0%;
89
104
  }
90
105
 
91
106
  /* Typography */
@@ -127,6 +142,14 @@
127
142
  @apply whitespace-pre overflow-x-auto
128
143
  text-foreground-muted;
129
144
  }
145
+
146
+ /* Scrollbars */
147
+
148
+ .l-ui-body,
149
+ .l-ui-body * {
150
+ scrollbar-width: thin;
151
+ scrollbar-color: hsl(var(--foreground-muted) / 0.3) transparent;
152
+ }
130
153
  }
131
154
 
132
155
  /* Theme */
@@ -142,9 +165,10 @@
142
165
  --color-border: hsl(var(--border));
143
166
  --color-border-control: hsl(var(--border-control));
144
167
  --color-surface: hsl(var(--surface));
145
- --color-surface-active: hsl(var(--surface-active));
168
+ --color-surface-highlighted: hsl(var(--surface-highlighted));
146
169
  --color-button-primary-bg: hsl(var(--button-primary-bg));
147
170
  --color-button-primary-text: hsl(var(--button-primary-text));
171
+ --color-button-primary-icon: hsl(var(--button-primary-icon));
148
172
  --color-danger: hsl(var(--danger));
149
173
  --color-danger-light: hsl(var(--danger-light));
150
174
  --color-danger-text: hsl(var(--danger-text));
@@ -155,7 +179,6 @@
155
179
  --color-warning-text: hsl(var(--warning-text));
156
180
  --color-error-bg: hsl(var(--error-bg));
157
181
  --color-error-text: hsl(var(--error-text));
158
- --color-backdrop: hsl(var(--backdrop));
159
182
  --color-ring: hsl(var(--ring));
160
183
  }
161
184
 
@@ -346,7 +369,7 @@
346
369
  .l-ui-markdown code {
347
370
  @apply px-1.5 py-0.5
348
371
  text-xs
349
- bg-surface-active
372
+ bg-surface-highlighted
350
373
  rounded;
351
374
  }
352
375
 
@@ -593,7 +616,7 @@
593
616
  .l-ui-backdrop--navigation {
594
617
  @apply fixed top-[var(--header-height)] left-0 right-0 bottom-0
595
618
  z-[45]
596
- bg-backdrop/50 backdrop-blur-xs
619
+ backdrop-blur-sm
597
620
  opacity-0 pointer-events-none
598
621
  transition-opacity duration-200;
599
622
  }
@@ -647,7 +670,7 @@
647
670
  .l-ui-navigation__secondary {
648
671
  @apply flex flex-col
649
672
  pl-4
650
- border-l-4 border-surface;
673
+ border-l-4 border-border;
651
674
 
652
675
  .l-ui-navigation__item--active {
653
676
  @apply bg-transparent
@@ -854,9 +877,9 @@ pre.l-ui-surface {
854
877
  }
855
878
  }
856
879
 
857
- .l-ui-surface--active {
880
+ .l-ui-surface--highlighted {
858
881
  @apply surface
859
- bg-surface-active;
882
+ bg-surface-highlighted;
860
883
  }
861
884
 
862
885
  .l-ui-surface--collapsible {
@@ -865,14 +888,14 @@ pre.l-ui-surface {
865
888
  bg-surface;
866
889
  }
867
890
 
868
- .l-ui-surface--collapsible-active {
891
+ .l-ui-surface--collapsible-highlighted {
869
892
  @apply surface
870
893
  p-0
871
- bg-surface-active;
894
+ bg-surface-highlighted;
872
895
  }
873
896
 
874
897
  .l-ui-surface--collapsible,
875
- .l-ui-surface--collapsible-active {
898
+ .l-ui-surface--collapsible-highlighted {
876
899
  &[open] > .l-ui-surface__summary .l-ui-surface__chevron {
877
900
  @apply rotate-90;
878
901
  }
@@ -1252,7 +1275,7 @@ pre.l-ui-surface {
1252
1275
 
1253
1276
  @utility pagination__item--active {
1254
1277
  @apply font-bold
1255
- bg-surface-active
1278
+ bg-surface-highlighted
1256
1279
  text-foreground
1257
1280
  pointer-events-none;
1258
1281
  }
@@ -1334,7 +1357,7 @@ pre.l-ui-surface {
1334
1357
 
1335
1358
  .l-ui-badge--default {
1336
1359
  @apply badge
1337
- bg-surface-active
1360
+ bg-surface-highlighted
1338
1361
  text-foreground-muted;
1339
1362
  }
1340
1363
 
@@ -1363,9 +1386,9 @@ pre.l-ui-surface {
1363
1386
  w-14 h-14
1364
1387
  z-40
1365
1388
  cursor-pointer
1366
- bg-button-primary-bg/60
1367
- text-button-primary-text
1368
- rounded-full shadow-lg backdrop-blur-xs
1389
+ bg-button-primary-bg
1390
+ text-button-primary-icon
1391
+ rounded-full
1369
1392
  focus-ring
1370
1393
  transition-all;
1371
1394
  touch-action: none;
@@ -1391,6 +1414,10 @@ pre.l-ui-surface {
1391
1414
  opacity-0 pointer-events-none;
1392
1415
  }
1393
1416
 
1417
+ .l-ui-panel__icon {
1418
+ @apply w-7 h-7;
1419
+ }
1420
+
1394
1421
  .l-ui-panel__icon--light {
1395
1422
  @apply block
1396
1423
  dark:invert-0;
@@ -1632,15 +1659,28 @@ pre.l-ui-surface {
1632
1659
  .l-ui-scroll-to-bottom {
1633
1660
  @apply
1634
1661
  sticky bottom-2 flex items-center justify-center
1635
- mx-auto -mt-9 h-9 w-9
1662
+ ml-auto mr-0 -mt-9 h-9 w-9
1636
1663
  rounded-full
1637
1664
  cursor-pointer
1638
- bg-button-primary-bg/60 text-button-primary-text shadow-lg backdrop-blur-xs
1665
+ bg-button-primary-bg text-button-primary-icon
1639
1666
  focus-ring
1640
1667
  opacity-0 pointer-events-none
1641
1668
  transition-opacity duration-200;
1642
1669
  }
1643
1670
 
1671
+ .l-ui-scroll-to-bottom::before {
1672
+ content: "";
1673
+ @apply block w-4 h-4 bg-button-primary-icon;
1674
+ -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M4 6l4 4 4-4'/%3E%3C/svg%3E");
1675
+ mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M4 6l4 4 4-4'/%3E%3C/svg%3E");
1676
+ -webkit-mask-size: contain;
1677
+ mask-size: contain;
1678
+ -webkit-mask-repeat: no-repeat;
1679
+ mask-repeat: no-repeat;
1680
+ -webkit-mask-position: center;
1681
+ mask-position: center;
1682
+ }
1683
+
1644
1684
  .l-ui-scroll-to-bottom[data-visible] {
1645
1685
  @apply
1646
1686
  opacity-100 pointer-events-auto;
@@ -1704,7 +1744,7 @@ pre.l-ui-surface {
1704
1744
  }
1705
1745
 
1706
1746
  .l-ui-modal::backdrop {
1707
- @apply bg-backdrop/50 backdrop-blur-sm;
1747
+ @apply backdrop-blur-sm;
1708
1748
  }
1709
1749
 
1710
1750
  .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,12 +60,20 @@
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
- <%= 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 }) %>
69
- <%= image_tag(@l_ui_panel_icon_dark_url.presence || "layered_ui/panel_icon_dark.svg", alt: "", class: "l-ui-icon--lg l-ui-panel__icon--dark", aria: { hidden: true }) %>
68
+ <% if @l_ui_panel_icon_light_url.present? %>
69
+ <%= image_tag(@l_ui_panel_icon_light_url, alt: "", class: "l-ui-icon--lg l-ui-panel__icon--light", aria: { hidden: true }) %>
70
+ <% else %>
71
+ <svg class="l-ui-panel__icon l-ui-panel__icon--light" width="24" height="24" viewBox="0 0 24 24" fill="none" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" stroke="currentColor" d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"/></svg>
72
+ <% end %>
73
+ <% if @l_ui_panel_icon_dark_url.present? %>
74
+ <%= image_tag(@l_ui_panel_icon_dark_url, alt: "", class: "l-ui-icon--lg l-ui-panel__icon--dark", aria: { hidden: true }) %>
75
+ <% else %>
76
+ <svg class="l-ui-panel__icon l-ui-panel__icon--dark" width="24" height="24" viewBox="0 0 24 24" fill="none" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" stroke="currentColor" d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"/></svg>
77
+ <% end %>
70
78
  </button>
71
79
  </div>
@@ -26,8 +26,10 @@ module Layered
26
26
  * This file is NOT overwritten by the install generator, so your
27
27
  * changes are preserved when you upgrade layered-ui-rails.
28
28
  *
29
- * Values are HSL channels: <hue> <saturation>% <lightness>%
30
- * Example: --accent: 220 80% 55%;
29
+ * Values are space-separated HSL channels (e.g. `220 80% 55%`,
30
+ * or `0 0% 100%` for white). Keywords and hex/rgb won't work -
31
+ * tokens are wrapped in `hsl()` when consumed. A converter such
32
+ * as https://colorpicker.dev/ can help translate hex/rgb values.
31
33
  */
32
34
 
33
35
  /* ----------------------------------------------------------------
@@ -35,6 +37,12 @@ module Layered
35
37
  *
36
38
  * Set a single brand color. Primary buttons, active tabs, and
37
39
  * active navigation items all inherit from these two variables.
40
+ *
41
+ * If your accent color needs a different text/icon color on
42
+ * buttons (e.g. a pink accent with white button text in dark
43
+ * mode), override --button-primary-text in Tier 2 below. To
44
+ * recolor only the icon (leaving button text unchanged),
45
+ * override --button-primary-icon instead.
38
46
  * ---------------------------------------------------------------- */
39
47
 
40
48
  :root {
@@ -62,9 +70,10 @@ module Layered
62
70
  --border-control: 0 0% 55%;
63
71
  --ring: 0 0% 13%;
64
72
  --surface: 0 0% 96%;
65
- --surface-active: 0 0% 91%;
73
+ --surface-highlighted: 0 0% 91%;
66
74
  --button-primary-bg: var(--accent);
67
75
  --button-primary-text: var(--accent-foreground);
76
+ --button-primary-icon: var(--button-primary-text);
68
77
  --danger: 0 72% 38%;
69
78
  --danger-light: 0 100% 97%;
70
79
  --danger-text: 0 72% 35%;
@@ -74,7 +83,6 @@ module Layered
74
83
  --warning-text: 48 96% 15%;
75
84
  --error-bg: 0 84% 75%;
76
85
  --error-text: 0 93% 12%;
77
- --backdrop: 0 0% 0%;
78
86
  }
79
87
 
80
88
  .dark {
@@ -85,9 +93,10 @@ module Layered
85
93
  --border-control: 0 0% 40%;
86
94
  --ring: 0 0% 89%;
87
95
  --surface: 0 0% 8%;
88
- --surface-active: 0 0% 16%;
96
+ --surface-highlighted: 0 0% 16%;
89
97
  --button-primary-bg: var(--accent);
90
98
  --button-primary-text: var(--accent-foreground);
99
+ --button-primary-icon: var(--button-primary-text);
91
100
  --danger: 0 85% 60%;
92
101
  --danger-light: 0 93% 15%;
93
102
  --danger-text: 0 85% 64%;
@@ -97,7 +106,6 @@ module Layered
97
106
  --warning-text: 48 96% 80%;
98
107
  --error-bg: 0 93% 12%;
99
108
  --error-text: 0 84% 75%;
100
- --backdrop: 0 0% 0%;
101
109
  }
102
110
  */
103
111
 
@@ -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.7.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.7.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