decidim-core 0.32.0 → 0.32.1

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.
Files changed (52) hide show
  1. checksums.yaml +4 -4
  2. data/app/cells/decidim/comments_button_cell.rb +9 -1
  3. data/app/cells/decidim/data_consent/category.erb +20 -33
  4. data/app/cells/decidim/footer_topics/show.erb +5 -8
  5. data/app/cells/decidim/share_widget/qr_code_modal.erb +1 -1
  6. data/app/helpers/decidim/menu_helper.rb +0 -2
  7. data/app/models/decidim/user.rb +2 -2
  8. data/app/packs/src/decidim/controllers/accordion/accordion.test.js +196 -42
  9. data/app/packs/src/decidim/controllers/accordion/controller.js +93 -17
  10. data/app/packs/src/decidim/controllers/dropdown/controller.js +6 -0
  11. data/app/packs/src/decidim/controllers/dropdown/dropdown.test.js +28 -0
  12. data/app/packs/src/decidim/controllers/main_menu/controller.js +4 -0
  13. data/app/packs/src/decidim/controllers/main_menu/main_menu.test.js +23 -0
  14. data/app/packs/src/decidim/controllers/password_toggler/controller.js +22 -1
  15. data/app/packs/src/decidim/controllers/password_toggler/password_toggler.test.js +11 -20
  16. data/app/packs/src/decidim/direct_uploads/upload_modal.js +2 -1
  17. data/app/packs/src/decidim/map/controller.js +10 -1
  18. data/app/packs/src/decidim/refactor/moved/focus_guard.js +12 -12
  19. data/app/packs/src/decidim/refactor/moved/focus_guard.test.js +136 -0
  20. data/app/packs/stylesheets/decidim/_cards.scss +1 -1
  21. data/app/packs/stylesheets/decidim/_cookies.scss +13 -5
  22. data/app/packs/stylesheets/decidim/_documents.scss +1 -1
  23. data/app/packs/stylesheets/decidim/_footer.scss +33 -1
  24. data/app/packs/stylesheets/decidim/_participatory_spaces.scss +1 -1
  25. data/app/presenters/decidim/footer_menu_presenter.rb +2 -4
  26. data/app/resolvers/decidim/meta_image_url_resolver.rb +7 -0
  27. data/app/views/decidim/searches/_filters.html.erb +1 -1
  28. data/app/views/layouts/decidim/footer/_main.html.erb +1 -1
  29. data/app/views/layouts/decidim/footer/_main_links.html.erb +56 -28
  30. data/config/locales/ca-IT.yml +6 -0
  31. data/config/locales/ca.yml +6 -0
  32. data/config/locales/cs.yml +16 -0
  33. data/config/locales/en.yml +6 -0
  34. data/config/locales/es-MX.yml +6 -0
  35. data/config/locales/es-PY.yml +6 -0
  36. data/config/locales/es.yml +6 -0
  37. data/config/locales/eu.yml +5 -1
  38. data/config/locales/fi-plain.yml +6 -0
  39. data/config/locales/fi.yml +6 -0
  40. data/config/locales/fr-CA.yml +7 -0
  41. data/config/locales/fr.yml +7 -0
  42. data/config/locales/ja.yml +25 -0
  43. data/config/locales/pl.yml +2 -0
  44. data/db/migrate/20250217192438_convert_user_groups_into_users.rb +3 -2
  45. data/decidim-core.gemspec +4 -1
  46. data/lib/decidim/core/test/shared_examples/access_mode_restricted_participatory_spaces.rb +8 -5
  47. data/lib/decidim/core/test/shared_examples/access_mode_transparent_participatory_spaces.rb +14 -11
  48. data/lib/decidim/core/test/shared_examples/comments_examples.rb +26 -0
  49. data/lib/decidim/core/version.rb +1 -1
  50. data/lib/decidim/core.rb +1 -1
  51. data/lib/decidim/map/dynamic_map.rb +3 -1
  52. metadata +26 -5
@@ -70,6 +70,29 @@ describe("MainMenuController", () => {
70
70
  expect(closeSpy).toHaveBeenCalledWith("click", controller.handleCloseButtonClick)
71
71
  })
72
72
 
73
+ it("sets aria-expanded to false when missing", async () => {
74
+ document.body.innerHTML = `
75
+ <button data-controller="main-menu" data-target="main-menu-container" data-close-button="main-menu-close">
76
+ Menu
77
+ </button>
78
+ <div id="main-menu-container" aria-hidden="true"></div>
79
+ <button id="main-menu-close">Close</button>
80
+ `
81
+
82
+ application.stop()
83
+ application = Application.start()
84
+ application.register("main-menu", MainMenuController)
85
+
86
+ const newButton = document.querySelector('[data-controller="main-menu"]')
87
+ await new Promise((resolve) => {
88
+ setTimeout(() => {
89
+ resolve(application.getControllerForElementAndIdentifier(newButton, "main-menu"))
90
+ }, 0)
91
+ })
92
+
93
+ expect(newButton.getAttribute("aria-expanded")).toBe("false")
94
+ })
95
+
73
96
  it("returns early when menu container is missing", async () => {
74
97
  document.body.innerHTML = `
75
98
  <button data-controller="main-menu" data-target="missing-menu"></button>
@@ -55,6 +55,18 @@ export default class extends Controller {
55
55
  hide: icon("eye-off-line")
56
56
  }
57
57
 
58
+ const existingWrapper = this.input.closest(".input-group__password");
59
+ if (existingWrapper) {
60
+ const existingButton = existingWrapper.querySelector('button[type="button"]');
61
+ const existingStatusText = existingWrapper.querySelector('span.sr-only[aria-live="polite"]');
62
+ if (existingButton && existingStatusText) {
63
+ this.button = existingButton;
64
+ this.statusText = existingStatusText;
65
+ this.bindListeners();
66
+ return;
67
+ }
68
+ }
69
+
58
70
  this.init();
59
71
  }
60
72
 
@@ -81,8 +93,17 @@ export default class extends Controller {
81
93
  */
82
94
  init() {
83
95
  this.createControls();
96
+ this.bindListeners();
97
+ }
98
+
99
+ bindListeners() {
100
+ if (this.button && this.boundToggleVisibility) {
101
+ this.button.removeEventListener("click", this.boundToggleVisibility);
102
+ }
103
+ if (this.form && this.boundHidePassword) {
104
+ this.form.removeEventListener("submit", this.boundHidePassword);
105
+ }
84
106
 
85
- // Bind methods to maintain correct context
86
107
  this.boundToggleVisibility = this.toggleVisibility.bind(this);
87
108
  this.boundHidePassword = this.hidePassword.bind(this);
88
109
 
@@ -22,17 +22,7 @@ describe("PasswordToggler", () => {
22
22
  container = document.createElement("div");
23
23
  container.innerHTML = `
24
24
  <div data-controller="password-toggler" class="flex flex-row items-center gap-x-2" data-show-password="Show secret" data-hide-password="Hide Secret" data-hidden-password="Secret is hidden" data-shown-password="Secret is shown">
25
- <div class="input-group__password">
26
- <div class="input-group__password">
27
- <button type="button" aria-controls="token_162" aria-label="Show password">
28
- <svg width="0.75em" height="0.75em" role="img" aria-hidden="true">
29
- <title>eye-line</title>
30
- <use href="/decidim-packs/media/images/remixicon.symbol-e643e553623ffcd49f94.svg#ri-eye-line"></use>
31
- </svg>
32
- </button>
33
- <input type="password" id="token_162" value="AXXXXXXXX" class="w-full" autocomplete="off">
34
- </div>
35
- </div>
25
+ <input type="password" id="token_162" value="AXXXXXXXX" class="w-full" autocomplete="off">
36
26
  <div class="basis-1/4">
37
27
  <button type="button" class="button button__sm button__text-primary" data-clipboard-copy="#token_162">
38
28
  <span>Copy secret</span>
@@ -204,22 +194,23 @@ describe("PasswordToggler", () => {
204
194
  });
205
195
  });
206
196
 
207
- describe("integration with existing markup", () => {
208
- it("works with existing button structure", () => {
209
- const existingButton = passwordElement.querySelector('button[aria-controls="token_162"]');
210
- expect(existingButton).toBeTruthy();
197
+ describe("idempotency", () => {
198
+ it("does not duplicate controls on reconnect", () => {
199
+ controller.disconnect();
200
+ controller.connect();
211
201
 
212
- controller.init();
202
+ const wrappers = passwordElement.querySelectorAll(".input-group__password");
203
+ const buttons = passwordElement.querySelectorAll('button[aria-controls="token_162"]');
213
204
 
214
- // Should work alongside existing button
215
- expect(controller.button).toBeDefined();
216
- expect(controller.input.getAttribute("type")).toBe("password");
205
+ expect(wrappers.length).toBe(1);
206
+ expect(buttons.length).toBe(1);
217
207
  });
208
+ });
218
209
 
210
+ describe("integration with existing markup", () => {
219
211
  it("handles password input with existing value", () => {
220
212
  expect(controller.input.value).toBe("AXXXXXXXX");
221
213
 
222
- controller.init();
223
214
  controller.showPassword();
224
215
 
225
216
  expect(controller.input.getAttribute("type")).toBe("text");
@@ -253,7 +253,8 @@ export default class UploadModal {
253
253
  }
254
254
 
255
255
  setProgressBar(name, value) {
256
- const item = Array.from(this.uploadItems.querySelectorAll("[data-filename]")).find((el) => el.dataset.filename === name);
256
+ const items = Array.from(this.uploadItems.querySelectorAll("[data-filename]"));
257
+ const item = items.filter((el) => el.dataset.filename === name).pop();
257
258
  if (item) {
258
259
  item.querySelector("progress").value = value;
259
260
  }
@@ -29,7 +29,16 @@ export default class MapController {
29
29
  }
30
30
 
31
31
  load() {
32
- this.map = L.map(this.mapId);
32
+ const zoomControl = L.control.zoom({
33
+ zoomInTitle: this.config.zoomInText || "Zoom in",
34
+ zoomOutTitle: this.config.zoomOutText || "Zoom out"
35
+ });
36
+
37
+ this.map = L.map(this.mapId, {
38
+ zoomControl: false
39
+ });
40
+
41
+ this.map.addControl(zoomControl);
33
42
 
34
43
  this.map.scrollWheelZoom.disable();
35
44
 
@@ -5,14 +5,12 @@ const focusableDisableableNodes = ["BUTTON", "INPUT", "TEXTAREA", "SELECT"];
5
5
  export default class FocusGuard {
6
6
  constructor(container) {
7
7
  this.container = container;
8
- this.guardedElement = null;
9
- this.triggerElement = null;
8
+ this.stack = [];
10
9
  }
11
10
 
12
11
  trap(element, trigger) {
13
12
  this.enable();
14
- this.guardedElement = element;
15
- this.triggerElement = trigger;
13
+ this.stack.push({ element, trigger });
16
14
  }
17
15
 
18
16
  enable() {
@@ -45,14 +43,15 @@ export default class FocusGuard {
45
43
  }
46
44
 
47
45
  disable() {
48
- const guards = this.container.querySelectorAll(`:scope > .${focusGuardClass}`);
49
- guards.forEach((guard) => guard.remove());
46
+ const current = this.stack.pop();
50
47
 
51
- this.guardedElement = null;
48
+ if (this.stack.length === 0) {
49
+ const guards = this.container.querySelectorAll(`:scope > .${focusGuardClass}`);
50
+ guards.forEach((guard) => guard.remove());
51
+ }
52
52
 
53
- if (this.triggerElement) {
54
- this.triggerElement.focus();
55
- this.triggerElement = null;
53
+ if (current && current.trigger) {
54
+ current.trigger.focus();
56
55
  }
57
56
  }
58
57
 
@@ -67,12 +66,13 @@ export default class FocusGuard {
67
66
  };
68
67
 
69
68
  handleContainerFocus(guard) {
70
- if (!this.guardedElement) {
69
+ const current = this.stack[this.stack.length - 1];
70
+ if (!current || !current.element) {
71
71
  guard.blur();
72
72
  return;
73
73
  }
74
74
 
75
- const visibleNodes = Array.from(this.guardedElement.querySelectorAll("*")).filter((item) => {
75
+ const visibleNodes = Array.from(current.element.querySelectorAll("*")).filter((item) => {
76
76
  return this.isVisible(item);
77
77
  });
78
78
 
@@ -0,0 +1,136 @@
1
+ /* global jest */
2
+
3
+ import FocusGuard from "src/decidim/refactor/moved/focus_guard"
4
+
5
+ describe("FocusGuard", () => {
6
+ let focusGuard = null;
7
+
8
+ beforeEach(() => {
9
+ document.body.innerHTML = "";
10
+ focusGuard = new FocusGuard(document.body);
11
+ });
12
+
13
+ afterEach(() => {
14
+ document.body.innerHTML = "";
15
+ });
16
+
17
+ describe("single dialog", () => {
18
+ it("creates focus guards on trap", () => {
19
+ const dialog = document.createElement("div");
20
+ const trigger = document.createElement("button");
21
+ document.body.appendChild(dialog);
22
+ document.body.appendChild(trigger);
23
+
24
+ focusGuard.trap(dialog, trigger);
25
+ const guards = document.body.querySelectorAll(".focusguard");
26
+ expect(guards.length).toBe(2);
27
+ });
28
+
29
+ it("removes focus guards and returns focus on disable", () => {
30
+ const dialog = document.createElement("div");
31
+ const trigger = document.createElement("button");
32
+ document.body.appendChild(dialog);
33
+ document.body.appendChild(trigger);
34
+
35
+ focusGuard.trap(dialog, trigger);
36
+ focusGuard.disable();
37
+
38
+ const guards = document.body.querySelectorAll(".focusguard");
39
+ expect(guards.length).toBe(0);
40
+ expect(document.activeElement).toBe(trigger);
41
+ });
42
+ });
43
+
44
+ describe("nested dialogs", () => {
45
+ it("keeps guards when inner dialog is disabled", () => {
46
+ const outerDialog = document.createElement("div");
47
+ const outerTrigger = document.createElement("button");
48
+ const innerDialog = document.createElement("div");
49
+ const innerTrigger = document.createElement("button");
50
+
51
+ document.body.appendChild(outerDialog);
52
+ document.body.appendChild(outerTrigger);
53
+ document.body.appendChild(innerDialog);
54
+ document.body.appendChild(innerTrigger);
55
+
56
+ focusGuard.trap(outerDialog, outerTrigger);
57
+ focusGuard.trap(innerDialog, innerTrigger);
58
+
59
+ expect(document.body.querySelectorAll(".focusguard").length).toBe(2);
60
+
61
+ focusGuard.disable();
62
+
63
+ expect(document.body.querySelectorAll(".focusguard").length).toBe(2);
64
+ expect(document.activeElement).toBe(innerTrigger);
65
+ });
66
+
67
+ it("removes guards when outer dialog is disabled last", () => {
68
+ const outerDialog = document.createElement("div");
69
+ const outerTrigger = document.createElement("button");
70
+ const innerDialog = document.createElement("div");
71
+ const innerTrigger = document.createElement("button");
72
+
73
+ document.body.appendChild(outerDialog);
74
+ document.body.appendChild(outerTrigger);
75
+ document.body.appendChild(innerDialog);
76
+ document.body.appendChild(innerTrigger);
77
+
78
+ focusGuard.trap(outerDialog, outerTrigger);
79
+ focusGuard.trap(innerDialog, innerTrigger);
80
+
81
+ focusGuard.disable();
82
+ focusGuard.disable();
83
+
84
+ expect(document.body.querySelectorAll(".focusguard").length).toBe(0);
85
+ expect(document.activeElement).toBe(outerTrigger);
86
+ });
87
+
88
+ it("cycles focus to the current top dialog when a guard receives focus", () => {
89
+ jest.spyOn(focusGuard, "isVisible").mockReturnValue(true);
90
+
91
+ const outerDialog = document.createElement("div");
92
+ const outerInput = document.createElement("input");
93
+ outerDialog.appendChild(outerInput);
94
+
95
+ const innerDialog = document.createElement("div");
96
+ const innerInput = document.createElement("input");
97
+ innerDialog.appendChild(innerInput);
98
+
99
+ document.body.appendChild(outerDialog);
100
+ document.body.appendChild(innerDialog);
101
+
102
+ focusGuard.trap(outerDialog, null);
103
+ focusGuard.trap(innerDialog, null);
104
+
105
+ const endGuard = document.body.querySelector('.focusguard[data-position="end"]');
106
+ endGuard.focus();
107
+
108
+ expect(document.activeElement).toBe(innerInput);
109
+ });
110
+
111
+ it("cycles focus backward to the current top dialog when the start guard receives focus", () => {
112
+ jest.spyOn(focusGuard, "isVisible").mockReturnValue(true);
113
+
114
+ const outerDialog = document.createElement("div");
115
+ const outerInput = document.createElement("input");
116
+ outerDialog.appendChild(outerInput);
117
+
118
+ const innerDialog = document.createElement("div");
119
+ const innerInput = document.createElement("input");
120
+ const innerButton = document.createElement("button");
121
+ innerDialog.appendChild(innerInput);
122
+ innerDialog.appendChild(innerButton);
123
+
124
+ document.body.appendChild(outerDialog);
125
+ document.body.appendChild(innerDialog);
126
+
127
+ focusGuard.trap(outerDialog, null);
128
+ focusGuard.trap(innerDialog, null);
129
+
130
+ const startGuard = document.body.querySelector('.focusguard[data-position="start"]');
131
+ startGuard.focus();
132
+
133
+ expect(document.activeElement).toBe(innerButton);
134
+ });
135
+ });
136
+ });
@@ -55,7 +55,7 @@
55
55
  }
56
56
 
57
57
  &-content {
58
- @apply flex flex-col gap-2.5;
58
+ @apply flex flex-col gap-2.5 min-w-0 break-words;
59
59
  }
60
60
 
61
61
  &-title {
@@ -65,17 +65,25 @@
65
65
  &-panel {
66
66
  @apply divide-y divide-gray-3 [&>*]:py-4 [&>p]:text-gray-2;
67
67
 
68
- &__tr {
69
- @apply flex items-center justify-between rounded first-of-type:bg-background-4;
68
+ table {
69
+ @apply w-full border-collapse;
70
70
  }
71
71
 
72
- &__th {
73
- @apply w-1/4 p-2 bg-background-4 text-gray-2 font-bold text-sm;
72
+ thead tr {
73
+ @apply bg-background-4;
74
74
  }
75
75
 
76
- &__td {
76
+ th {
77
+ @apply w-1/4 p-2 text-gray-2 font-bold text-sm text-left;
78
+ }
79
+
80
+ td {
77
81
  @apply w-1/4 p-2 text-gray-2 text-xs;
78
82
  }
83
+
84
+ tbody tr {
85
+ @apply border-b border-gray-3;
86
+ }
79
87
  }
80
88
 
81
89
  &-toggle {
@@ -3,7 +3,7 @@
3
3
  @apply divide-y divide-background [&>*]:py-4 first:[&>*]:pt-0 last:[&>*]:pb-0;
4
4
 
5
5
  > * {
6
- @apply flex items-center justify-between gap-4;
6
+ @apply grid sm:flex items-center justify-between gap-4;
7
7
  }
8
8
  }
9
9
 
@@ -3,12 +3,44 @@ footer {
3
3
  @apply bg-gray-4;
4
4
 
5
5
  &__top {
6
- @apply hidden lg:flex flex-row gap-8 container py-10;
6
+ @apply flex flex-col lg:flex-row gap-0 lg:gap-8 container py-10;
7
7
 
8
8
  .prose a,
9
9
  .prose strong {
10
10
  @apply text-white;
11
11
  }
12
+
13
+ .footer__nav {
14
+ @apply border-b border-white/20 lg:border-0;
15
+
16
+ ul {
17
+ @apply space-y-4 break-inside-avoid mb-4;
18
+
19
+ li a {
20
+ @apply font-semibold underline decoration-1 underline-offset-1;
21
+ }
22
+ }
23
+
24
+ > [data-controls] {
25
+ @apply flex items-center justify-between w-full py-4 lg:pointer-events-none lg:cursor-default text-left text-white font-semibold;
26
+
27
+ svg {
28
+ @apply w-4 h-4 fill-current transition-transform lg:hidden;
29
+ }
30
+
31
+ &[aria-expanded="true"] svg {
32
+ @apply rotate-180;
33
+ }
34
+ }
35
+
36
+ [id*="panel"][aria-hidden="true"] {
37
+ @apply hidden lg:block;
38
+ }
39
+
40
+ [id*="panel"][aria-hidden="false"] {
41
+ @apply block;
42
+ }
43
+ }
12
44
  }
13
45
 
14
46
  &__down {
@@ -49,7 +49,7 @@
49
49
  }
50
50
 
51
51
  &__collection-content {
52
- @apply w-auto ml-6 my-4 pl-4 lg:pl-8 border-l border-[#D3D5D9];
52
+ @apply w-auto sm:ml-6 sm:my-4 sm:pl-4 lg:pl-8 sm:border-l sm:border-[#D3D5D9];
53
53
 
54
54
  p {
55
55
  @apply pt-4;
@@ -3,10 +3,8 @@
3
3
  module Decidim
4
4
  # A presenter to render footer menu
5
5
  class FooterMenuPresenter < MenuPresenter
6
- def render
7
- content_tag(:nav, :role => "navigation", "aria-label" => @options[:label]) do
8
- safe_join([content_tag(:h2, @options[:label], class: "h5 mb-4"), render_menu])
9
- end
6
+ def render_menu_items
7
+ render_menu
10
8
  end
11
9
  end
12
10
  end
@@ -79,6 +79,9 @@ module Decidim
79
79
  image_url = image_element["src"]
80
80
  next if image_url.blank?
81
81
 
82
+ blob = find_blob_by_id(image_url.split("/").last)
83
+ return blob if blob.present?
84
+
82
85
  blob = find_blob_by_key(image_url.split("/").second_to_last)
83
86
  return blob if blob.present?
84
87
  end
@@ -120,6 +123,10 @@ module Decidim
120
123
  ActiveStorage::Blob.find_signed(blob_key) if blob_key.present?
121
124
  end
122
125
 
126
+ def find_blob_by_id(blob_id)
127
+ ActiveStorage::Blob.find_by(id: blob_id) if blob_id.present?
128
+ end
129
+
123
130
  def blob_from_attached_file(file)
124
131
  return unless file.respond_to?(:attached?) && file.attached?
125
132
 
@@ -16,7 +16,7 @@
16
16
  </button>
17
17
  <% end %>
18
18
  </div>
19
- <button id="dropdown-trigger-search" data-controller="dropdown" data-target="dropdown-menu-search" data-auto-close="true" aria-expanded="true">
19
+ <button id="dropdown-trigger-search" data-controller="dropdown" data-target="dropdown-menu-search" data-auto-close="true">
20
20
  <%= content_tag :span, t("decidim.searches.filters_small_view.filter_by"), class: "#{"is-active" if params.dig(:filter, :with_resource_type) == nil}" %>
21
21
  <% @blocks.each do |elements| %>
22
22
  <% elements.each do |type, results| %>
@@ -3,7 +3,7 @@
3
3
  <div class="lg:w-1/3">
4
4
  <%= render partial: "layouts/decidim/footer/main_intro" %>
5
5
  </div>
6
- <div class="lg:w-2/3 grid grid-cols-1 gap-x-4 gap-y-10 lg:grid-cols-4 text-md text-white">
6
+ <div class="lg:w-2/3 grid grid-cols-1 gap-x-4 lg:grid-cols-4 text-md text-white">
7
7
  <%= render partial: "layouts/decidim/footer/main_links" %>
8
8
  </div>
9
9
  </div>
@@ -1,31 +1,59 @@
1
- <%= footer_menu.render %>
1
+ <div data-controller="accordion" data-multiselectable="true" class="contents">
2
+ <nav class="footer__nav" role="navigation" aria-label="<%= t("layouts.decidim.footer.decidim_title") %>">
3
+ <button type="button" id="panel-footer-menu-trigger" data-controls="panel-footer-menu" data-open-lg="true">
4
+ <span class="h4"><%= t("layouts.decidim.footer.decidim_title") %></span>
5
+ <%= icon "arrow-down-s-line" %>
6
+ </button>
7
+ <div id="panel-footer-menu" aria-hidden="true">
8
+ <%= footer_menu.render_menu_items %>
9
+ </div>
10
+ </nav>
2
11
 
3
- <nav role="navigation" aria-label="<%= t("layouts.decidim.user_menu.profile") %>">
4
- <h2 class="h4 mb-4"><%= t("layouts.decidim.user_menu.profile") %></h2>
5
- <ul class="space-y-4 break-inside-avoid">
6
- <% if current_user %>
7
- <li class="font-semibold underline"><%= link_to t("layouts.decidim.user_menu.configuration"), decidim.account_path %></li>
8
- <li class="font-semibold underline"><%= link_to t("layouts.decidim.user_menu.public_profile"), decidim.profile_path(current_user.nickname) %></li>
9
- <li class="font-semibold underline"><%= link_to t("layouts.decidim.user_menu.notifications"), decidim.notifications_path %></li>
10
- <li class="font-semibold underline"><%= link_to t("layouts.decidim.user_menu.conversations"), decidim.conversations_path %></li>
11
- <% else %>
12
- <% if current_organization.sign_up_enabled? %>
13
- <li class="font-semibold underline"><%= link_to t("layouts.decidim.footer.sign_up"), decidim.new_user_registration_path %></li>
14
- <% end %>
15
- <li class="font-semibold underline"><%= link_to t("layouts.decidim.footer.log_in"), decidim.new_user_session_path %></li>
16
- <% end %>
17
- </ul>
18
- </nav>
12
+ <nav class="footer__nav" role="navigation" aria-label="<%= t("layouts.decidim.user_menu.profile") %>">
13
+ <button type="button" id="panel-profile-trigger" data-controls="panel-profile" data-open-lg="true">
14
+ <span class="h4"><%= t("layouts.decidim.user_menu.profile") %></span>
15
+ <%= icon "arrow-down-s-line" %>
16
+ </button>
17
+ <div id="panel-profile" aria-hidden="true">
18
+ <ul>
19
+ <% if current_user %>
20
+ <li><%= link_to t("layouts.decidim.user_menu.configuration"), decidim.account_path %></li>
21
+ <li><%= link_to t("layouts.decidim.user_menu.public_profile"), decidim.profile_path(current_user.nickname) %></li>
22
+ <li><%= link_to t("layouts.decidim.user_menu.notifications"), decidim.notifications_path %></li>
23
+ <li><%= link_to t("layouts.decidim.user_menu.conversations"), decidim.conversations_path %></li>
24
+ <% else %>
25
+ <% if current_organization.sign_up_enabled? %>
26
+ <li><%= link_to t("layouts.decidim.footer.sign_up"), decidim.new_user_registration_path %></li>
27
+ <% end %>
28
+ <li><%= link_to t("layouts.decidim.footer.log_in"), decidim.new_user_session_path %></li>
29
+ <% end %>
30
+ </ul>
31
+ </div>
32
+ </nav>
19
33
 
20
- <nav role="navigation" aria-label="<%= t("layouts.decidim.footer.resources") %>">
21
- <h2 class="h4 mb-4"><%= t("layouts.decidim.footer.resources") %></h2>
22
- <ul class="space-y-4 break-inside-avoid">
23
- <li class="font-semibold underline"><%= link_to t("decidim.profiles.show.activity"), decidim.last_activities_path %></li>
24
- <% if Decidim.module_installed?(:meetings) %>
25
- <li class="font-semibold underline"><%= link_to t("decidim.pages.home.extended.meetings"), Decidim::Meetings::DirectoryEngine.routes.url_helpers.root_path %></li>
26
- <% end %>
27
- <li class="font-semibold underline"><%= link_to t("layouts.decidim.footer.open_data"), decidim.open_data_path %></li>
28
- </ul>
29
- </nav>
34
+ <nav class="footer__nav" role="navigation" aria-label="<%= t("layouts.decidim.footer.resources") %>">
35
+ <button type="button" id="panel-resources-trigger" data-controls="panel-resources" data-open-lg="true">
36
+ <span class="h4"><%= t("layouts.decidim.footer.resources") %></span>
37
+ <%= icon "arrow-down-s-line" %>
38
+ </button>
39
+ <div id="panel-resources" aria-hidden="true">
40
+ <ul>
41
+ <li><%= link_to t("decidim.profiles.show.activity"), decidim.last_activities_path %></li>
42
+ <% if Decidim.module_installed?(:meetings) %>
43
+ <li><%= link_to t("decidim.pages.home.extended.meetings"), Decidim::Meetings::DirectoryEngine.routes.url_helpers.root_path %></li>
44
+ <% end %>
45
+ <li><%= link_to t("layouts.decidim.footer.open_data"), decidim.open_data_path %></li>
46
+ </ul>
47
+ </div>
48
+ </nav>
30
49
 
31
- <%= cell("decidim/footer_topics", nil) %>
50
+ <nav class="footer__nav" role="navigation" aria-label="<%= t("layouts.decidim.footer.help") %>">
51
+ <button type="button" id="panel-help-trigger" data-controls="panel-help" data-open-lg="true">
52
+ <span class="h4"><%= t("layouts.decidim.footer.help") %></span>
53
+ <%= icon "arrow-down-s-line" %>
54
+ </button>
55
+ <div id="panel-help" aria-hidden="true">
56
+ <%= cell("decidim/footer_topics", nil) %>
57
+ </div>
58
+ </nav>
59
+ </div>
@@ -1205,6 +1205,8 @@ ca-IT:
1205
1205
  dynamic:
1206
1206
  screen_reader_explanation: El següent element és un mapa que presenta els components d'aquesta pàgina com a punts al mapa. L'element es pot fer servir amb un lector de pantalla però pot ser difícil d'entendre.
1207
1207
  skip_button: Saltar el mapa
1208
+ zoom_in: Ampliar
1209
+ zoom_out: Allunyar
1208
1210
  static:
1209
1211
  latlng_text: 'latitud: %{latitude}, longitud: %{longitude}'
1210
1212
  map_service_brand: OpenStreetMap
@@ -1546,6 +1548,10 @@ ca-IT:
1546
1548
  never_logged_in: mai ha iniciat sessió
1547
1549
  subject: Avís de compte inactiu
1548
1550
  participatory_space:
1551
+ labels:
1552
+ reference: Referència de l'espai de participació
1553
+ restricted: Espai de participació restringit
1554
+ transparent: Espai de participació transparent
1549
1555
  pages:
1550
1556
  user_profile:
1551
1557
  member_of:
@@ -1205,6 +1205,8 @@ ca:
1205
1205
  dynamic:
1206
1206
  screen_reader_explanation: El següent element és un mapa que presenta els components d'aquesta pàgina com a punts al mapa. L'element es pot fer servir amb un lector de pantalla però pot ser difícil d'entendre.
1207
1207
  skip_button: Saltar el mapa
1208
+ zoom_in: Ampliar
1209
+ zoom_out: Allunyar
1208
1210
  static:
1209
1211
  latlng_text: 'latitud: %{latitude}, longitud: %{longitude}'
1210
1212
  map_service_brand: OpenStreetMap
@@ -1546,6 +1548,10 @@ ca:
1546
1548
  never_logged_in: mai ha iniciat sessió
1547
1549
  subject: Avís de compte inactiu
1548
1550
  participatory_space:
1551
+ labels:
1552
+ reference: Referència de l'espai de participació
1553
+ restricted: Espai de participació restringit
1554
+ transparent: Espai de participació transparent
1549
1555
  pages:
1550
1556
  user_profile:
1551
1557
  member_of: