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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2346c306d3b5e355c1f61a51254d79701fe3a9ad2e8bc5817aa7311b0e024db0
4
- data.tar.gz: b01fdcd1dca34dd339ea4183933b5b6fcded78d3a2ee2c4c371ed92cd18e3214
3
+ metadata.gz: ffb58ab0d1712c81928bbf3685708f5571e98dd1af70484024219dc6a491f2cc
4
+ data.tar.gz: 495f951c875f7572f38b77e6739fcbd49856160a0d84e96cd76f701c97d1262d
5
5
  SHA512:
6
- metadata.gz: 29160e5b7d0dea492dd8cb2591b3797a564e2bef6bdabe7fa355ad79cde4dc516cf8537eef1c6298852a22dacf74126b5405bdbcd6fe63dd358adc37779e693e
7
- data.tar.gz: 8ef704e93632b9f1d60eb4f521f82c7b55e072ef507cea2267fbccdd6ecd76980e43bf6720c3ce0449df38dcfc3753d970167cf5a6b80af85cb868fb5a1169e1
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 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`, and `Layered::Ui.devise_scope` if the Devise mapping isn't `:user`. Helpers: `l_ui_devise_installed?`, `l_ui_user_signed_in?`.
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-[256px];
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-[256px]
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, as: resource_name, url: confirmation_path(resource_name), method: :post, class: 'l-ui-form l-ui-mt-3') do |f| %>
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), "aria-describedby": "#{resource_name}_email_error" %>
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, as: resource_name, url: password_path(resource_name), method: :put, class: 'l-ui-form l-ui-mt-3') do |f| %>
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", "aria-describedby": "#{resource_name}_password_error" %>
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", "aria-describedby": "#{resource_name}_password_confirmation_error" %>
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, as: resource_name, url: password_path(resource_name), method: :post, class: 'l-ui-form l-ui-mt-3') do |f| %>
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", "aria-describedby": "#{resource_name}_email_error" %>
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, as: resource_name, url: registration_path(resource_name), class: 'l-ui-form l-ui-mt-3') do |f| %>
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", "aria-describedby": "#{resource_name}_name_error" %>
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", "aria-describedby": "#{resource_name}_email_error" %>
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", "aria-describedby": "#{resource_name}_password_error" %>
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", "aria-describedby": "#{resource_name}_password_confirmation_error" %>
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, as: resource_name, url: session_path(resource_name), class: 'l-ui-form l-ui-mt-3') do |f| %>
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", "aria-describedby": "#{resource_name}_email_error" %>
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", "aria-describedby": "#{resource_name}_password_error" %>
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
- <%- if devise_mapping.recoverable? || devise_mapping.confirmable? || (devise_mapping.lockable? && resource_class.unlock_strategy_enabled?(:email)) %>
14
- <h2 class="l-ui-mt-6 l-ui-heading--section">
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 devise_mapping.recoverable? && controller_name != 'passwords' && controller_name != 'registrations' %>
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 devise_mapping.confirmable? && controller_name != 'confirmations' %>
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 devise_mapping.lockable? && resource_class.unlock_strategy_enabled?(:email) && controller_name != 'unlocks' %>
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, as: resource_name, url: unlock_path(resource_name), method: :post, class: 'l-ui-form l-ui-mt-3') do |f| %>
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", "aria-describedby": "#{resource_name}_email_error" %>
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 = [l_ui_field_error_id(record, attribute)]
11
- describedby.unshift(l_ui_field_hint_id(record, attribute)) if config[:hint]
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
- <div class="l-ui-navigation__user-name-and-email">
11
- <% if l_ui_current_user.respond_to?(:name) %>
12
- <div class="l-ui-navigation__user-name">
13
- <%= l_ui_current_user.name %>
14
- </div>
15
- <% end %>
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
- <% if l_ui_current_user.respond_to?(:email) %>
18
- <div class="l-ui-navigation__user-email">
19
- <%= l_ui_current_user.email %>
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
- </div>
23
- <% if l_ui_destroy_session_path %>
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>
@@ -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
@@ -1,5 +1,5 @@
1
1
  module Layered
2
2
  module Ui
3
- VERSION = "0.23.0"
3
+ VERSION = "0.24.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.23.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