decidim-core 0.31.5 → 0.31.7

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 (160) hide show
  1. checksums.yaml +4 -4
  2. data/app/cells/decidim/amendable/amend_button_card/show.erb +2 -2
  3. data/app/cells/decidim/author_cell.rb +0 -4
  4. data/app/cells/decidim/card_metadata_cell.rb +1 -1
  5. data/app/cells/decidim/comments_button_cell.rb +9 -1
  6. data/app/cells/decidim/content_blocks/cta_settings_form/show.erb +1 -1
  7. data/app/cells/decidim/content_blocks/hero_settings_form/show.erb +2 -2
  8. data/app/cells/decidim/content_blocks/highlighted_content_banner_cell.rb +1 -1
  9. data/app/cells/decidim/content_blocks/highlighted_content_banner_settings_form/show.erb +1 -1
  10. data/app/cells/decidim/content_blocks/highlighted_participatory_spaces_cell.rb +1 -1
  11. data/app/cells/decidim/content_blocks/participatory_space_hero_settings_form/show.erb +1 -1
  12. data/app/cells/decidim/content_blocks/static_page/section_settings_form/show.erb +3 -1
  13. data/app/cells/decidim/content_blocks/static_page/summary_settings_form/show.erb +3 -1
  14. data/app/cells/decidim/content_blocks/static_page/two_pane_section_settings_form/show.erb +4 -2
  15. data/app/cells/decidim/data_consent/category.erb +20 -33
  16. data/app/cells/decidim/footer_topics/show.erb +5 -8
  17. data/app/cells/decidim/participatory_space_dropdown_metadata/links.erb +1 -1
  18. data/app/cells/decidim/resource_types_filter/show.erb +2 -2
  19. data/app/cells/decidim/share_widget/qr_code_modal.erb +1 -1
  20. data/app/commands/decidim/destroy_account.rb +12 -1
  21. data/app/commands/decidim/multiple_attachments_methods.rb +28 -27
  22. data/app/controllers/concerns/decidim/devise_controllers.rb +10 -0
  23. data/app/events/decidim/amendable/amendment_base_event.rb +7 -1
  24. data/app/helpers/decidim/menu_helper.rb +0 -2
  25. data/app/jobs/decidim/process_inactive_participant_job.rb +0 -7
  26. data/app/mailers/decidim/delete_user_mailer.rb +14 -0
  27. data/app/mailers/decidim/participants_account_mailer.rb +0 -16
  28. data/app/models/decidim/moderation.rb +1 -1
  29. data/app/models/decidim/participatory_space_private_user.rb +1 -1
  30. data/app/models/decidim/user.rb +2 -2
  31. data/app/models/decidim/user_base_entity.rb +17 -2
  32. data/app/models/decidim/user_moderation.rb +1 -1
  33. data/app/packs/src/decidim/controllers/accordion/accordion.test.js +196 -42
  34. data/app/packs/src/decidim/controllers/accordion/controller.js +93 -17
  35. data/app/packs/src/decidim/controllers/dropdown/controller.js +6 -0
  36. data/app/packs/src/decidim/controllers/dropdown/dropdown.test.js +28 -0
  37. data/app/packs/src/decidim/controllers/form_validator/form_validator.js +6 -6
  38. data/app/packs/src/decidim/controllers/form_validator/form_validator.test.js +23 -1
  39. data/app/packs/src/decidim/controllers/mention/controller.js +296 -140
  40. data/app/packs/src/decidim/controllers/mention/input_mentions.test.js +120 -457
  41. data/app/packs/src/decidim/controllers/multiple_mentions/controller.js +68 -32
  42. data/app/packs/src/decidim/controllers/multiple_mentions/input_multiple_mentions.test.js +30 -23
  43. data/app/packs/src/decidim/controllers/password_toggler/controller.js +22 -1
  44. data/app/packs/src/decidim/controllers/password_toggler/password_toggler.test.js +11 -20
  45. data/app/packs/src/decidim/direct_uploads/upload_field.js +4 -4
  46. data/app/packs/src/decidim/direct_uploads/upload_modal.js +12 -7
  47. data/app/packs/src/decidim/geocoding/reverse_geocoding.js +15 -5
  48. data/app/packs/src/decidim/geocoding/reverse_geocoding.test.js +197 -0
  49. data/app/packs/src/decidim/index.js +4 -3
  50. data/app/packs/src/decidim/map/controller.js +10 -1
  51. data/app/packs/src/decidim/refactor/moved/focus_guard.js +12 -12
  52. data/app/packs/src/decidim/refactor/moved/focus_guard.test.js +136 -0
  53. data/app/packs/src/decidim/sw/sw.js +1 -1
  54. data/app/packs/src/decidim/utilities/text.js +6 -6
  55. data/app/packs/stylesheets/decidim/_cards.scss +1 -1
  56. data/app/packs/stylesheets/decidim/_conversations.scss +14 -0
  57. data/app/packs/stylesheets/decidim/_cookies.scss +13 -5
  58. data/app/packs/stylesheets/decidim/_documents.scss +1 -1
  59. data/app/packs/stylesheets/decidim/_editor_suggestions.scss +49 -0
  60. data/app/packs/stylesheets/decidim/_floating_help.scss +1 -1
  61. data/app/packs/stylesheets/decidim/_footer.scss +33 -1
  62. data/app/packs/stylesheets/decidim/_participatory_spaces.scss +1 -1
  63. data/app/packs/stylesheets/decidim/_tom_select.scss +23 -0
  64. data/app/packs/stylesheets/decidim/application.scss +2 -0
  65. data/app/packs/stylesheets/decidim/editor.scss +2 -33
  66. data/app/packs/stylesheets/decidim/geocoding_addons.scss +10 -2
  67. data/app/presenters/decidim/footer_menu_presenter.rb +2 -4
  68. data/app/presenters/decidim/menu_item_presenter.rb +9 -3
  69. data/app/presenters/decidim/stats_presenter.rb +1 -1
  70. data/app/resolvers/decidim/meta_image_url_resolver.rb +7 -0
  71. data/app/uploaders/decidim/image_uploader.rb +1 -1
  72. data/app/views/decidim/delete_user_mailer/delete.html.erb +6 -0
  73. data/app/views/decidim/gamification/badges/index.html.erb +1 -1
  74. data/app/views/decidim/homepage/show.html.erb +1 -1
  75. data/app/views/decidim/last_activities/index.html.erb +1 -1
  76. data/app/views/decidim/messaging/conversations/_error_modal.html.erb +11 -19
  77. data/app/views/decidim/messaging/conversations/error.js.erb +12 -7
  78. data/app/views/decidim/newsletters/unsubscribe.html.erb +1 -1
  79. data/app/views/decidim/offline/show.html.erb +1 -1
  80. data/app/views/decidim/pages/index.html.erb +1 -1
  81. data/app/views/decidim/profiles/show.html.erb +1 -1
  82. data/app/views/decidim/searches/_filters.html.erb +1 -1
  83. data/app/views/decidim/shared/_resource_actions.html.erb +4 -4
  84. data/app/views/decidim/user_activities/index.html.erb +1 -1
  85. data/app/views/layouts/decidim/_wrapper.html.erb +2 -2
  86. data/app/views/layouts/decidim/footer/_main.html.erb +1 -1
  87. data/app/views/layouts/decidim/footer/_main_links.html.erb +56 -28
  88. data/app/views/layouts/decidim/header/_menu_breadcrumb_mobile_tablet.html.erb +1 -1
  89. data/app/views/layouts/decidim/shared/_layout_center.html.erb +1 -1
  90. data/app/views/layouts/decidim/shared/_layout_item.html.erb +1 -1
  91. data/app/views/layouts/decidim/shared/_layout_two_col.html.erb +1 -1
  92. data/config/locales/ar.yml +0 -25
  93. data/config/locales/bg.yml +0 -25
  94. data/config/locales/ca-IT.yml +37 -33
  95. data/config/locales/ca.yml +37 -33
  96. data/config/locales/cs.yml +12 -31
  97. data/config/locales/de.yml +5 -42
  98. data/config/locales/el.yml +0 -22
  99. data/config/locales/en.yml +36 -32
  100. data/config/locales/es-MX.yml +36 -32
  101. data/config/locales/es-PY.yml +36 -32
  102. data/config/locales/es.yml +36 -32
  103. data/config/locales/eu.yml +72 -70
  104. data/config/locales/fi-plain.yml +37 -32
  105. data/config/locales/fi.yml +37 -32
  106. data/config/locales/fr-CA.yml +16 -28
  107. data/config/locales/fr.yml +16 -28
  108. data/config/locales/gl.yml +0 -22
  109. data/config/locales/hu.yml +0 -22
  110. data/config/locales/id-ID.yml +0 -21
  111. data/config/locales/is-IS.yml +0 -6
  112. data/config/locales/it.yml +1 -20
  113. data/config/locales/ja.yml +52 -44
  114. data/config/locales/lb.yml +0 -23
  115. data/config/locales/lt.yml +0 -24
  116. data/config/locales/lv.yml +0 -21
  117. data/config/locales/nl.yml +0 -23
  118. data/config/locales/no.yml +0 -23
  119. data/config/locales/pl.yml +4 -27
  120. data/config/locales/pt-BR.yml +2 -40
  121. data/config/locales/pt.yml +0 -23
  122. data/config/locales/ro-RO.yml +1 -32
  123. data/config/locales/ru.yml +0 -8
  124. data/config/locales/sk.yml +1 -39
  125. data/config/locales/sv.yml +6 -36
  126. data/config/locales/tr-TR.yml +0 -24
  127. data/config/locales/uk.yml +0 -6
  128. data/config/locales/zh-CN.yml +0 -21
  129. data/config/locales/zh-TW.yml +0 -24
  130. data/db/migrate/20250217192438_convert_user_groups_into_users.rb +3 -2
  131. data/decidim-core.gemspec +3 -0
  132. data/lib/decidim/api/functions/user_entity_list.rb +2 -0
  133. data/lib/decidim/attachment_attributes.rb +58 -9
  134. data/lib/decidim/command.rb +1 -1
  135. data/lib/decidim/content_renderers/base_renderer.rb +112 -0
  136. data/lib/decidim/content_renderers/blob_renderer.rb +4 -7
  137. data/lib/decidim/content_renderers/mention_resource_renderer.rb +10 -6
  138. data/lib/decidim/content_renderers/resource_renderer.rb +16 -7
  139. data/lib/decidim/content_renderers/user_renderer.rb +11 -9
  140. data/lib/decidim/core/content_blocks/registry_manager.rb +4 -4
  141. data/lib/decidim/core/engine.rb +8 -0
  142. data/lib/decidim/core/test/factories.rb +3 -0
  143. data/lib/decidim/core/test/shared_examples/admin_resource_gallery_examples.rb +10 -10
  144. data/lib/decidim/core/test/shared_examples/amendable/amendment_event_examples.rb +2 -2
  145. data/lib/decidim/core/test/shared_examples/amendable/amendment_promoted_event_examples.rb +3 -3
  146. data/lib/decidim/core/test/shared_examples/comments_examples.rb +32 -6
  147. data/lib/decidim/core/test/shared_examples/has_space_in_mcell_examples.rb +2 -1
  148. data/lib/decidim/core/test/shared_examples/resource_liked_event_examples.rb +3 -3
  149. data/lib/decidim/core/version.rb +1 -1
  150. data/lib/decidim/core.rb +1 -1
  151. data/lib/decidim/map/autocomplete.rb +4 -3
  152. data/lib/decidim/map/dynamic_map.rb +3 -1
  153. data/lib/decidim/searchable.rb +5 -0
  154. data/lib/decidim/view_model.rb +1 -1
  155. data/lib/tasks/decidim_mailers_tasks.rake +31 -9
  156. metadata +32 -10
  157. data/app/commands/decidim/gallery_methods.rb +0 -107
  158. data/app/packs/src/decidim/vendor/tribute.js +0 -1890
  159. data/app/packs/stylesheets/decidim/_tribute.scss +0 -36
  160. data/app/views/decidim/participants_account_mailer/removal_notification.html.erb +0 -11
@@ -0,0 +1,197 @@
1
+ /* global global, jest */
2
+
3
+ import { initializeReverseGeocoding } from "src/decidim/geocoding/reverse_geocoding";
4
+
5
+ describe("reverseGeocoding", () => {
6
+ let container = null;
7
+ let button = null;
8
+ let input = null;
9
+ let label = null;
10
+ let mockGeolocation = null;
11
+ let mockPost = null;
12
+ let originalGeolocation = null;
13
+
14
+ beforeEach(() => {
15
+ document.body.innerHTML = "";
16
+
17
+ container = document.createElement("div");
18
+ container.innerHTML = `
19
+ <label>
20
+ <input id="test_address" type="text" />
21
+ <div class="geocoding__locate">
22
+ <button
23
+ class="geocoding__button"
24
+ type="button"
25
+ data-input="test_address"
26
+ data-error-no-location="Could not detect location."
27
+ data-error-unsupported="Device not supported."
28
+ data-locating-text="Locating..."
29
+ data-url="/locate">
30
+ Use my location
31
+ </button>
32
+ </div>
33
+ </label>
34
+ `;
35
+ document.body.appendChild(container);
36
+
37
+ button = container.querySelector(".geocoding__button");
38
+ input = container.querySelector("#test_address");
39
+ label = container.querySelector("label");
40
+
41
+ originalGeolocation = navigator.geolocation;
42
+ mockGeolocation = {
43
+ getCurrentPosition: jest.fn()
44
+ };
45
+ Reflect.defineProperty(global.navigator, "geolocation", {
46
+ value: mockGeolocation,
47
+ configurable: true
48
+ });
49
+
50
+ mockPost = jest.fn();
51
+ global.jQuery = jest.fn(() => ({ trigger: jest.fn() }));
52
+ global.jQuery.post = mockPost;
53
+ // eslint-disable-next-line id-length
54
+ global.$ = global.jQuery;
55
+
56
+ initializeReverseGeocoding();
57
+ });
58
+
59
+ afterEach(() => {
60
+ Reflect.defineProperty(global.navigator, "geolocation", {
61
+ value: originalGeolocation,
62
+ configurable: true
63
+ });
64
+ jest.restoreAllMocks();
65
+ });
66
+
67
+ describe("when clicking the button", () => {
68
+ it("shows the spinner and disables the button", () => {
69
+ mockGeolocation.getCurrentPosition.mockImplementation(() => {});
70
+
71
+ button.click();
72
+
73
+ expect(button.disabled).toBe(true);
74
+ expect(button.classList.contains("geocoding__button--locating")).toBe(true);
75
+ expect(button.querySelector(".geocoding__spinner")).not.toBeNull();
76
+ expect(button.textContent).toContain("Locating...");
77
+ });
78
+
79
+ it("uses custom locating text from data attribute", () => {
80
+ button.dataset.locatingText = "Finding you...";
81
+ mockGeolocation.getCurrentPosition.mockImplementation(() => {});
82
+
83
+ button.click();
84
+
85
+ expect(button.textContent).toContain("Finding you...");
86
+ });
87
+
88
+ it("stores original content for restoration", () => {
89
+ mockGeolocation.getCurrentPosition.mockImplementation(() => {});
90
+
91
+ button.click();
92
+
93
+ expect(button.dataset.originalContent).toContain("Use my location");
94
+ });
95
+ });
96
+
97
+ describe("when geolocation succeeds", () => {
98
+ it("restores the button after successful reverse geocoding", () => {
99
+ const mockPosition = {
100
+ coords: { latitude: 40.7128, longitude: -74.006 }
101
+ };
102
+ mockGeolocation.getCurrentPosition.mockImplementation((success) => {
103
+ success(mockPosition);
104
+ });
105
+
106
+ const mockDeferred = { fail: jest.fn() };
107
+ mockPost.mockImplementation((url, data, callback) => {
108
+ callback({ address: "New York, NY, USA" });
109
+ return mockDeferred;
110
+ });
111
+
112
+ button.click();
113
+
114
+ expect(button.disabled).toBe(false);
115
+ expect(button.classList.contains("geocoding__button--locating")).toBe(false);
116
+ expect(button.querySelector(".geocoding__spinner")).toBeNull();
117
+ expect(button.textContent).toContain("Use my location");
118
+ expect(input.value).toBe("New York, NY, USA");
119
+ });
120
+
121
+ it("restores the button when reverse geocoding fails", () => {
122
+ const mockPosition = {
123
+ coords: { latitude: 40.7128, longitude: -74.006 }
124
+ };
125
+ mockGeolocation.getCurrentPosition.mockImplementation((success) => {
126
+ success(mockPosition);
127
+ });
128
+
129
+ const mockDeferred = { fail: jest.fn((callback) => {
130
+ callback({}, "error", "Not Found");
131
+ }) };
132
+ mockPost.mockReturnValue(mockDeferred);
133
+
134
+ button.click();
135
+
136
+ expect(button.disabled).toBe(false);
137
+ expect(button.classList.contains("geocoding__button--locating")).toBe(false);
138
+ expect(button.textContent).toContain("Use my location");
139
+ });
140
+ });
141
+
142
+ describe("when geolocation fails", () => {
143
+ it("restores the button when user denies permission", () => {
144
+ const mockError = { message: "User denied geolocation" };
145
+ mockGeolocation.getCurrentPosition.mockImplementation((success, error) => {
146
+ error(mockError);
147
+ });
148
+
149
+ button.click();
150
+
151
+ expect(button.disabled).toBe(false);
152
+ expect(button.classList.contains("geocoding__button--locating")).toBe(false);
153
+ expect(button.textContent).toContain("Use my location");
154
+ });
155
+
156
+ it("shows an error message", () => {
157
+ const mockError = { message: "User denied geolocation" };
158
+ mockGeolocation.getCurrentPosition.mockImplementation((success, error) => {
159
+ error(mockError);
160
+ });
161
+
162
+ button.click();
163
+
164
+ const errorElement = label.querySelector(".form-error");
165
+ expect(errorElement).not.toBeNull();
166
+ expect(errorElement.textContent).toContain("Could not detect location.");
167
+ });
168
+ });
169
+
170
+ describe("when geolocation is not supported", () => {
171
+ it("shows an unsupported error and does not show spinner", () => {
172
+ Reflect.defineProperty(global.navigator, "geolocation", {
173
+ value: null,
174
+ configurable: true
175
+ });
176
+
177
+ button.click();
178
+
179
+ expect(button.disabled).toBe(false);
180
+ expect(button.classList.contains("geocoding__button--locating")).toBe(false);
181
+ const errorElement = label.querySelector(".form-error");
182
+ expect(errorElement).not.toBeNull();
183
+ expect(errorElement.textContent).toContain("Device not supported.");
184
+ });
185
+ });
186
+
187
+ describe("when button is already disabled", () => {
188
+ it("does not trigger geolocation again", () => {
189
+ button.disabled = true;
190
+ mockGeolocation.getCurrentPosition.mockImplementation(() => {});
191
+
192
+ button.click();
193
+
194
+ expect(mockGeolocation.getCurrentPosition).not.toHaveBeenCalled();
195
+ });
196
+ });
197
+ });
@@ -34,7 +34,6 @@ import "src/decidim/security/selfxss_warning"
34
34
  import "src/decidim/session_timeouter"
35
35
  import "src/decidim/results_listing"
36
36
  import "src/decidim/data_consent"
37
- import "src/decidim/sw"
38
37
  import "src/decidim/attachments"
39
38
  import "src/decidim/dropdown_menu"
40
39
  import "src/decidim/callout"
@@ -62,6 +61,7 @@ window.Decidim = window.Decidim || {
62
61
  announceForScreenReader
63
62
  };
64
63
 
64
+ window.createDialog = createDialog;
65
65
  window.morphdom = morphdom
66
66
 
67
67
  // eslint-disable-next-line max-params
@@ -156,6 +156,8 @@ document.addEventListener("turbo:load", () => {
156
156
  deprecationMessage(container, '.callout[role="alert"]', '.flash[role="alert"]'));
157
157
  document.querySelectorAll(".js-back-to-list").forEach((container) =>
158
158
  deprecationMessage(container, ".js-back-to-list", "NEEDS TO BE REMOVED"));
159
+ document.querySelectorAll("[data-toggler]").forEach((container) =>
160
+ deprecationMessage(container, "[data-toggler]", "Use the Stimulus toggle controller with hidden targets"));
159
161
  })
160
162
 
161
163
  // REDESIGN_PENDING: deprecated
@@ -236,8 +238,7 @@ const initializer = (element = document) => {
236
238
  document.dispatchEvent(new CustomEvent("decidim:loaded", { detail: { element } }));
237
239
  }
238
240
 
239
- // If no jQuery is used the Tribute feature used in comments to autocomplete
240
- // mentions stops working
241
+ // Keep this under jQuery ready to support components initialized on legacy templates
241
242
  $(() => initializer());
242
243
 
243
244
  // Run initializer action over the new DOM elements
@@ -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
+ });
@@ -53,7 +53,7 @@ self.addEventListener("notificationclick", (event) => {
53
53
  // avoid caching admin or users paths
54
54
  registerRoute(
55
55
  ({ url }) =>
56
- ["/admin/", "/users/"].some((path) => url.pathname.startsWith(path)),
56
+ ["admin", "users"].some((pathSegment) => url.pathname.split("/").includes(pathSegment)),
57
57
  new NetworkOnly()
58
58
  );
59
59
 
@@ -1,17 +1,17 @@
1
- export const escapeHtml = (text) => {
1
+ const escapeQuotes = (text) => {
2
2
  if (!text) {
3
3
  return "";
4
4
  }
5
5
 
6
- const el = document.createElement("div");
7
- el.appendChild(document.createTextNode(text));
8
- return el.innerHTML;
6
+ return text.replace(/"/g, "&quot;");
9
7
  }
10
8
 
11
- export const escapeQuotes = (text) => {
9
+ export const escapeHtml = (text) => {
12
10
  if (!text) {
13
11
  return "";
14
12
  }
15
13
 
16
- return text.replace(/"/g, "&quot;");
14
+ const el = document.createElement("div");
15
+ el.appendChild(document.createTextNode(text));
16
+ return escapeQuotes(el.innerHTML);
17
17
  }
@@ -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 {
@@ -88,6 +88,20 @@
88
88
  }
89
89
 
90
90
  &__modal {
91
+ &-error {
92
+ [data-dialog-closable] {
93
+ @apply top-6 right-6;
94
+ }
95
+
96
+ [data-dialog-container] {
97
+ @apply inline grid-cols-none;
98
+ }
99
+
100
+ [data-dialog-title] {
101
+ @apply col-span-2 md:col-span-1 md:col-start-2 grid grid-cols-[auto_1fr] items-start md:items-center gap-2 pb-4 text-left text-xl text-gray-2 md:border-b md:border-gray-3;
102
+ }
103
+ }
104
+
91
105
  &-results {
92
106
  @apply mb-20 flex flex-wrap gap-x-8 gap-y-4;
93
107
  }
@@ -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
 
@@ -0,0 +1,49 @@
1
+ .editor-suggestions-props {
2
+ --editor-suggestions-border-color: #000;
3
+ --editor-suggestions-background-color: #eee;
4
+ --editor-suggestions-selected-highlight-color: #ccc;
5
+ }
6
+
7
+ .editor-suggestions {
8
+ @apply editor-suggestions-props border-0 bg-[var(--editor-suggestions-background-color)] max-w-sm drop-shadow-md rounded-md;
9
+
10
+ &:hover {
11
+ .editor-suggestions-item[data-selected]:not(:hover) {
12
+ @apply bg-transparent;
13
+ }
14
+ }
15
+
16
+ .editor-suggestions-item {
17
+ @apply flex items-center gap-2 w-full text-left py-[0.375rem] px-1.5 border-0 rounded-none text-sm;
18
+
19
+ &:first-child {
20
+ @apply rounded-t-md;
21
+ }
22
+
23
+ &:last-child {
24
+ @apply rounded-b-md;
25
+ }
26
+
27
+ &:hover,
28
+ &[data-selected] {
29
+ @apply bg-[var(--editor-suggestions-selected-highlight-color)];
30
+ }
31
+
32
+ .editor-suggestions-item-avatar {
33
+ @apply rounded-full w-6 h-6 object-cover flex-none;
34
+ }
35
+
36
+ .editor-suggestions-item-label {
37
+ @apply truncate;
38
+ }
39
+ }
40
+
41
+ .editor-suggestions-item-disabled {
42
+ @apply cursor-default;
43
+
44
+ &:hover,
45
+ &[data-selected] {
46
+ @apply bg-transparent;
47
+ }
48
+ }
49
+ }
@@ -1,5 +1,5 @@
1
1
  [data-floating-help] {
2
- @apply absolute z-10 top-5 md:top-10 right-0 [&_button]:bg-background [&_button]:px-2 [&_button]:rounded-r-none [&_svg]:text-gray [&_svg]:fill-current;
2
+ @apply absolute z-10 top-20 md:top-10 right-0 [&_button]:bg-background [&_button]:px-2 [&_button]:rounded-r-none [&_svg]:text-gray [&_svg]:fill-current;
3
3
 
4
4
  &:hover {
5
5
  @apply [&_svg]:text-white [&_button]:text-white [&_button]:bg-secondary [&_button]:no-underline;
@@ -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;
@@ -0,0 +1,23 @@
1
+ @use "tom-select/dist/scss/tom-select";
2
+
3
+ .ts {
4
+ &-control {
5
+ @apply border-gray text-md min-h-[40px];
6
+
7
+ input {
8
+ @apply font-normal text-black text-md;
9
+ }
10
+ }
11
+
12
+ &-dropdown {
13
+ @apply text-md text-gray-2 font-normal;
14
+
15
+ .option img {
16
+ @apply inline;
17
+ }
18
+
19
+ .active {
20
+ @apply bg-gray-3;
21
+ }
22
+ }
23
+ }