super_settings 2.4.3 → 2.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. checksums.yaml +4 -4
  2. data/AGENTS.md +27 -0
  3. data/ARCHITECTURE.md +232 -0
  4. data/CHANGELOG.md +29 -0
  5. data/README.md +56 -6
  6. data/VERSION +1 -1
  7. data/app/helpers/super_settings/settings_helper.rb +5 -11
  8. data/app/locales/ar.json +44 -0
  9. data/app/locales/cs.json +44 -0
  10. data/app/locales/da.json +44 -0
  11. data/app/locales/de.json +44 -0
  12. data/app/locales/el.json +44 -0
  13. data/app/locales/en.json +55 -0
  14. data/app/locales/es.json +44 -0
  15. data/app/locales/fa.json +44 -0
  16. data/app/locales/fr.json +44 -0
  17. data/app/locales/gd.json +44 -0
  18. data/app/locales/he.json +44 -0
  19. data/app/locales/hi.json +44 -0
  20. data/app/locales/it.json +44 -0
  21. data/app/locales/ja.json +44 -0
  22. data/app/locales/ko.json +44 -0
  23. data/app/locales/lt.json +44 -0
  24. data/app/locales/nb.json +44 -0
  25. data/app/locales/nl.json +44 -0
  26. data/app/locales/pl.json +44 -0
  27. data/app/locales/pt-br.json +44 -0
  28. data/app/locales/pt.json +44 -0
  29. data/app/locales/ru.json +44 -0
  30. data/app/locales/sv.json +44 -0
  31. data/app/locales/ta.json +44 -0
  32. data/app/locales/tr.json +44 -0
  33. data/app/locales/uk.json +44 -0
  34. data/app/locales/ur.json +44 -0
  35. data/app/locales/vi.json +44 -0
  36. data/app/locales/zh-cn.json +44 -0
  37. data/app/locales/zh-tw.json +44 -0
  38. data/app/views/layouts/super_settings/settings.html.erb +4 -4
  39. data/config/routes.rb +2 -0
  40. data/lib/super_settings/application/api.js +37 -12
  41. data/lib/super_settings/application/helper.rb +78 -11
  42. data/lib/super_settings/application/index.html.erb +44 -44
  43. data/lib/super_settings/application/layout.html.erb +113 -6
  44. data/lib/super_settings/application/layout_styles.css +311 -21
  45. data/lib/super_settings/application/layout_vars.css.erb +15 -7
  46. data/lib/super_settings/application/scripts.js +72 -13
  47. data/lib/super_settings/application/style_vars.css.erb +108 -53
  48. data/lib/super_settings/application/styles.css +179 -84
  49. data/lib/super_settings/application.rb +21 -3
  50. data/lib/super_settings/configuration.rb +27 -5
  51. data/lib/super_settings/controller_actions.rb +39 -2
  52. data/lib/super_settings/http_client.rb +2 -1
  53. data/lib/super_settings/local_cache.rb +12 -16
  54. data/lib/super_settings/mini_i18n.rb +110 -0
  55. data/lib/super_settings/rack_application.rb +94 -7
  56. data/lib/super_settings/rest_api.rb +7 -3
  57. data/lib/super_settings/setting.rb +1 -1
  58. data/lib/super_settings/storage/active_record_storage/models.rb +7 -9
  59. data/lib/super_settings/storage/http_storage.rb +1 -2
  60. data/lib/super_settings/storage/json_storage.rb +1 -1
  61. data/lib/super_settings/storage/mongodb_storage.rb +2 -1
  62. data/lib/super_settings/storage/s3_storage.rb +1 -1
  63. data/lib/super_settings/storage.rb +1 -1
  64. data/lib/super_settings/version.rb +1 -1
  65. data/lib/super_settings.rb +16 -18
  66. data/super_settings.gemspec +2 -1
  67. metadata +35 -3
@@ -1,42 +1,94 @@
1
- * {
1
+ *, *::before, *::after {
2
2
  box-sizing: border-box;
3
+ margin: 0;
4
+ padding: 0;
3
5
  }
4
6
 
7
+ html { font-size: 16px; }
8
+
5
9
  body {
6
- font-family: sans-serif;
7
- font-size: 1rem;
8
- line-height: 1.5;
9
- text-align: left;
10
+ font-family: system-ui, -apple-system, sans-serif;
11
+ background: var(--background-color);
10
12
  color: var(--text-color);
11
- background-color: var(--background-color);
13
+ line-height: 1.6;
14
+ -webkit-font-smoothing: antialiased;
15
+ -moz-osx-font-smoothing: grayscale;
12
16
  margin: 0;
13
17
  padding: 0;
18
+ min-height: 100vh;
14
19
  }
15
20
 
16
- header {
17
- background-color: #666;
18
- color: white;
19
- padding: 1rem;
21
+ .super-settings-page-header {
22
+ border-bottom: 1px solid var(--border-color, #e2e5ea);
23
+ background: var(--header-background-color, #edeef1);
20
24
  }
21
25
 
22
- header h1.logo {
23
- font-size: 20px;
24
- margin: 0;
26
+ .super-settings-page-header-inner {
27
+ max-width: 1280px;
28
+ margin: 0 auto;
29
+ padding: 20px 24px;
30
+ display: flex;
31
+ align-items: center;
32
+ justify-content: space-between;
25
33
  }
26
34
 
27
- header h1.logo a {
28
- color: #ffffff;
35
+ .super-settings-page-header-brand {
36
+ display: flex;
37
+ align-items: center;
38
+ gap: 12px;
29
39
  text-decoration: none;
40
+ color: inherit;
41
+ }
42
+
43
+ a.super-settings-page-header-brand:visited {
44
+ color: inherit;
30
45
  }
31
- header h1.logo a:visited {
32
- color: #ffffff;
46
+
47
+ .super-settings-page-header-mark {
48
+ width: 32px;
49
+ height: 32px;
50
+ display: flex;
51
+ align-items: center;
52
+ justify-content: center;
53
+ color: #0d9488;
54
+ flex-shrink: 0;
55
+ }
56
+
57
+ .super-settings-page-header-mark svg {
58
+ width: 24px;
59
+ height: 24px;
33
60
  }
34
61
 
35
- header h1.logo img {
36
- max-height: 3rem;
62
+ .super-settings-page-header-mark img {
63
+ max-height: 32px;
64
+ max-width: 32px;
37
65
  vertical-align: middle;
38
- display: inline-block;
39
- margin-right: 1rem;
66
+ }
67
+
68
+ .super-settings-page-header-title {
69
+ font-family: system-ui, -apple-system, sans-serif;
70
+ font-size: 1.125rem;
71
+ font-weight: 700;
72
+ color: var(--text-color);
73
+ letter-spacing: -0.025em;
74
+ line-height: 1.2;
75
+ margin: 0;
76
+ }
77
+
78
+ .super-settings-page-header-subtitle {
79
+ font-family: system-ui, -apple-system, sans-serif;
80
+ font-size: 0.8125rem;
81
+ font-weight: 400;
82
+ color: #9ca3af;
83
+ line-height: 1.3;
84
+ margin: 0;
85
+ letter-spacing: 0.01em;
86
+ }
87
+
88
+ .super-settings-container {
89
+ max-width: 1280px;
90
+ margin: 0 auto;
91
+ padding: 0 24px;
40
92
  }
41
93
 
42
94
  a {
@@ -47,3 +99,241 @@ a {
47
99
  a:visited {
48
100
  color: var(--link-color);
49
101
  }
102
+
103
+ /* ══════════════════════════════════════════
104
+ FOOTER
105
+ ══════════════════════════════════════════ */
106
+
107
+ .super-settings-page-footer {
108
+ max-width: 1280px;
109
+ margin: 0 auto;
110
+ padding: 0 24px 24px 24px;
111
+ display: flex;
112
+ justify-content: flex-end;
113
+ }
114
+
115
+ .super-settings-theme-toggle {
116
+ display: flex;
117
+ align-items: center;
118
+ justify-content: center;
119
+ width: 36px;
120
+ height: 36px;
121
+ border: 1px solid transparent;
122
+ border-radius: 8px;
123
+ background: transparent;
124
+ color: #9ca3af;
125
+ cursor: pointer;
126
+ flex-shrink: 0;
127
+ transition: color 0.2s, border-color 0.2s;
128
+ }
129
+
130
+ .super-settings-theme-toggle:hover {
131
+ color: var(--link-color);
132
+ border-color: var(--link-color);
133
+ }
134
+
135
+ .super-settings-theme-icon {
136
+ width: 18px;
137
+ height: 18px;
138
+ }
139
+
140
+ .super-settings-theme-icon-moon {
141
+ display: none;
142
+ }
143
+
144
+ [data-theme="dark"] .super-settings-theme-icon-sun {
145
+ display: none;
146
+ }
147
+
148
+ [data-theme="dark"] .super-settings-theme-icon-moon {
149
+ display: block;
150
+ }
151
+
152
+ /* ══════════════════════════════════════════
153
+ LANGUAGE MENU (in footer)
154
+ ══════════════════════════════════════════ */
155
+
156
+ .super-settings-language-menu {
157
+ position: relative;
158
+ display: block;
159
+ flex-shrink: 0;
160
+ }
161
+
162
+ .super-settings-language-menu > summary {
163
+ list-style: none;
164
+ }
165
+
166
+ .super-settings-language-menu > summary::-webkit-details-marker {
167
+ display: none;
168
+ }
169
+
170
+ .super-settings-language-trigger {
171
+ display: flex;
172
+ align-items: center;
173
+ gap: 8px;
174
+ padding: 6px 12px;
175
+ border-radius: 6px;
176
+ border: 1px solid var(--border-color, #ccc);
177
+ background: var(--background-color, #fff);
178
+ color: var(--text-color);
179
+ cursor: pointer;
180
+ transition: border-color 0.15s ease, color 0.15s ease;
181
+ max-width: 180px;
182
+ white-space: nowrap;
183
+ }
184
+
185
+ .super-settings-language-trigger:hover {
186
+ border-color: var(--link-color);
187
+ color: var(--link-color);
188
+ }
189
+
190
+ .super-settings-language-trigger:focus-visible {
191
+ outline: 2px solid var(--link-color);
192
+ outline-offset: 2px;
193
+ }
194
+
195
+ .super-settings-language-trigger-icon {
196
+ width: 16px;
197
+ height: 16px;
198
+ display: inline-flex;
199
+ align-items: center;
200
+ justify-content: center;
201
+ flex-shrink: 0;
202
+ opacity: 0.7;
203
+ }
204
+
205
+ .super-settings-language-trigger-icon svg {
206
+ width: 13px;
207
+ height: 13px;
208
+ }
209
+
210
+ .super-settings-language-trigger-current {
211
+ flex: 1;
212
+ min-width: 0;
213
+ font-size: 0.75rem;
214
+ font-weight: 500;
215
+ overflow: hidden;
216
+ text-overflow: ellipsis;
217
+ white-space: nowrap;
218
+ }
219
+
220
+ .super-settings-language-trigger-caret {
221
+ width: 12px;
222
+ height: 12px;
223
+ display: inline-flex;
224
+ align-items: center;
225
+ justify-content: center;
226
+ color: inherit;
227
+ opacity: 0.5;
228
+ transition: transform 0.15s ease, opacity 0.15s ease;
229
+ }
230
+
231
+ .super-settings-language-trigger-caret svg {
232
+ width: 100%;
233
+ height: 100%;
234
+ }
235
+
236
+ .super-settings-language-menu[open] .super-settings-language-trigger {
237
+ border-color: var(--link-color);
238
+ color: var(--link-color);
239
+ }
240
+
241
+ .super-settings-language-menu[open] .super-settings-language-trigger-caret {
242
+ opacity: 0.8;
243
+ transform: rotate(180deg);
244
+ }
245
+
246
+ .super-settings-language-popup {
247
+ position: absolute;
248
+ right: 0;
249
+ bottom: calc(100% + 6px);
250
+ min-width: 160px;
251
+ max-height: 260px;
252
+ margin: 0;
253
+ padding: 4px;
254
+ background: var(--background-color, #fff);
255
+ border: 1px solid var(--border-color, #ccc);
256
+ border-radius: 8px;
257
+ box-shadow: 0 4px 16px rgba(0,0,0,0.12);
258
+ opacity: 0;
259
+ transform: translateY(4px);
260
+ pointer-events: none;
261
+ transition: opacity 0.15s ease, transform 0.15s ease;
262
+ overflow-y: auto;
263
+ }
264
+
265
+ .super-settings-language-popup li {
266
+ list-style: none;
267
+ margin: 0;
268
+ padding: 0;
269
+ }
270
+
271
+ .super-settings-language-menu[open] .super-settings-language-popup {
272
+ opacity: 1;
273
+ transform: translateY(0);
274
+ pointer-events: auto;
275
+ }
276
+
277
+ .super-settings-language-option {
278
+ width: 100%;
279
+ display: flex;
280
+ align-items: center;
281
+ gap: 8px;
282
+ padding: 6px 10px;
283
+ border: none;
284
+ border-radius: 4px;
285
+ background: transparent;
286
+ color: var(--text-color);
287
+ cursor: pointer;
288
+ text-align: start;
289
+ transition: background 0.1s ease;
290
+ font-family: inherit;
291
+ font-weight: 500;
292
+ }
293
+
294
+ .super-settings-language-option-name {
295
+ flex: 1;
296
+ min-width: 0;
297
+ font-size: 0.75rem;
298
+ overflow: hidden;
299
+ white-space: nowrap;
300
+ text-overflow: ellipsis;
301
+ }
302
+
303
+ .super-settings-language-option:hover,
304
+ .super-settings-language-option:focus-visible {
305
+ background: var(--border-color, #eee);
306
+ }
307
+
308
+ .super-settings-language-option-check {
309
+ width: 12px;
310
+ height: 12px;
311
+ color: var(--link-color);
312
+ opacity: 0;
313
+ transform: scale(0.5);
314
+ transition: opacity 0.15s ease, transform 0.15s ease;
315
+ flex-shrink: 0;
316
+ }
317
+
318
+ .super-settings-language-option-check svg {
319
+ width: 100%;
320
+ height: 100%;
321
+ }
322
+
323
+ .super-settings-language-option.is-active {
324
+ background: color-mix(in srgb, var(--link-color) 10%, transparent);
325
+ }
326
+
327
+ .super-settings-language-option.is-active .super-settings-language-option-check {
328
+ opacity: 1;
329
+ transform: scale(1);
330
+ }
331
+
332
+ @media (max-width: 640px) {
333
+ .super-settings-page-header-inner {
334
+ padding: 16px 24px;
335
+ }
336
+ .super-settings-page-footer {
337
+ padding: 0 24px 16px 24px;
338
+ }
339
+ }
@@ -1,8 +1,10 @@
1
- <% unless color_scheme != :dark %>
1
+ <% unless color_scheme == :dark %>
2
2
  :root {
3
- --text-color: #212529;
4
- --background-color: #ffffff;
5
- --link-color: #369;
3
+ --text-color: #111827;
4
+ --background-color: #f4f5f7;
5
+ --header-background-color: #edeef1;
6
+ --link-color: #0d9488;
7
+ --border-color: #e2e5ea;
6
8
  }
7
9
  <% end %>
8
10
 
@@ -11,9 +13,15 @@
11
13
  <% end %>
12
14
  <% if color_scheme == :system || color_scheme == :dark %>
13
15
  :root {
14
- --text-color: #fff;
15
- --background-color: #333;
16
- --link-color: rgb(133, 179, 225);
16
+ <% elsif dark_mode_selector %>
17
+ :root<%= dark_mode_selector %> {
18
+ <% end %>
19
+ <% if color_scheme == :system || color_scheme == :dark || dark_mode_selector %>
20
+ --text-color: #eef0f8;
21
+ --background-color: #1a1d23;
22
+ --header-background-color: #21252b;
23
+ --link-color: #2dd4bf;
24
+ --border-color: #363b44;
17
25
  }
18
26
  <% end %>
19
27
  <% if color_scheme == :system %>
@@ -1,4 +1,8 @@
1
1
  (function() {
2
+ // ── i18n helper ──
3
+ const _i18n = window.__superSettingsI18n || {};
4
+ const t = (key) => _i18n[key] || key;
5
+
2
6
  // Return the card element for a setting.
3
7
  function findSettingCard(id) {
4
8
  if (id) {
@@ -29,6 +33,8 @@
29
33
  countSpan.innerHTML = count;
30
34
  discardButton.disabled = false;
31
35
  }
36
+ // Force repaint to prevent Safari box-shadow artifact when button width changes
37
+ void saveButton.offsetWidth;
32
38
  }
33
39
  }
34
40
 
@@ -255,6 +261,7 @@
255
261
  }
256
262
  bindSettingControlEvents(card);
257
263
  filterSettings(document.querySelector("#super-settings-filter").value);
264
+ card.classList.add("super-settings-card-reveal");
258
265
  card.scrollIntoView({block: "nearest"});
259
266
  enableSaveButton();
260
267
  return card;
@@ -277,7 +284,11 @@
277
284
 
278
285
  // Update the settings count display.
279
286
  function updateSettingsCount(count) {
280
- document.querySelector(".js-settings-count").textContent = `${count} ${count === 1 ? "Setting" : "Settings"}`;
287
+ document.querySelector(".js-settings-count").textContent = `${count} ${count === 1 ? t("count.setting") : t("count.settings")}`;
288
+ const sortControls = document.querySelector(".super-settings-sort-controls");
289
+ if (sortControls) {
290
+ sortControls.style.display = (count > 0) ? "" : "none";
291
+ }
281
292
  }
282
293
 
283
294
  // Apply the given filter to only show settings that have a key, value, or description
@@ -358,7 +369,7 @@
358
369
  flash.classList.add("super-settings-text-success");
359
370
  flash.classList.remove("super-settings-text-danger");
360
371
  } else {
361
- flash.classList.add("tsuper-settings-ext-danger");
372
+ flash.classList.add("super-settings-text-danger");
362
373
  flash.classList.remove("super-settings-text-success");
363
374
  }
364
375
  flash.innerText = message;
@@ -366,6 +377,13 @@
366
377
  dismissFlash();
367
378
  }
368
379
 
380
+ // Show an API error in the flash message area instead of using window.alert.
381
+ function showAPIError(error) {
382
+ console.error("Error:", error);
383
+ const message = t("error.generic");
384
+ showFlash(message, false);
385
+ }
386
+
369
387
  // Automatically hide the flash message displaying the results of the last save operation.
370
388
  function dismissFlash() {
371
389
  if (document.querySelector(".js-flash")) {
@@ -392,12 +410,12 @@
392
410
  function renderHistoryTable(parent, payload) {
393
411
  parent.innerHTML = document.querySelector("#setting-history-table").innerHTML.trim();
394
412
  parent.querySelector(".super-settings-history-key").innerText = payload.key;
395
- const historyItems = parent.querySelector("#super-settings-history");
413
+ const historyItems = parent.querySelector(".super-settings-history-items");
396
414
  let itemsHTML = "";
397
415
  payload.histories.forEach(function(history) {
398
416
  const date = new Date(Date.parse(history.created_at));
399
417
  const dateString = dateFormatter().format(date);
400
- const value = (history.deleted ? '<em class="super-settings-text-danger">deleted</em>' : escapeHTML(history.value));
418
+ const value = (history.deleted ? '<em class="super-settings-text-danger">' + escapeHTML(t("history.deleted")) + '</em>' : escapeHTML(history.value));
401
419
  itemsHTML += `<div class="super-settings-history-item">
402
420
  <div class="super-settings-history-time">${escapeHTML(dateString)}</div>
403
421
  <div class="super-settings-history-user">${escapeHTML(history.changed_by)}</div>
@@ -409,10 +427,10 @@
409
427
  if (payload.previous_page_params || payload.next_page_params) {
410
428
  let paginationHTML = `<div class="super-settings-align-center">`;
411
429
  if (payload.previous_page_params) {
412
- paginationHTML += `<div style="float:left;"><a href="#" class="js-show-history" title="Newer" data-offset="${payload.previous_page_params.offset}" data-limit="${payload.previous_page_params.limit}" data-key="${payload.previous_page_params.key}")>&#8592; Newer</a></div>`;
430
+ paginationHTML += `<div style="float:left;"><a href="#" class="js-show-history" title="${escapeHTML(t("history.newer"))}" data-offset="${payload.previous_page_params.offset}" data-limit="${payload.previous_page_params.limit}" data-key="${payload.previous_page_params.key}")>&#8592; ${escapeHTML(t("history.newer"))}</a></div>`;
413
431
  }
414
432
  if (payload.next_page_params) {
415
- paginationHTML += `<div style="float:right;"><a href="#" class="js-show-history" title="Older" data-offset="${payload.next_page_params.offset}" data-limit="${payload.next_page_params.limit}" data-key="${payload.next_page_params.key}")>Older &#8594;</a></div>`;
433
+ paginationHTML += `<div style="float:right;"><a href="#" class="js-show-history" title="${escapeHTML(t("history.older"))}" data-offset="${payload.next_page_params.offset}" data-limit="${payload.next_page_params.limit}" data-key="${payload.next_page_params.key}")>${escapeHTML(t("history.older"))} &#8594;</a></div>`;
416
434
  }
417
435
  paginationHTML += '<div style="clear:both;"></div>';
418
436
  parent.querySelector(".super-settings-history-container").insertAdjacentHTML("afterend", paginationHTML);
@@ -538,7 +556,7 @@
538
556
  SuperSettingsAPI.fetchHistory(params, function(settingHistory){
539
557
  renderHistoryTable(content, settingHistory);
540
558
  showModal();
541
- });
559
+ }, showAPIError);
542
560
  }
543
561
 
544
562
  // Listener for closing the modal window overlay.
@@ -611,6 +629,7 @@
611
629
  if (setting) {
612
630
  const newCard = settingCard(setting);
613
631
  bindSettingControlEvents(newCard);
632
+ newCard.classList.add("super-settings-card-reveal");
614
633
  card.replaceWith(newCard);
615
634
  } else {
616
635
  card.remove();
@@ -676,15 +695,15 @@
676
695
  SuperSettingsAPI.updateSettings({settings: settingsData}, function(results) {
677
696
  if (results.success) {
678
697
  fetchActiveSettings();
679
- showFlash("Settings saved", true)
698
+ showFlash(t("flash.saved"), true)
680
699
  } else {
681
700
  event.target.disabled = false;
682
- showFlash("Failed to save settings", false)
701
+ showFlash(t("flash.save_failed"), false)
683
702
  if (results.errors) {
684
703
  showValidationErrors(results.errors)
685
704
  }
686
705
  }
687
- });
706
+ }, showAPIError);
688
707
  }
689
708
 
690
709
  // Listener for the filter input field.
@@ -861,7 +880,7 @@
861
880
 
862
881
  function promptUnsavedChanges(event) {
863
882
  if (changesCount() > 0) {
864
- return "Are you sure you want to leave?";
883
+ return t("prompt.unsaved");
865
884
  } else {
866
885
  return undefined;
867
886
  }
@@ -881,7 +900,7 @@
881
900
  const settings = settings_hash["settings"];
882
901
  activeSettings = settings;
883
902
  renderSettingsContainer(settings);
884
- if (hashParams && hashParams.edit) {
903
+ if (hashParams && hashParams.edit && !document.querySelector(".super-settings[data-read-only]")) {
885
904
  const setting = findSettingByKey(hashParams.edit);
886
905
  if (setting) {
887
906
  editSetting(setting);
@@ -890,12 +909,17 @@
890
909
  }
891
910
  }
892
911
  enableSaveButton();
893
- });
912
+ }, showAPIError);
894
913
  }
895
914
 
896
915
  let activeSettings = [];
897
916
 
898
917
  docReady(function() {
918
+ const appElement = document.querySelector(".super-settings[data-api-base-url]");
919
+ if (appElement) {
920
+ SuperSettingsAPI.baseUrl = appElement.dataset.apiBaseUrl;
921
+ }
922
+
899
923
  storeAccessToken();
900
924
 
901
925
  addListener(document.querySelector("#super-settings-filter"), "input", filterListener);
@@ -915,5 +939,40 @@
915
939
  fetchActiveSettings(hashParams);
916
940
 
917
941
  window.onbeforeunload = promptUnsavedChanges;
942
+
943
+ // ── Language Menu ──
944
+ const languageMenu = document.getElementById("super-settings-language-menu");
945
+ const languageOptions = document.querySelectorAll(".super-settings-language-option");
946
+ const closeLanguageMenu = () => {
947
+ if (languageMenu) languageMenu.removeAttribute("open");
948
+ };
949
+
950
+ if (languageMenu && languageOptions.length > 0) {
951
+ languageOptions.forEach((option) => {
952
+ option.addEventListener("click", () => {
953
+ const locale = option.dataset.locale;
954
+ if (!locale) return;
955
+
956
+ closeLanguageMenu();
957
+
958
+ // Persist the choice in a cookie (accessible server-side)
959
+ document.cookie = "super_settings_locale=" + encodeURIComponent(locale) + ";path=/;max-age=31536000;SameSite=Lax";
960
+
961
+ // Also store in localStorage for client-side persistence
962
+ try { localStorage.setItem("super_settings_locale", locale); } catch(e) {}
963
+
964
+ // Reload with lang query param so server picks it up immediately
965
+ const url = new URL(window.location.href);
966
+ url.searchParams.set("lang", locale);
967
+ window.location.href = url.toString();
968
+ });
969
+ });
970
+
971
+ document.addEventListener("click", (e) => {
972
+ if (languageMenu.hasAttribute("open") && !e.target.closest("#super-settings-language-menu")) {
973
+ closeLanguageMenu();
974
+ }
975
+ });
976
+ }
918
977
  })
919
978
  })();