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 +4 -4
- data/.claude/skills/layered-ui-rails/SKILL.md +12 -5
- data/.claude/skills/layered-ui-rails/references/CONTROLLERS.md +15 -2
- data/.claude/skills/layered-ui-rails/references/CSS.md +150 -142
- data/AGENTS.md +1 -1
- data/CHANGELOG.md +26 -0
- data/README.md +34 -22
- data/app/assets/tailwind/layered/ui/styles.css +42 -22
- data/app/helpers/layered/ui/{pagination_helper.rb → pagy_helper.rb} +1 -1
- data/app/javascript/layered_ui/controllers/l_ui/panel_button_controller.js +24 -69
- data/app/javascript/layered_ui/controllers/l_ui/search_form_controller.js +23 -9
- data/app/views/layouts/layered_ui/_panel.html.erb +2 -2
- data/lib/generators/layered/ui/create_overrides_generator.rb +6 -4
- data/lib/layered/ui/engine.rb +1 -1
- data/lib/layered/ui/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8b3fa30f64b14c50db43ea23c4fbdb699903a7f4c2f957a8b22cd85772868a7b
|
|
4
|
+
data.tar.gz: 215e0d57d2403591bf31dd9d31ea05c281e5f36f83ebe822a4ae3aa902eabf0b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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-
|
|
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
|
+
<%= 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 %>
|
|
164
|
+
<%= l_ui_search_form(@users_q, url: users_path, fields: [:name, :email],
|
|
165
|
+
clear: true, turbo_frame: "users_collection") %>
|
|
166
|
+
<%= l_ui_table(@users, ..., query: @users_q, turbo_frame: "users_collection") %>
|
|
167
|
+
<%= l_ui_pagy(@users_pagy) %>
|
|
168
|
+
<% end %>
|
|
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
|
|
20
|
-
.l-ui-button--primary
|
|
21
|
-
.l-ui-button--danger
|
|
22
|
-
.l-ui-button--outline
|
|
23
|
-
.l-ui-button--outline-danger
|
|
24
|
-
.l-ui-button--icon
|
|
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
|
|
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
|
|
43
|
-
.l-ui-surface--
|
|
44
|
-
.l-ui-surface--sm
|
|
45
|
-
.l-ui-surface--collapsible
|
|
46
|
-
.l-ui-surface--collapsible-
|
|
47
|
-
.l-ui-surface__summary
|
|
48
|
-
.l-ui-surface__chevron
|
|
49
|
-
.l-ui-surface__content
|
|
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
|
|
56
|
-
.l-ui-form__group
|
|
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
|
|
59
|
-
.l-ui-form__errors
|
|
60
|
-
.l-ui-form__errors-list
|
|
61
|
-
.l-ui-form__actions
|
|
62
|
-
.l-ui-form__field-error
|
|
63
|
-
.l-ui-form__hint
|
|
64
|
-
.l-ui-form__required
|
|
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
|
|
67
|
-
.l-ui-label--checkbox
|
|
66
|
+
.l-ui-label Form label
|
|
67
|
+
.l-ui-label--checkbox Checkbox label variant
|
|
68
68
|
|
|
69
|
-
.l-ui-select
|
|
70
|
-
.l-ui-select-wrapper
|
|
69
|
+
.l-ui-select Select dropdown
|
|
70
|
+
.l-ui-select-wrapper Select wrapper (custom arrow)
|
|
71
71
|
|
|
72
|
-
.l-ui-search__inline
|
|
72
|
+
.l-ui-search__inline Inline search form layout
|
|
73
73
|
|
|
74
|
-
.l-ui-container--checkbox
|
|
75
|
-
.l-ui-radio__group
|
|
76
|
-
.l-ui-radio__item
|
|
77
|
-
.l-ui-radio__input
|
|
78
|
-
.l-ui-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
|
|
81
|
-
.l-ui-switch__input
|
|
82
|
-
.l-ui-switch__track
|
|
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
|
|
89
|
-
.l-ui-table__header
|
|
90
|
-
.l-ui-table__header-cell
|
|
91
|
-
.l-ui-table__header-cell--action
|
|
92
|
-
.l-ui-table__sort-link
|
|
93
|
-
.l-ui-table__sort-indicator
|
|
94
|
-
.l-ui-table__body
|
|
95
|
-
.l-ui-table__cell
|
|
96
|
-
.l-ui-table__cell--primary
|
|
97
|
-
.l-ui-table__cell--action
|
|
98
|
-
.l-ui-table__action--danger
|
|
99
|
-
.l-ui-container--table
|
|
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
|
|
132
|
-
.l-ui-notice--warning
|
|
133
|
-
.l-ui-notice--error
|
|
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
|
|
140
|
-
.l-ui-badge--rounded
|
|
141
|
-
.l-ui-badge--default
|
|
142
|
-
.l-ui-badge--success
|
|
143
|
-
.l-ui-badge--warning
|
|
144
|
-
.l-ui-badge--danger
|
|
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
|
|
151
|
-
.l-ui-tabs__tab
|
|
152
|
-
.l-ui-tabs__tab--active
|
|
153
|
-
.l-ui-tabs__panel
|
|
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
|
|
160
|
-
.l-ui-modal__header
|
|
161
|
-
.l-ui-modal__body
|
|
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
|
|
168
|
-
.l-ui-breadcrumbs__list
|
|
169
|
-
.l-ui-breadcrumbs__item
|
|
170
|
-
.l-ui-breadcrumbs__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
|
|
177
|
-
.l-ui-pagination__item
|
|
178
|
-
.l-ui-pagination__item--active
|
|
179
|
-
.l-ui-pagination__item--disabled
|
|
180
|
-
.l-ui-pagination__gap
|
|
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
|
|
187
|
-
.l-ui-container--navigation.open
|
|
188
|
-
.l-ui-backdrop--navigation
|
|
189
|
-
.l-ui-backdrop--navigation.open
|
|
190
|
-
.l-ui-navigation
|
|
191
|
-
.l-ui-navigation__links
|
|
192
|
-
.l-ui-navigation__item
|
|
193
|
-
.l-ui-navigation__item--active
|
|
194
|
-
.l-ui-navigation__secondary
|
|
195
|
-
.l-ui-navigation__user
|
|
196
|
-
.l-ui-navigation__user-name
|
|
197
|
-
.l-ui-navigation__user-email
|
|
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
|
|
204
|
-
.l-ui-header
|
|
205
|
-
.l-ui-header__icon
|
|
206
|
-
.l-ui-header__icon--light
|
|
207
|
-
.l-ui-header__icon--dark
|
|
208
|
-
.l-ui-header__logo
|
|
209
|
-
.l-ui-header__logo--light
|
|
210
|
-
.l-ui-header__logo--dark
|
|
211
|
-
.l-ui-theme-toggle
|
|
212
|
-
.l-ui-theme-toggle__icon--light
|
|
213
|
-
.l-ui-theme-toggle__icon--dark
|
|
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
|
|
220
|
-
.l-ui-container--panel.open
|
|
221
|
-
.l-ui-panel
|
|
222
|
-
.l-ui-panel__button
|
|
223
|
-
.l-ui-panel__button--dragging
|
|
224
|
-
.l-ui-panel__button--snapping
|
|
225
|
-
.l-ui-panel__icon--light
|
|
226
|
-
.l-ui-panel__icon--dark
|
|
227
|
-
.l-ui-panel__resize-handle
|
|
228
|
-
.l-ui-panel__header
|
|
229
|
-
.l-ui-panel__header-heading
|
|
230
|
-
.l-ui-panel__body
|
|
231
|
-
.l-ui-panel__input
|
|
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
|
|
238
|
-
.l-ui-conversation__messages
|
|
239
|
-
.l-ui-conversation__composer
|
|
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
|
|
241
|
+
.l-ui-conversation__separator Date separator
|
|
242
242
|
|
|
243
|
-
.l-ui-message
|
|
244
|
-
.l-ui-message--sent
|
|
245
|
-
.l-ui-message__avatar
|
|
246
|
-
.l-ui-message__bubble
|
|
247
|
-
.l-ui-message__author
|
|
248
|
-
.l-ui-message__body
|
|
249
|
-
.l-ui-message__footer
|
|
250
|
-
.l-ui-message__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
|
|
272
|
-
.l-ui-utility--mt-sm/md/lg/xl/2xl
|
|
273
|
-
.l-ui-utility--mb-0
|
|
274
|
-
.l-ui-sr-only
|
|
275
|
-
.l-ui-skip-link
|
|
276
|
-
.l-ui-list
|
|
277
|
-
.l-ui-container--grid
|
|
278
|
-
.l-ui-container--spread
|
|
279
|
-
.l-ui-container--pagy
|
|
280
|
-
.l-ui-scroll-lock
|
|
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
|
|
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
|
|
296
|
+
--foreground Primary text color
|
|
292
297
|
--foreground-muted Secondary/muted text
|
|
293
|
-
--border Default border
|
|
298
|
+
--border Default border color
|
|
294
299
|
--border-control Form control border
|
|
295
|
-
--ring Focus ring
|
|
300
|
+
--ring Focus ring color
|
|
296
301
|
--surface Card/surface background
|
|
297
|
-
--surface-
|
|
298
|
-
--
|
|
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
|
|
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`, `
|
|
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
|
+
[](https://opensource.org/licenses/Apache-2.0)
|
|
3
4
|
[](https://github.com/layered-ai-public/layered-ui-rails/actions/workflows/ci.yml)
|
|
4
5
|
[](https://www.w3.org/WAI/WCAG22/quickref/)
|
|
5
|
-
[](https://opensource.org/licenses/Apache-2.0)
|
|
6
6
|
[](https://www.layered.ai/)
|
|
7
7
|
[](https://github.com/layered-ai-public/layered-ui-rails)
|
|
8
8
|
[](https://discord.gg/aCGqz9Bx)
|
|
9
|
+
[](https://www.youtube.com/@UseLayeredAi)
|
|
10
|
+
[](https://x.com/UseLayeredAi)
|
|
11
|
+
[](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
|
|
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
|
-
/*
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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
|
-
|
|
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-
|
|
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--
|
|
877
|
+
.l-ui-surface--highlighted {
|
|
858
878
|
@apply surface
|
|
859
|
-
bg-surface-
|
|
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-
|
|
888
|
+
.l-ui-surface--collapsible-highlighted {
|
|
869
889
|
@apply surface
|
|
870
890
|
p-0
|
|
871
|
-
bg-surface-
|
|
891
|
+
bg-surface-highlighted;
|
|
872
892
|
}
|
|
873
893
|
|
|
874
894
|
.l-ui-surface--collapsible,
|
|
875
|
-
.l-ui-surface--collapsible-
|
|
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-
|
|
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-
|
|
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
|
|
1386
|
+
bg-button-primary-bg
|
|
1367
1387
|
text-button-primary-text
|
|
1368
|
-
rounded-full
|
|
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
|
-
|
|
1655
|
+
ml-auto mr-0 -mt-9 h-9 w-9
|
|
1636
1656
|
rounded-full
|
|
1637
1657
|
cursor-pointer
|
|
1638
|
-
bg-button-primary-bg
|
|
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
|
|
1727
|
+
@apply backdrop-blur-sm;
|
|
1708
1728
|
}
|
|
1709
1729
|
|
|
1710
1730
|
.l-ui-modal__header {
|
|
@@ -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.
|
|
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
|
-
|
|
72
|
-
|
|
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
|
-
|
|
185
|
-
const
|
|
186
|
-
storageSet("panelButtonPosition", JSON.stringify({ edge, top
|
|
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
|
-
|
|
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.
|
|
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
|
-
//
|
|
4
|
-
//
|
|
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)
|
|
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#
|
|
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-
|
|
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-
|
|
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
|
|
data/lib/layered/ui/engine.rb
CHANGED
|
@@ -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::
|
|
32
|
+
helper Layered::Ui::PagyHelper
|
|
33
33
|
helper Layered::Ui::TableHelper
|
|
34
34
|
helper Layered::Ui::FormHelper
|
|
35
35
|
helper Layered::Ui::RansackHelper
|
data/lib/layered/ui/version.rb
CHANGED
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.
|
|
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/
|
|
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
|