layered-ui-rails 0.23.0 → 0.24.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 +10 -2
- data/.claude/skills/layered-ui-rails/references/CSS.md +5 -0
- data/.claude/skills/layered-ui-rails/references/HELPERS.md +4 -1
- data/CHANGELOG.md +19 -0
- data/app/assets/tailwind/layered_ui/engine.css +46 -4
- data/app/helpers/layered/ui/authentication_helper.rb +11 -0
- data/app/helpers/layered/ui/form_helper.rb +9 -0
- data/app/views/devise/confirmations/new.html.erb +2 -2
- data/app/views/devise/passwords/edit.html.erb +5 -5
- data/app/views/devise/passwords/new.html.erb +2 -2
- data/app/views/devise/registrations/edit.html.erb +86 -0
- data/app/views/devise/registrations/new.html.erb +7 -7
- data/app/views/devise/sessions/new.html.erb +3 -3
- data/app/views/devise/shared/_links.html.erb +9 -5
- data/app/views/devise/unlocks/new.html.erb +2 -2
- data/app/views/layered/ui/managed_resource/_field_input.html.erb +2 -3
- data/app/views/layouts/layered_ui/_navigation.html.erb +22 -14
- data/app/views/layouts/layered_ui/_user_name_and_email.html.erb +15 -0
- data/lib/layered/ui/engine.rb +8 -0
- data/lib/layered/ui/version.rb +1 -1
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ffb58ab0d1712c81928bbf3685708f5571e98dd1af70484024219dc6a491f2cc
|
|
4
|
+
data.tar.gz: 495f951c875f7572f38b77e6739fcbd49856160a0d84e96cd76f701c97d1262d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4f855f052b32a2b172a8bb390d1bd19680fccc568996a9b286c4edf9eec2665ad3e0c97b8ff686d760da9dd8f9306ef941cb1ed449f65b984ac01d139818230d
|
|
7
|
+
data.tar.gz: a0abb3efe8343044f8581ad5f1c476f3a822c7225daf3ec5638d4a61cb685d5db3886f7f884d461ab685a4b8ff67f68894fca25324665219051ebd439a6a428a
|
|
@@ -240,7 +240,7 @@ Override CSS custom properties after the engine import. Values are full CSS colo
|
|
|
240
240
|
}
|
|
241
241
|
```
|
|
242
242
|
|
|
243
|
-
Key tokens: `--accent`, `--accent-foreground`, `--background`, `--foreground`, `--foreground-muted`, `--border`, `--border-control`, `--surface`, `--surface-highlighted`, `--danger`, `--header-height`.
|
|
243
|
+
Key tokens: `--accent`, `--accent-foreground`, `--background`, `--foreground`, `--foreground-muted`, `--border`, `--border-control`, `--surface`, `--surface-highlighted`, `--danger`, `--header-height`, `--l-ui-navigation-width`.
|
|
244
244
|
|
|
245
245
|
## Asset overrides
|
|
246
246
|
|
|
@@ -250,9 +250,17 @@ Place files in `app/assets/images/layered_ui/` to replace engine defaults:
|
|
|
250
250
|
|
|
251
251
|
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).
|
|
252
252
|
|
|
253
|
+
## Ejecting a view
|
|
254
|
+
|
|
255
|
+
Any engine view can be replaced by copying it to the same path in the host application - `app/views/devise/registrations/edit.html.erb` overrides the Settings screen, `app/views/layouts/layered_ui/_navigation.html.erb` the sidebar, and so on. Application views resolve before engine views, so no configuration is needed.
|
|
256
|
+
|
|
257
|
+
Find the original to copy with `bundle info layered-ui-rails --path`. Unlike engine views, an ejected view lives in the host application, so it may use plain Tailwind utilities alongside the `l-ui-*` classes - the host's Tailwind build scans it.
|
|
258
|
+
|
|
259
|
+
Note that `bin/rails generate devise:views` copies Devise's own unstyled views into `app/views/devise/`, which overrides *every* styled view the engine provides, not just the one being changed. Eject the individual views instead.
|
|
260
|
+
|
|
253
261
|
## Optional integrations
|
|
254
262
|
|
|
255
|
-
- **Devise** - auto-detected. Provides styled auth views, header login/register buttons, sidebar user info
|
|
263
|
+
- **Devise** - auto-detected. Provides styled auth views (including an account settings screen at `registrations#edit`), header login/register buttons, and sidebar user info with a Settings/Logout menu. 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`, and `Layered::Ui.devise_scope` if the Devise mapping isn't `:user`. Helpers: `l_ui_devise_installed?`, `l_ui_user_signed_in?`.
|
|
256
264
|
- **Pagy** - auto-detected. Use `l_ui_pagy(@pagy)` for styled pagination.
|
|
257
265
|
- **Ransack** - auto-detected. Use `l_ui_search_form` and `l_ui_sort_link` for styled search and sortable tables.
|
|
258
266
|
|
|
@@ -398,6 +398,9 @@ The listbox and filtered-out options are hidden with the `hidden` attribute, so
|
|
|
398
398
|
.l-ui-navigation__section-chevron Toggle chevron (rotates when closed)
|
|
399
399
|
.l-ui-navigation__section-items Section body (ul, hidden when closed)
|
|
400
400
|
.l-ui-navigation__user User info section
|
|
401
|
+
.l-ui-navigation__user-button Unpadded full-width account menu trigger (pair with .l-ui-button)
|
|
402
|
+
.l-ui-navigation__user-chevron Upward chevron inside the account menu trigger
|
|
403
|
+
.l-ui-navigation__user-popover Account menu popover sized to the sidebar's inner column
|
|
401
404
|
.l-ui-navigation__user-name User name text
|
|
402
405
|
.l-ui-navigation__user-email User email text
|
|
403
406
|
```
|
|
@@ -488,6 +491,7 @@ Always combine the base block with one size modifier (e.g. `class="l-ui-icon l-u
|
|
|
488
491
|
```
|
|
489
492
|
.l-ui-mt-0/2/3/4/6/8 Margin top (fixed scale)
|
|
490
493
|
.l-ui-mb-0 Margin bottom zero
|
|
494
|
+
.l-ui-mb-4 Margin bottom (1rem)
|
|
491
495
|
.l-ui-sr-only Visually hidden, screen reader only
|
|
492
496
|
.l-ui-skip-link Accessibility skip link
|
|
493
497
|
.l-ui-list Styled list
|
|
@@ -533,6 +537,7 @@ Tier 2 - Full palette (override individually as needed):
|
|
|
533
537
|
--error-text Error text
|
|
534
538
|
--header-height Header height (default 63px)
|
|
535
539
|
--l-ui-gutter Page/header horizontal + bottom padding (default 1rem)
|
|
540
|
+
--l-ui-navigation-width Sidebar navigation width (default 256px)
|
|
536
541
|
--l-ui-contained-width Max width of contained content - header + .l-ui-page__contained (default 80rem)
|
|
537
542
|
```
|
|
538
543
|
|
|
@@ -283,6 +283,7 @@ Field options:
|
|
|
283
283
|
l_ui_normalise_field(record, config) # Normalise a raw field config into canonical form
|
|
284
284
|
l_ui_field_error_id(record, attribute) # Error element ID for aria-describedby
|
|
285
285
|
l_ui_field_hint_id(record, attribute) # Hint element ID for aria-describedby
|
|
286
|
+
l_ui_field_describedby(record, attribute, hint: false) # Space-joined hint + error IDs for aria-describedby
|
|
286
287
|
```
|
|
287
288
|
|
|
288
289
|
## Modal
|
|
@@ -514,12 +515,14 @@ Use these when overriding the header actions group with `:l_ui_header_actions` t
|
|
|
514
515
|
l_ui_user_signed_in? # Returns true if current user is present
|
|
515
516
|
l_ui_current_user # Returns the current user object
|
|
516
517
|
l_ui_devise_installed? # Returns true if Devise is loaded
|
|
518
|
+
l_ui_settings_screen? # Returns true on the Settings screen (Devise registrations#edit)
|
|
517
519
|
l_ui_new_registration_path # Devise registration path for the configured scope, or nil
|
|
520
|
+
l_ui_edit_registration_path # Devise account settings path for the configured scope, or nil
|
|
518
521
|
l_ui_new_session_path # Devise sign-in path for the configured scope, or nil
|
|
519
522
|
l_ui_destroy_session_path # Devise sign-out path for the configured scope, or nil
|
|
520
523
|
```
|
|
521
524
|
|
|
522
|
-
Configure the current user method, and the Devise scope used to build the login/register/logout paths (`new_registration_path`, `new_session_path`, `destroy_session_path`):
|
|
525
|
+
Configure the current user method, and the Devise scope used to build the login/register/settings/logout paths (`new_registration_path`, `edit_registration_path`, `new_session_path`, `destroy_session_path`):
|
|
523
526
|
|
|
524
527
|
```ruby
|
|
525
528
|
# config/initializers/layered_ui.rb
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,25 @@
|
|
|
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.24.0] - 2026-08-22
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Devise account settings: Devise's `registrations#edit` is styled as a Settings screen at `/users/edit`, with the account details under an "Account" tab. A signed-in user can change their email (and their name, if the model has one), set a new password, or cancel their account - all of it confirmed with the current password, as Devise expects. If your user model has a `name` attribute, permit it for `:account_update` as well as `:sign_up`. Link to the screen with `l_ui_edit_registration_path`, and use `l_ui_settings_screen?` to keep application chrome that a layout otherwise strips from Devise views.
|
|
10
|
+
- Sidebar account menu: the signed-in user's name and email in the sidebar are now a menu button opening a popover holding Settings and Logout, replacing the standalone logout button. The menu is sized to the sidebar's inner column so it lines up with the name and email, and only the entries whose Devise routes exist are rendered.
|
|
11
|
+
- Ejecting a view: any engine view can be replaced by copying it to the matching path in your application - `app/views/devise/registrations/edit.html.erb` takes over the Settings screen, `app/views/layouts/layered_ui/_navigation.html.erb` the sidebar - with no configuration. The engine's views are still prepended so they beat Devise's own, but your `app/views` is prepended back on top, restoring the precedence Rails gives you by default. Note that `bin/rails generate devise:views` generates *every* Devise view, so it replaces the whole styled set rather than the one you meant to change. See `UPGRADING.md`.
|
|
12
|
+
- `--l-ui-navigation-width`: the sidebar navigation width (default 256px), previously hard-coded, now overridable alongside the other layout tokens.
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
|
|
16
|
+
- Devise form fields are scoped to the mapping rather than the model, so an app whose mapping name differs from its model (`devise_for :members, class_name: "User"`) submits parameters under the name Devise looks for. Previously the fields were named after the model, and nothing the user typed reached the controller.
|
|
17
|
+
- Field hints on the Devise views carry an id and are referenced by their input's `aria-describedby` (assembled by the new `l_ui_field_describedby` helper), so a screen reader announces the password length requirement along with the field instead of leaving it unlinked.
|
|
18
|
+
- The "Need assistance?" heading on the Devise views is rendered only when at least one of the links beneath it is. Previously it appeared whenever the matching Devise modules were enabled, so a page whose only link was suppressed showed a heading over nothing.
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
|
|
22
|
+
- Logout has moved out of the sidebar into the account menu. A host that targeted the sidebar's `l-ui-button--outline-danger` logout button, in CSS or in a system test, will need to look inside `l-ui-navigation__user-popover` instead.
|
|
23
|
+
|
|
5
24
|
## [0.23.0] - 2026-08-22
|
|
6
25
|
|
|
7
26
|
### Added
|
|
@@ -105,6 +105,7 @@
|
|
|
105
105
|
--l-ui-hero-image: url('layered_ui/hero_background_light.webp');
|
|
106
106
|
--header-height: 63px;
|
|
107
107
|
--l-ui-gutter: 1rem;
|
|
108
|
+
--l-ui-navigation-width: 256px;
|
|
108
109
|
--l-ui-contained-width: 80rem;
|
|
109
110
|
}
|
|
110
111
|
|
|
@@ -322,6 +323,10 @@
|
|
|
322
323
|
@apply !mb-0;
|
|
323
324
|
}
|
|
324
325
|
|
|
326
|
+
.l-ui-mb-4 {
|
|
327
|
+
@apply !mb-4;
|
|
328
|
+
}
|
|
329
|
+
|
|
325
330
|
/* List */
|
|
326
331
|
|
|
327
332
|
.l-ui-list {
|
|
@@ -518,7 +523,7 @@
|
|
|
518
523
|
}
|
|
519
524
|
|
|
520
525
|
.l-ui-body--always-show-navigation .l-ui-page--with-navigation {
|
|
521
|
-
@apply md:ml-[
|
|
526
|
+
@apply md:ml-[var(--l-ui-navigation-width)];
|
|
522
527
|
}
|
|
523
528
|
|
|
524
529
|
/* Zero the page's top gutter so the first section sits flush at the viewport top, behind the
|
|
@@ -990,7 +995,7 @@
|
|
|
990
995
|
.l-ui-navigation-container {
|
|
991
996
|
@apply fixed top-[calc(var(--header-height)+1px)] left-0 bottom-0
|
|
992
997
|
z-50
|
|
993
|
-
w-[
|
|
998
|
+
w-[var(--l-ui-navigation-width)]
|
|
994
999
|
bg-background
|
|
995
1000
|
md:border-r md:border-border
|
|
996
1001
|
-translate-x-full
|
|
@@ -1162,9 +1167,34 @@
|
|
|
1162
1167
|
p-4;
|
|
1163
1168
|
}
|
|
1164
1169
|
|
|
1170
|
+
/* Fill the sidebar's inner column: the navigation width less the gutters on
|
|
1171
|
+
.l-ui-navigation__user, so the menu lines up with the name, email, and trigger.
|
|
1172
|
+
max-w-none overrides .l-ui-popover's own max-w-xs. */
|
|
1173
|
+
.l-ui-navigation__user-popover .l-ui-popover {
|
|
1174
|
+
@apply w-[calc(var(--l-ui-navigation-width)-var(--l-ui-gutter)*2)] max-w-none;
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
/* Points up: the menu opens above the trigger, which sits at the foot of the sidebar. */
|
|
1178
|
+
.l-ui-navigation__user-chevron {
|
|
1179
|
+
@apply inline-block
|
|
1180
|
+
w-3 h-3
|
|
1181
|
+
rotate-180;
|
|
1182
|
+
|
|
1183
|
+
background-color: currentColor;
|
|
1184
|
+
-webkit-mask-image: url('layered_ui/icon_chevron_down.svg');
|
|
1185
|
+
mask-image: url('layered_ui/icon_chevron_down.svg');
|
|
1186
|
+
-webkit-mask-repeat: no-repeat;
|
|
1187
|
+
mask-repeat: no-repeat;
|
|
1188
|
+
-webkit-mask-position: center;
|
|
1189
|
+
mask-position: center;
|
|
1190
|
+
-webkit-mask-size: contain;
|
|
1191
|
+
mask-size: contain;
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1165
1194
|
.l-ui-navigation__user-name-and-email {
|
|
1166
|
-
@apply flex flex-col
|
|
1167
|
-
gap-1
|
|
1195
|
+
@apply flex flex-1 flex-col min-w-0
|
|
1196
|
+
gap-1
|
|
1197
|
+
break-words;
|
|
1168
1198
|
}
|
|
1169
1199
|
|
|
1170
1200
|
.l-ui-navigation__user-name {
|
|
@@ -1271,6 +1301,18 @@
|
|
|
1271
1301
|
@apply md:hidden;
|
|
1272
1302
|
}
|
|
1273
1303
|
|
|
1304
|
+
/* The whole navigation user block is the account menu trigger: pair with
|
|
1305
|
+
.l-ui-button. Name and email on the left, chevron on the right. Drops the
|
|
1306
|
+
button's own padding so its contents sit flush with .l-ui-navigation__user's
|
|
1307
|
+
gutter, which is where the popover's borders land. Defined here rather than
|
|
1308
|
+
with the other navigation classes so it follows .l-ui-button in the cascade. */
|
|
1309
|
+
.l-ui-navigation__user-button {
|
|
1310
|
+
@apply w-full h-auto justify-between
|
|
1311
|
+
gap-2 p-0
|
|
1312
|
+
text-left
|
|
1313
|
+
rounded-sm;
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1274
1316
|
.l-ui-button--icon {
|
|
1275
1317
|
@apply min-w-[44px]
|
|
1276
1318
|
p-2
|
|
@@ -14,10 +14,21 @@ module Layered
|
|
|
14
14
|
l_ui_current_user.present?
|
|
15
15
|
end
|
|
16
16
|
|
|
17
|
+
# True on the account settings screen (Devise's registrations#edit). Layouts
|
|
18
|
+
# that strip app chrome from Devise views want an exception for it, as it is
|
|
19
|
+
# part of the signed-in app rather than the authentication flow.
|
|
20
|
+
def l_ui_settings_screen?
|
|
21
|
+
controller_name == "registrations" && action_name == "edit"
|
|
22
|
+
end
|
|
23
|
+
|
|
17
24
|
def l_ui_new_registration_path
|
|
18
25
|
l_ui_devise_path(:new, :registration)
|
|
19
26
|
end
|
|
20
27
|
|
|
28
|
+
def l_ui_edit_registration_path
|
|
29
|
+
l_ui_devise_path(:edit, :registration)
|
|
30
|
+
end
|
|
31
|
+
|
|
21
32
|
def l_ui_new_session_path
|
|
22
33
|
l_ui_devise_path(:new, :session)
|
|
23
34
|
end
|
|
@@ -65,6 +65,15 @@ module Layered
|
|
|
65
65
|
"#{record.model_name.param_key}_#{attribute}_hint"
|
|
66
66
|
end
|
|
67
67
|
|
|
68
|
+
# Space-joined ids for a field's aria-describedby: its hint (when the field
|
|
69
|
+
# has one) followed by its error message, which is always rendered.
|
|
70
|
+
def l_ui_field_describedby(record, attribute, hint: false)
|
|
71
|
+
ids = []
|
|
72
|
+
ids << l_ui_field_hint_id(record, attribute) if hint
|
|
73
|
+
ids << l_ui_field_error_id(record, attribute)
|
|
74
|
+
ids.join(" ")
|
|
75
|
+
end
|
|
76
|
+
|
|
68
77
|
private
|
|
69
78
|
|
|
70
79
|
def l_ui_field_type_for(model_class, attribute)
|
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
<div class="l-ui-page__narrow">
|
|
3
3
|
<h1>Resend confirmation instructions</h1>
|
|
4
4
|
|
|
5
|
-
<%= form_with(model: resource,
|
|
5
|
+
<%= form_with(model: resource, scope: resource_name, url: confirmation_path(resource_name), method: :post, class: 'l-ui-form l-ui-mt-3') do |f| %>
|
|
6
6
|
<%= render 'layered_ui/shared/form_errors', item: resource %>
|
|
7
7
|
|
|
8
8
|
<div class="l-ui-form__group">
|
|
9
9
|
<%= render 'layered_ui/shared/label', form: f, field: :email, required: true %>
|
|
10
|
-
<%= f.email_field :email, autofocus: true, autocomplete: "email", class: "l-ui-form__field", value: (resource.pending_reconfirmation? ? resource.unconfirmed_email : resource.email),
|
|
10
|
+
<%= f.email_field :email, autofocus: true, autocomplete: "email", class: "l-ui-form__field", value: (resource.pending_reconfirmation? ? resource.unconfirmed_email : resource.email), aria: { describedby: l_ui_field_describedby(resource, :email) } %>
|
|
11
11
|
<%= render 'layered_ui/shared/field_error', object: resource, field: :email %>
|
|
12
12
|
</div>
|
|
13
13
|
|
|
@@ -2,16 +2,16 @@
|
|
|
2
2
|
<div class="l-ui-page__narrow">
|
|
3
3
|
<h1>Change your password</h1>
|
|
4
4
|
|
|
5
|
-
<%= form_with(model: resource,
|
|
5
|
+
<%= form_with(model: resource, scope: resource_name, url: password_path(resource_name), method: :put, class: 'l-ui-form l-ui-mt-3') do |f| %>
|
|
6
6
|
<%= render 'layered_ui/shared/form_errors', item: resource %>
|
|
7
7
|
<%= f.hidden_field :reset_password_token %>
|
|
8
8
|
|
|
9
9
|
<div class="l-ui-form__group">
|
|
10
10
|
<%= render 'layered_ui/shared/label', form: f, field: :password, name: "New password", required: true %>
|
|
11
|
-
<%= f.password_field :password, autofocus: true, autocomplete: "new-password", class: "l-ui-form__field",
|
|
11
|
+
<%= f.password_field :password, autofocus: true, autocomplete: "new-password", class: "l-ui-form__field", aria: { describedby: l_ui_field_describedby(resource, :password, hint: @minimum_password_length.present?) } %>
|
|
12
12
|
<%= render 'layered_ui/shared/field_error', object: resource, field: :password %>
|
|
13
13
|
<% if @minimum_password_length %>
|
|
14
|
-
<div class="l-ui-form__hint">
|
|
14
|
+
<div class="l-ui-form__hint" id="<%= l_ui_field_hint_id(resource, :password) %>">
|
|
15
15
|
(<%= @minimum_password_length %> characters minimum)
|
|
16
16
|
</div>
|
|
17
17
|
<% end %>
|
|
@@ -19,10 +19,10 @@
|
|
|
19
19
|
|
|
20
20
|
<div class="l-ui-form__group">
|
|
21
21
|
<%= render 'layered_ui/shared/label', form: f, field: :password_confirmation, name: "Confirm new password", required: true %>
|
|
22
|
-
<%= f.password_field :password_confirmation, autocomplete: "new-password", class: "l-ui-form__field",
|
|
22
|
+
<%= f.password_field :password_confirmation, autocomplete: "new-password", class: "l-ui-form__field", aria: { describedby: l_ui_field_describedby(resource, :password_confirmation, hint: @minimum_password_length.present?) } %>
|
|
23
23
|
<%= render 'layered_ui/shared/field_error', object: resource, field: :password_confirmation %>
|
|
24
24
|
<% if @minimum_password_length %>
|
|
25
|
-
<div class="l-ui-form__hint">
|
|
25
|
+
<div class="l-ui-form__hint" id="<%= l_ui_field_hint_id(resource, :password_confirmation) %>">
|
|
26
26
|
Please enter the same password again
|
|
27
27
|
</div>
|
|
28
28
|
<% end %>
|
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
<div class="l-ui-page__narrow">
|
|
3
3
|
<h1>Forgot your password?</h1>
|
|
4
4
|
|
|
5
|
-
<%= form_with(model: resource,
|
|
5
|
+
<%= form_with(model: resource, scope: resource_name, url: password_path(resource_name), method: :post, class: 'l-ui-form l-ui-mt-3') do |f| %>
|
|
6
6
|
<%= render 'layered_ui/shared/form_errors', item: resource %>
|
|
7
7
|
|
|
8
8
|
<div class="l-ui-form__group">
|
|
9
9
|
<%= render 'layered_ui/shared/label', form: f, field: :email, required: true %>
|
|
10
|
-
<%= f.email_field :email, autofocus: true, autocomplete: "email", class: "l-ui-form__field",
|
|
10
|
+
<%= f.email_field :email, autofocus: true, autocomplete: "email", class: "l-ui-form__field", aria: { describedby: l_ui_field_describedby(resource, :email) } %>
|
|
11
11
|
<%= render 'layered_ui/shared/field_error', object: resource, field: :email %>
|
|
12
12
|
</div>
|
|
13
13
|
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
<h1>Settings</h1>
|
|
2
|
+
|
|
3
|
+
<div class="l-ui-tabs l-ui-mt-4" data-controller="l-ui--tabs">
|
|
4
|
+
<div class="l-ui-tabs__list" role="tablist" data-action="keydown->l-ui--tabs#keydown">
|
|
5
|
+
<button class="l-ui-tabs__tab l-ui-tabs__tab--active" role="tab"
|
|
6
|
+
data-action="l-ui--tabs#select"
|
|
7
|
+
data-l-ui--tabs-target="tab"
|
|
8
|
+
aria-selected="true" aria-controls="l-ui-settings-panel-account" id="l-ui-settings-tab-account">
|
|
9
|
+
Account
|
|
10
|
+
</button>
|
|
11
|
+
</div>
|
|
12
|
+
|
|
13
|
+
<div class="l-ui-tabs__panel" role="tabpanel"
|
|
14
|
+
data-l-ui--tabs-target="panel"
|
|
15
|
+
id="l-ui-settings-panel-account" aria-labelledby="l-ui-settings-tab-account">
|
|
16
|
+
<%= form_with(model: resource, scope: resource_name, url: registration_path(resource_name), method: :put, class: 'l-ui-form l-ui-mt-3') do |f| %>
|
|
17
|
+
<%= render 'layered_ui/shared/form_errors', item: resource %>
|
|
18
|
+
|
|
19
|
+
<% if resource.class.column_names.include?('name') %>
|
|
20
|
+
<div class="l-ui-form__group">
|
|
21
|
+
<%= render 'layered_ui/shared/label', form: f, field: :name, required: true %>
|
|
22
|
+
<%= f.text_field :name, autocomplete: "name", class: "l-ui-form__field", aria: { describedby: l_ui_field_describedby(resource, :name) } %>
|
|
23
|
+
<%= render 'layered_ui/shared/field_error', object: resource, field: :name %>
|
|
24
|
+
</div>
|
|
25
|
+
<% end %>
|
|
26
|
+
|
|
27
|
+
<div class="l-ui-form__group">
|
|
28
|
+
<%= render 'layered_ui/shared/label', form: f, field: :email, required: true %>
|
|
29
|
+
<%= f.email_field :email, autocomplete: "email", class: "l-ui-form__field", aria: { describedby: l_ui_field_describedby(resource, :email, hint: devise_mapping.confirmable? && resource.pending_reconfirmation?) } %>
|
|
30
|
+
<%= render 'layered_ui/shared/field_error', object: resource, field: :email %>
|
|
31
|
+
|
|
32
|
+
<% if devise_mapping.confirmable? && resource.pending_reconfirmation? %>
|
|
33
|
+
<div class="l-ui-form__hint" id="<%= l_ui_field_hint_id(resource, :email) %>">
|
|
34
|
+
Currently waiting confirmation for: <%= resource.unconfirmed_email %>
|
|
35
|
+
</div>
|
|
36
|
+
<% end %>
|
|
37
|
+
</div>
|
|
38
|
+
|
|
39
|
+
<h2 class="l-ui-mt-8 l-ui-mb-4 l-ui-heading--section">
|
|
40
|
+
Change your password
|
|
41
|
+
</h2>
|
|
42
|
+
|
|
43
|
+
<p>Leave these blank if you do not want to change your password.</p>
|
|
44
|
+
|
|
45
|
+
<div class="l-ui-form__group">
|
|
46
|
+
<%= render 'layered_ui/shared/label', form: f, field: :password, name: "New password", required: false %>
|
|
47
|
+
<%= f.password_field :password, autocomplete: "new-password", class: "l-ui-form__field", aria: { describedby: l_ui_field_describedby(resource, :password, hint: @minimum_password_length.present?) } %>
|
|
48
|
+
<%= render 'layered_ui/shared/field_error', object: resource, field: :password %>
|
|
49
|
+
<% if @minimum_password_length %>
|
|
50
|
+
<div class="l-ui-form__hint" id="<%= l_ui_field_hint_id(resource, :password) %>">
|
|
51
|
+
(<%= @minimum_password_length %> characters minimum)
|
|
52
|
+
</div>
|
|
53
|
+
<% end %>
|
|
54
|
+
</div>
|
|
55
|
+
|
|
56
|
+
<div class="l-ui-form__group">
|
|
57
|
+
<%= render 'layered_ui/shared/label', form: f, field: :password_confirmation, name: "Confirm new password", required: false %>
|
|
58
|
+
<%= f.password_field :password_confirmation, autocomplete: "new-password", class: "l-ui-form__field", aria: { describedby: l_ui_field_describedby(resource, :password_confirmation) } %>
|
|
59
|
+
<%= render 'layered_ui/shared/field_error', object: resource, field: :password_confirmation %>
|
|
60
|
+
</div>
|
|
61
|
+
|
|
62
|
+
<div class="l-ui-form__group l-ui-form__group--large-gap">
|
|
63
|
+
<%= render 'layered_ui/shared/label', form: f, field: :current_password, name: "Current password", required: true %>
|
|
64
|
+
<%= f.password_field :current_password, autocomplete: "current-password", class: "l-ui-form__field", aria: { describedby: l_ui_field_describedby(resource, :current_password, hint: true) } %>
|
|
65
|
+
<%= render 'layered_ui/shared/field_error', object: resource, field: :current_password %>
|
|
66
|
+
<div class="l-ui-form__hint" id="<%= l_ui_field_hint_id(resource, :current_password) %>">
|
|
67
|
+
We need your current password to confirm your changes
|
|
68
|
+
</div>
|
|
69
|
+
</div>
|
|
70
|
+
|
|
71
|
+
<div class="l-ui-form__actions">
|
|
72
|
+
<%= f.submit "Save changes", class: "l-ui-button l-ui-button--primary" %>
|
|
73
|
+
</div>
|
|
74
|
+
<% end %>
|
|
75
|
+
|
|
76
|
+
<h2 class="l-ui-mt-8 l-ui-mb-4 l-ui-heading--section">
|
|
77
|
+
Cancel your account
|
|
78
|
+
</h2>
|
|
79
|
+
|
|
80
|
+
<p>Cancelling your account is permanent and cannot be undone.</p>
|
|
81
|
+
|
|
82
|
+
<div class="l-ui-form__group">
|
|
83
|
+
<%= button_to "Cancel my account", registration_path(resource_name), method: :delete, class: "l-ui-button l-ui-button--outline-danger", form: { data: { turbo_confirm: "Are you sure? This cannot be undone." } } %>
|
|
84
|
+
</div>
|
|
85
|
+
</div>
|
|
86
|
+
</div>
|
|
@@ -2,29 +2,29 @@
|
|
|
2
2
|
<div class="l-ui-page__narrow">
|
|
3
3
|
<h1>Register</h1>
|
|
4
4
|
|
|
5
|
-
<%= form_with(model: resource,
|
|
5
|
+
<%= form_with(model: resource, scope: resource_name, url: registration_path(resource_name), class: 'l-ui-form l-ui-mt-3') do |f| %>
|
|
6
6
|
<%= render 'layered_ui/shared/form_errors', item: resource %>
|
|
7
7
|
|
|
8
8
|
<% if resource.class.column_names.include?('name') %>
|
|
9
9
|
<div class="l-ui-form__group">
|
|
10
10
|
<%= render 'layered_ui/shared/label', form: f, field: :name, required: true %>
|
|
11
|
-
<%= f.text_field :name, autocomplete: "name", class: "l-ui-form__field",
|
|
11
|
+
<%= f.text_field :name, autocomplete: "name", class: "l-ui-form__field", aria: { describedby: l_ui_field_describedby(resource, :name) } %>
|
|
12
12
|
<%= render 'layered_ui/shared/field_error', object: resource, field: :name %>
|
|
13
13
|
</div>
|
|
14
14
|
<% end %>
|
|
15
15
|
|
|
16
16
|
<div class="l-ui-form__group">
|
|
17
17
|
<%= render 'layered_ui/shared/label', form: f, field: :email, required: true %>
|
|
18
|
-
<%= f.email_field :email, autocomplete: "email", class: "l-ui-form__field",
|
|
18
|
+
<%= f.email_field :email, autocomplete: "email", class: "l-ui-form__field", aria: { describedby: l_ui_field_describedby(resource, :email) } %>
|
|
19
19
|
<%= render 'layered_ui/shared/field_error', object: resource, field: :email %>
|
|
20
20
|
</div>
|
|
21
21
|
|
|
22
22
|
<div class="l-ui-form__group">
|
|
23
23
|
<%= render 'layered_ui/shared/label', form: f, field: :password, required: true %>
|
|
24
|
-
<%= f.password_field :password, autocomplete: "new-password", class: "l-ui-form__field",
|
|
24
|
+
<%= f.password_field :password, autocomplete: "new-password", class: "l-ui-form__field", aria: { describedby: l_ui_field_describedby(resource, :password, hint: @minimum_password_length.present?) } %>
|
|
25
25
|
<%= render 'layered_ui/shared/field_error', object: resource, field: :password %>
|
|
26
26
|
<% if @minimum_password_length %>
|
|
27
|
-
<div class="l-ui-form__hint">
|
|
27
|
+
<div class="l-ui-form__hint" id="<%= l_ui_field_hint_id(resource, :password) %>">
|
|
28
28
|
(<%= @minimum_password_length %> characters minimum)
|
|
29
29
|
</div>
|
|
30
30
|
<% end %>
|
|
@@ -32,10 +32,10 @@
|
|
|
32
32
|
|
|
33
33
|
<div class="l-ui-form__group">
|
|
34
34
|
<%= render 'layered_ui/shared/label', form: f, field: :password_confirmation, name: "Confirm password", required: true %>
|
|
35
|
-
<%= f.password_field :password_confirmation, autocomplete: "new-password", class: "l-ui-form__field",
|
|
35
|
+
<%= f.password_field :password_confirmation, autocomplete: "new-password", class: "l-ui-form__field", aria: { describedby: l_ui_field_describedby(resource, :password_confirmation, hint: @minimum_password_length.present?) } %>
|
|
36
36
|
<%= render 'layered_ui/shared/field_error', object: resource, field: :password_confirmation %>
|
|
37
37
|
<% if @minimum_password_length %>
|
|
38
|
-
<div class="l-ui-form__hint">
|
|
38
|
+
<div class="l-ui-form__hint" id="<%= l_ui_field_hint_id(resource, :password_confirmation) %>">
|
|
39
39
|
Please enter the same password again
|
|
40
40
|
</div>
|
|
41
41
|
<% end %>
|
|
@@ -2,18 +2,18 @@
|
|
|
2
2
|
<div class="l-ui-page__narrow">
|
|
3
3
|
<h1>Login</h1>
|
|
4
4
|
|
|
5
|
-
<%= form_with(model: resource,
|
|
5
|
+
<%= form_with(model: resource, scope: resource_name, url: session_path(resource_name), class: 'l-ui-form l-ui-mt-3') do |f| %>
|
|
6
6
|
<%= render 'layered_ui/shared/form_errors', item: resource %>
|
|
7
7
|
|
|
8
8
|
<div class="l-ui-form__group">
|
|
9
9
|
<%= render 'layered_ui/shared/label', form: f, field: :email, required: true %>
|
|
10
|
-
<%= f.email_field :email, autofocus: true, autocomplete: "email", class: "l-ui-form__field",
|
|
10
|
+
<%= f.email_field :email, autofocus: true, autocomplete: "email", class: "l-ui-form__field", aria: { describedby: l_ui_field_describedby(resource, :email) } %>
|
|
11
11
|
<%= render 'layered_ui/shared/field_error', object: resource, field: :email %>
|
|
12
12
|
</div>
|
|
13
13
|
|
|
14
14
|
<div class="l-ui-form__group">
|
|
15
15
|
<%= render 'layered_ui/shared/label', form: f, field: :password, required: true %>
|
|
16
|
-
<%= f.password_field :password, autocomplete: "current-password", class: "l-ui-form__field",
|
|
16
|
+
<%= f.password_field :password, autocomplete: "current-password", class: "l-ui-form__field", aria: { describedby: l_ui_field_describedby(resource, :password) } %>
|
|
17
17
|
<%= render 'layered_ui/shared/field_error', object: resource, field: :password %>
|
|
18
18
|
</div>
|
|
19
19
|
|
|
@@ -10,21 +10,25 @@
|
|
|
10
10
|
</p>
|
|
11
11
|
<% end %>
|
|
12
12
|
|
|
13
|
-
<%-
|
|
14
|
-
|
|
13
|
+
<%- show_password_reset = devise_mapping.recoverable? && controller_name != 'passwords' && controller_name != 'registrations' %>
|
|
14
|
+
<%- show_confirmation = devise_mapping.confirmable? && controller_name != 'confirmations' %>
|
|
15
|
+
<%- show_unlock = devise_mapping.lockable? && resource_class.unlock_strategy_enabled?(:email) && controller_name != 'unlocks' %>
|
|
16
|
+
|
|
17
|
+
<%- if show_password_reset || show_confirmation || show_unlock %>
|
|
18
|
+
<h2 class="l-ui-mt-6 l-ui-mb-4 l-ui-heading--section">
|
|
15
19
|
Need assistance?
|
|
16
20
|
</h2>
|
|
17
21
|
<% end %>
|
|
18
22
|
|
|
19
|
-
<%- if
|
|
23
|
+
<%- if show_password_reset %>
|
|
20
24
|
<p><%= link_to "Reset your password", new_password_path(resource_name) %></p>
|
|
21
25
|
<% end %>
|
|
22
26
|
|
|
23
|
-
<%- if
|
|
27
|
+
<%- if show_confirmation %>
|
|
24
28
|
<p><%= link_to "Resend account confirmation email", new_confirmation_path(resource_name) %></p>
|
|
25
29
|
<% end %>
|
|
26
30
|
|
|
27
|
-
<%- if
|
|
31
|
+
<%- if show_unlock %>
|
|
28
32
|
<p><%= link_to "Resend account unlock email", new_unlock_path(resource_name) %></p>
|
|
29
33
|
<% end %>
|
|
30
34
|
|
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
<div class="l-ui-page__narrow">
|
|
3
3
|
<h1>Resend unlock instructions</h1>
|
|
4
4
|
|
|
5
|
-
<%= form_with(model: resource,
|
|
5
|
+
<%= form_with(model: resource, scope: resource_name, url: unlock_path(resource_name), method: :post, class: 'l-ui-form l-ui-mt-3') do |f| %>
|
|
6
6
|
<%= render 'layered_ui/shared/form_errors', item: resource %>
|
|
7
7
|
|
|
8
8
|
<div class="l-ui-form__group">
|
|
9
9
|
<%= render 'layered_ui/shared/label', form: f, field: :email, required: true %>
|
|
10
|
-
<%= f.email_field :email, autofocus: true, autocomplete: "email", class: "l-ui-form__field",
|
|
10
|
+
<%= f.email_field :email, autofocus: true, autocomplete: "email", class: "l-ui-form__field", aria: { describedby: l_ui_field_describedby(resource, :email) } %>
|
|
11
11
|
<%= render 'layered_ui/shared/field_error', object: resource, field: :email %>
|
|
12
12
|
</div>
|
|
13
13
|
|
|
@@ -7,9 +7,8 @@
|
|
|
7
7
|
base_opts[:required] = true if config[:required]
|
|
8
8
|
|
|
9
9
|
unless config[:as] == :hidden
|
|
10
|
-
describedby =
|
|
11
|
-
|
|
12
|
-
base_opts["aria-describedby"] = describedby.join(" ")
|
|
10
|
+
base_opts["aria-describedby"] =
|
|
11
|
+
l_ui_field_describedby(record, attribute, hint: config[:hint].present?)
|
|
13
12
|
end
|
|
14
13
|
|
|
15
14
|
field_class = ->(base) { token_list(base, extra_class) }
|
|
@@ -7,23 +7,31 @@
|
|
|
7
7
|
|
|
8
8
|
<% if l_ui_user_signed_in? %>
|
|
9
9
|
<div class="l-ui-navigation__user">
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
<%=
|
|
14
|
-
|
|
15
|
-
|
|
10
|
+
<% if l_ui_edit_registration_path || l_ui_destroy_session_path %>
|
|
11
|
+
<%= l_ui_popover(placement: :top, align: :end, container: { class: "l-ui-navigation__user-popover" }) do |p| %>
|
|
12
|
+
<% p.trigger(class: "l-ui-button l-ui-navigation__user-button") do %>
|
|
13
|
+
<%= render "layouts/layered_ui/user_name_and_email" %>
|
|
14
|
+
|
|
15
|
+
<span class="l-ui-sr-only">Account menu</span>
|
|
16
|
+
<span class="l-ui-navigation__user-chevron" aria-hidden="true"></span>
|
|
17
|
+
<% end %>
|
|
18
|
+
|
|
19
|
+
<div class="l-ui-popover__menu">
|
|
20
|
+
<% if l_ui_edit_registration_path %>
|
|
21
|
+
<%= link_to "Settings", l_ui_edit_registration_path, class: "l-ui-popover__menu-item" %>
|
|
22
|
+
<% end %>
|
|
23
|
+
|
|
24
|
+
<% if l_ui_edit_registration_path && l_ui_destroy_session_path %>
|
|
25
|
+
<hr class="l-ui-popover__menu-divider">
|
|
26
|
+
<% end %>
|
|
16
27
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
28
|
+
<% if l_ui_destroy_session_path %>
|
|
29
|
+
<%= button_to "Logout", l_ui_destroy_session_path, method: :delete, class: "l-ui-popover__menu-item l-ui-popover__menu-item--danger" %>
|
|
30
|
+
<% end %>
|
|
20
31
|
</div>
|
|
21
32
|
<% end %>
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
<div class="l-ui-mt-4">
|
|
25
|
-
<%= button_to "Logout", l_ui_destroy_session_path, method: :delete, class: "l-ui-button l-ui-button--outline-danger l-ui-button--full" %>
|
|
26
|
-
</div>
|
|
33
|
+
<% else %>
|
|
34
|
+
<%= render "layouts/layered_ui/user_name_and_email" %>
|
|
27
35
|
<% end %>
|
|
28
36
|
</div>
|
|
29
37
|
<% end %>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
<%# Spans throughout: this also renders inside the account menu trigger button, %>
|
|
2
|
+
<%# whose content model only allows phrasing content. %>
|
|
3
|
+
<span class="l-ui-navigation__user-name-and-email">
|
|
4
|
+
<% if l_ui_current_user.respond_to?(:name) %>
|
|
5
|
+
<span class="l-ui-navigation__user-name">
|
|
6
|
+
<%= l_ui_current_user.name %>
|
|
7
|
+
</span>
|
|
8
|
+
<% end %>
|
|
9
|
+
|
|
10
|
+
<% if l_ui_current_user.respond_to?(:email) %>
|
|
11
|
+
<span class="l-ui-navigation__user-email">
|
|
12
|
+
<%= l_ui_current_user.email %>
|
|
13
|
+
</span>
|
|
14
|
+
<% end %>
|
|
15
|
+
</span>
|
data/lib/layered/ui/engine.rb
CHANGED
|
@@ -43,13 +43,21 @@ module Layered
|
|
|
43
43
|
end
|
|
44
44
|
end
|
|
45
45
|
|
|
46
|
+
# The engine's views are prepended so they beat views shipped by other
|
|
47
|
+
# engines (Devise's own, in particular, which would otherwise win or lose
|
|
48
|
+
# on engine load order alone). The host application's app/views is then
|
|
49
|
+
# prepended back on top, so an application can still eject any engine view
|
|
50
|
+
# by copying it to the matching path - the ordering Rails gives you by
|
|
51
|
+
# default, which a bare prepend would take away.
|
|
46
52
|
initializer "layered-ui-rails.view_paths" do
|
|
47
53
|
ActiveSupport.on_load(:action_controller) do
|
|
48
54
|
prepend_view_path Engine.root.join("app/views")
|
|
55
|
+
prepend_view_path Rails.root.join("app/views")
|
|
49
56
|
end
|
|
50
57
|
|
|
51
58
|
ActiveSupport.on_load(:action_mailer) do
|
|
52
59
|
prepend_view_path Engine.root.join("app/views")
|
|
60
|
+
prepend_view_path Rails.root.join("app/views")
|
|
53
61
|
end
|
|
54
62
|
end
|
|
55
63
|
end
|
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.24.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- layered.ai
|
|
@@ -254,6 +254,7 @@ files:
|
|
|
254
254
|
- app/views/devise/mailer/unlock_instructions.html.erb
|
|
255
255
|
- app/views/devise/passwords/edit.html.erb
|
|
256
256
|
- app/views/devise/passwords/new.html.erb
|
|
257
|
+
- app/views/devise/registrations/edit.html.erb
|
|
257
258
|
- app/views/devise/registrations/new.html.erb
|
|
258
259
|
- app/views/devise/sessions/new.html.erb
|
|
259
260
|
- app/views/devise/shared/_links.html.erb
|
|
@@ -273,6 +274,7 @@ files:
|
|
|
273
274
|
- app/views/layouts/layered_ui/_notice.html.erb
|
|
274
275
|
- app/views/layouts/layered_ui/_panel.html.erb
|
|
275
276
|
- app/views/layouts/layered_ui/_theme_toggle.html.erb
|
|
277
|
+
- app/views/layouts/layered_ui/_user_name_and_email.html.erb
|
|
276
278
|
- app/views/layouts/layered_ui/application.html.erb
|
|
277
279
|
- config/importmap.rb
|
|
278
280
|
- lib/generators/layered/ui/create_overrides_generator.rb
|