ultra_settings 2.10.0 → 2.10.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 (54) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +26 -0
  3. data/README.md +18 -0
  4. data/VERSION +1 -1
  5. data/app/_config_description.html.erb +10 -1
  6. data/app/_data_source.html.erb +2 -2
  7. data/app/application.css +116 -24
  8. data/app/application_vars.css.erb +30 -12
  9. data/app/configuration.html.erb +19 -1
  10. data/app/layout.css +5 -5
  11. data/app/layout_vars.css.erb +4 -4
  12. data/app/locales/ar.json +5 -2
  13. data/app/locales/cs.json +5 -2
  14. data/app/locales/da.json +5 -2
  15. data/app/locales/de.json +5 -2
  16. data/app/locales/el.json +5 -2
  17. data/app/locales/en.json +5 -2
  18. data/app/locales/es.json +5 -2
  19. data/app/locales/fa.json +5 -2
  20. data/app/locales/fr.json +5 -2
  21. data/app/locales/gd.json +5 -2
  22. data/app/locales/he.json +5 -2
  23. data/app/locales/hi.json +5 -2
  24. data/app/locales/it.json +5 -2
  25. data/app/locales/ja.json +5 -2
  26. data/app/locales/ko.json +5 -2
  27. data/app/locales/lt.json +5 -2
  28. data/app/locales/nb.json +5 -2
  29. data/app/locales/nl.json +5 -2
  30. data/app/locales/pl.json +5 -2
  31. data/app/locales/pt-br.json +5 -2
  32. data/app/locales/pt.json +5 -2
  33. data/app/locales/ru.json +5 -2
  34. data/app/locales/sv.json +5 -2
  35. data/app/locales/ta.json +5 -2
  36. data/app/locales/tr.json +5 -2
  37. data/app/locales/uk.json +5 -2
  38. data/app/locales/ur.json +5 -2
  39. data/app/locales/vi.json +5 -2
  40. data/app/locales/zh-cn.json +5 -2
  41. data/app/locales/zh-tw.json +5 -2
  42. data/lib/ultra_settings/application_view.rb +13 -4
  43. data/lib/ultra_settings/audit_data_sources.rb +1 -1
  44. data/lib/ultra_settings/coerce.rb +1 -1
  45. data/lib/ultra_settings/configuration.rb +36 -10
  46. data/lib/ultra_settings/configuration_view.rb +166 -9
  47. data/lib/ultra_settings/mini_i18n.rb +14 -10
  48. data/lib/ultra_settings/rack_app.rb +4 -4
  49. data/lib/ultra_settings/view_helper.rb +27 -5
  50. data/lib/ultra_settings/web_view.rb +17 -10
  51. data/lib/ultra_settings/yaml_config.rb +10 -2
  52. data/lib/ultra_settings.rb +57 -1
  53. data/ultra_settings.gemspec +1 -1
  54. metadata +2 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 013b301669011dc685fc7016e1b66c12a6c8890f7b8dc9ba5bdb63c188eee191
4
- data.tar.gz: 1d558756ea2014cea76c2c98747ae0dd760e32a9526cb6548d61e93ea0e11e1b
3
+ metadata.gz: 92ad90432ce22bfa615bda0956cbd80d8f06115c925b99b0a250dc9c76be7dcf
4
+ data.tar.gz: 3163c675d4ef57edab7cc438f0d1553bf160dd8003b03f14c59f30b7f39d79fb
5
5
  SHA512:
6
- metadata.gz: 866378e50cdd9016084f5f6b1db66d97a32c3f9108c2ac44aaff59e7412a4980e439f254b6aed704d7f64ee955b9c33e23c4e698a5d179d2f072452fff37c1c2
7
- data.tar.gz: f262427eb43bb6a78bcb034ef276e98de5517eeb77283022fcd25a40af947de4dc6a7c123c1c45a6067b21aa36beaeca086a04b46ca7bc14b0d6fbe4590520d6
6
+ metadata.gz: b938c8d2275dfc72319485edde8bc36d5ae866d31225201967d98c9c78873256b838f682cd8709a52ca645b713eea578820baaf96cf8ebb8242debcdb355a9a0
7
+ data.tar.gz: e689fd1da5cc1a647475a61efb7586544ae6ae383c9bd1d057c738e7067f055341ab8e83c7f3b5103d0b49dd0700972d45ad4cbea9be2873b28611c68676d90b
data/CHANGELOG.md CHANGED
@@ -4,6 +4,32 @@ All notable changes to this project will be documented in this file.
4
4
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
5
5
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## 2.10.1
8
+
9
+ ### Added
10
+
11
+ - Added a button to the web UI for copying the current value of a setting to the clipboard. The button is disabled for secret settings and is not shown for settings that have no value.
12
+ - Added `UltraSettings.with_runtime_settings_reloaded` for rendering several embedded views with a single runtime settings reload.
13
+ - Added a toggle switch to the web UI for showing and hiding the YAML keys of a configuration. The switch is only shown when the YAML file does not exist, and the keys are hidden by default in that case since they are not used by the application.
14
+
15
+ ### Changed
16
+
17
+ - The web UI now fully reloads the runtime settings cache before rendering instead of requesting an incremental refresh. Previously a value changed from the UI could still render as its old value after the page reloaded, since the incremental refresh would silently do nothing if another thread was already refreshing, if the cache had never been loaded, or if the change had been saved by a different process. The reload is performed on any runtime settings object that responds to `load_settings` — including wrappers around `SuperSettings` — rather than only on `SuperSettings` itself, and an error raised while reloading is now reported to stderr instead of failing the page.
18
+ - The reload is now performed by `UltraSettings::ApplicationView#render` and `UltraSettings::ConfigurationView#render` rather than only by the standalone Rack app, so views embedded in a host application also display current values. Rendering a page with many configurations through `ApplicationView` still only reloads once; a page that embeds several `ConfigurationView` instances directly can wrap them in `UltraSettings.with_runtime_settings_reloaded` to reload once for the page rather than once per view.
19
+
20
+ ### Fixed
21
+
22
+ - YAML configuration files with anchors and aliases (e.g. `<<: *defaults`) and unquoted date or time values now load correctly on Ruby 3.1+ where `YAML.load` is safe by default.
23
+ - Fixed a race condition where concurrent requests to the web UI could render with each other's locale because per-request state was stored on a shared view instance.
24
+ - Fixed a race condition where the web UI translation cache could return a partially loaded set of locales to concurrent requests while the cache was being populated.
25
+ - The web UI now recognizes `RAILS_ENV` and `APP_ENV` in addition to `RACK_ENV` when determining if the app is running in development mode. Previously a Rails app running in production without `RACK_ENV` set would re-read templates and translations from disk on every request. The check is shared by the template cache, the translation cache, and the Rack app's view cache so that they cannot disagree.
26
+ - Fixed a memory leak where `Configuration#override!` retained an entry for every thread that had ever used it.
27
+ - Fixed `AuditDataSources` treating a `false` YAML value as missing and falling back to the field default.
28
+ - Static field memoization and configuration subclass tracking are now fully synchronized for Ruby runtimes without a global VM lock (JRuby, TruffleRuby).
29
+ - Web UI asset, template, translation, and YAML configuration files are now always read as UTF-8 so the gem works when the default external encoding is not UTF-8 (e.g. when `LANG` is not set).
30
+ - The web UI no longer raises an error when determining relative configuration file paths if the YAML configuration directory does not exist or is not set.
31
+ - The web UI no longer displays configuration file paths with a leading relative path prefix (e.g. `../app/config/settings.yml`). Paths are now shown relative to the working directory or to the YAML configuration directory, and the absolute path is shown if the file is not inside either one.
32
+
7
33
  ## 2.10.0
8
34
 
9
35
  ### Added
data/README.md CHANGED
@@ -171,6 +171,8 @@ UltraSettings.runtime_settings = RedisRuntimeSettings.new
171
171
 
172
172
  The runtime settings implementation may also define an `array` method that takes a single parameter to return an array value. If this method is not implemented, then array values must be returned as single line CSV strings.
173
173
 
174
+ The runtime settings implementation may also define a `load_settings` method. If it does, the web UI will call it before rendering so that values changed elsewhere are displayed immediately instead of whenever the implementation's own cache next refreshes. It should reload synchronously and return once the values are current. Errors raised from it are logged to stderr and do not prevent the page from rendering.
175
+
174
176
  > [!TIP]
175
177
  > If your runtime settings implementation does not securely store values, you should set `UltraSettings.runtime_settings_secure` to `false`. This will disable runtime settings on fields marked as secret to prevent leaking sensitive information.
176
178
 
@@ -391,6 +393,8 @@ end
391
393
 
392
394
  UltraSettings provides a web UI via a mountable Rack application. You can use this to view the settings values and documentation. The UI will not display the value of any setting marked as secret.
393
395
 
396
+ Each setting has a button for copying its current value to the clipboard. The copied text is the raw value rather than the quoted form shown on the page (arrays are copied as one entry per line). The button is disabled for secret settings since their values are never sent to the browser.
397
+
394
398
  ![Web UI](assets/web_ui.png)
395
399
 
396
400
  It is strongly recommended to secure the web UI with your application's authorization framework so that it is only visible to internal admin users.
@@ -445,6 +449,15 @@ You'll also need to include the CSS for the configuration view on your page.
445
449
  </head>
446
450
  ```
447
451
 
452
+ Each view reloads the runtime settings before rendering so that it displays current values. If you embed more than one view on a page, wrap them in `UltraSettings.with_runtime_settings_reloaded` so that the settings are only reloaded once for the whole page instead of once per view.
453
+
454
+ ```erb
455
+ <% UltraSettings.with_runtime_settings_reloaded do %>
456
+ <%= UltraSettings::ConfigurationView.new(MyServiceConfiguration.instance).render %>
457
+ <%= UltraSettings::ConfigurationView.new(OtherServiceConfiguration.instance).render %>
458
+ <% end %>
459
+ ```
460
+
448
461
  #### Customizing The Web UI
449
462
 
450
463
  You can specify the color scheme by setting by providing the `color_scheme` option to the `UltraSettings::ApplicationView` constructor. The default color scheme is `:light`. You can also set the scheme to `:dark` or `:system`.
@@ -482,6 +495,11 @@ All authorization is handled by the SuperSettings API — the same permissions t
482
495
  > [!IMPORTANT]
483
496
  > You will need to have the SuperSettings API mounted and properly locked down with authentication. If the API is not accessible, then the edit buttons will not be shown. Check the browser console for any errors if you expect the buttons to be shown but they are not appearing.
484
497
 
498
+ `SuperSettings` responds to `load_settings`, so the web UI reloads the SuperSettings cache before each render and a setting saved from the edit panel is displayed as soon as the page reloads.
499
+
500
+ > [!NOTE]
501
+ > In a Rails application, `SuperSettings::Context::RackMiddleware` pins each setting's value for the remainder of a request once it has been read. This does not affect the standalone Rack app since the reload happens before any value is read. However, if you embed `UltraSettings::ApplicationView` in a page that already read a runtime setting earlier in the same request, that particular setting will still render with its pre-reload value.
502
+
485
503
  ##### Embedding with Edit Support
486
504
 
487
505
  When embedding the settings view using `UltraSettings::ApplicationView`, you can pass the `super_settings_api_path` option to enable the inline editing UI:
data/VERSION CHANGED
@@ -1 +1 @@
1
- 2.10.0
1
+ 2.10.1
@@ -19,8 +19,17 @@
19
19
  <span class="ultra-settings-warn">
20
20
  <%= warning_icon(13) %>
21
21
  <code><%= html_escape(relative_path(configuration.class.configuration_file)) %></code>
22
- <%= t('config.file_not_found') %>
22
+ &bull; <%= t('config.file_not_found') %>
23
23
  </span>
24
+
25
+ <button type="button"
26
+ class="ultra-settings-yaml-toggle"
27
+ onclick="<%= html_escape(toggle_yaml_keys_script) %>"
28
+ role="switch"
29
+ aria-checked="false">
30
+ <span class="ultra-settings-yaml-toggle-switch" aria-hidden="true"></span>
31
+ <%= t('config.show_yaml_keys') %>
32
+ </button>
24
33
  <% end %>
25
34
  </div>
26
35
  <% end %>
@@ -2,7 +2,7 @@
2
2
  <% source_label = source_chip_label(source) %>
3
3
  <% chip_class = source_chip_class(source) %>
4
4
  <% source_name = source_key_name(field, source) %>
5
- <div class="ultra-settings-source-row<%= ' active' if current_source == source %>">
5
+ <div class="ultra-settings-source-row<%= ' active' if current_source == source %>" data-source="<%= html_escape(source) %>">
6
6
  <span class="ultra-settings-source-chip <%= chip_class %>"><%= source_label %></span>
7
7
 
8
8
  <% if source_name %>
@@ -46,7 +46,7 @@
46
46
  <% end %>
47
47
 
48
48
  <% if current_source == source %>
49
- <span class="ultra-settings-source-badge ultra-settings-sb-active"><%= t('source.active') %></span>
49
+ <span class="ultra-settings-source-badge ultra-settings-sb-active">&#9679; <%= t('source.active') %></span>
50
50
  <% elsif source_overridden_by(source, current_source) && !source_value.nil? %>
51
51
  <span class="ultra-settings-source-badge ultra-settings-sb-overridden"><%= t('source.overridden') %></span>
52
52
  <% end %>
data/app/application.css CHANGED
@@ -53,7 +53,7 @@
53
53
  width: 100%;
54
54
  padding: 10px 38px 10px 38px;
55
55
  background: var(--bg-card);
56
- border: 1px solid var(--border-card);
56
+ border: 1px solid var(--border-input);
57
57
  border-radius: 8px;
58
58
  color: var(--text-primary);
59
59
  font-size: 0.875rem;
@@ -130,7 +130,7 @@
130
130
  border-radius: 8px;
131
131
  cursor: pointer;
132
132
  user-select: none;
133
- border: 1px solid var(--border-card);
133
+ border: 1px solid var(--border-input);
134
134
  background: var(--bg-card);
135
135
  color: var(--text-secondary);
136
136
  font-size: 0.75rem;
@@ -181,7 +181,7 @@
181
181
  width: 12px;
182
182
  height: 12px;
183
183
  color: inherit;
184
- opacity: 0.5;
184
+ opacity: 0.75;
185
185
  transition: transform 0.15s ease, opacity 0.15s ease;
186
186
  }
187
187
 
@@ -196,7 +196,7 @@
196
196
  }
197
197
 
198
198
  .ultra-settings-language-menu[open] .ultra-settings-language-trigger-caret {
199
- opacity: 0.8;
199
+ opacity: 1;
200
200
  transform: rotate(180deg);
201
201
  }
202
202
 
@@ -416,6 +416,7 @@
416
416
  .ultra-settings-config-file {
417
417
  display: inline-flex;
418
418
  align-items: center;
419
+ flex-wrap: wrap;
419
420
  gap: 6px;
420
421
  font-size: 0.8125rem;
421
422
  color: var(--text-tertiary);
@@ -435,7 +436,52 @@
435
436
  border: 1px solid var(--border-light);
436
437
  }
437
438
 
438
- .ultra-settings-config-file .ultra-settings-warn { color: #dc2626; font-size: 0.8125rem; }
439
+ .ultra-settings-config-file .ultra-settings-warn { color: var(--warn-color); font-size: 0.8125rem; }
440
+
441
+ .ultra-settings-yaml-toggle {
442
+ display: inline-flex;
443
+ align-items: center;
444
+ gap: 6px;
445
+ font-family: var(--font-ui);
446
+ font-size: 0.8125rem;
447
+ color: var(--text-tertiary);
448
+ background: none;
449
+ border: none;
450
+ padding: 0;
451
+ cursor: pointer;
452
+ white-space: nowrap;
453
+ transition: color 0.2s ease;
454
+ }
455
+
456
+ .ultra-settings-yaml-toggle:hover { color: var(--text-primary); }
457
+
458
+ /* Switch showing whether the keys are visible. */
459
+ .ultra-settings-yaml-toggle-switch {
460
+ position: relative;
461
+ width: 26px;
462
+ height: 14px;
463
+ flex-shrink: 0;
464
+ border-radius: 7px;
465
+ background: var(--accent);
466
+ transition: background 0.15s ease;
467
+ }
468
+
469
+ .ultra-settings-yaml-toggle-switch::before {
470
+ content: "";
471
+ position: absolute;
472
+ top: 2px;
473
+ inset-inline-start: 14px;
474
+ width: 10px;
475
+ height: 10px;
476
+ border-radius: 50%;
477
+ background: var(--btn-primary-text);
478
+ transition: inset-inline-start 0.15s ease;
479
+ }
480
+
481
+ .ultra-settings-yaml-hidden .ultra-settings-yaml-toggle-switch { background: var(--border-input); }
482
+ .ultra-settings-yaml-hidden .ultra-settings-yaml-toggle-switch::before { inset-inline-start: 2px; }
483
+
484
+ .ultra-settings-yaml-hidden .ultra-settings-source-row[data-source="yaml"] { display: none; }
439
485
 
440
486
  /* ══════════════════════════════════════════
441
487
  FIELD CARDS
@@ -535,6 +581,10 @@
535
581
  margin-inline-start: auto;
536
582
  flex-shrink: 0;
537
583
  max-width: 50%;
584
+ display: flex;
585
+ align-items: center;
586
+ gap: 6px;
587
+ min-width: 0;
538
588
  }
539
589
 
540
590
  .ultra-settings-field-value {
@@ -547,6 +597,7 @@
547
597
  border-radius: 6px;
548
598
  border: 1px solid var(--border-light);
549
599
  max-width: 320px;
600
+ min-width: 0;
550
601
  overflow: hidden;
551
602
  text-overflow: ellipsis;
552
603
  white-space: nowrap;
@@ -565,6 +616,52 @@
565
616
  font-style: italic;
566
617
  }
567
618
 
619
+ /* Copy value button */
620
+ .ultra-settings-copy-btn {
621
+ flex-shrink: 0;
622
+ display: inline-flex;
623
+ align-items: center;
624
+ justify-content: center;
625
+ width: 26px;
626
+ height: 26px;
627
+ padding: 0;
628
+ background: transparent;
629
+ border: 1px solid transparent;
630
+ border-radius: 6px;
631
+ color: var(--text-tertiary);
632
+ cursor: pointer;
633
+ transition: color 0.12s ease, background 0.12s ease, border-color 0.12s ease;
634
+ }
635
+
636
+ .ultra-settings-copy-btn:hover:not(:disabled) {
637
+ color: var(--accent);
638
+ background: var(--accent-light);
639
+ border-color: var(--accent);
640
+ }
641
+
642
+ .ultra-settings-copy-btn:disabled {
643
+ opacity: 0.4;
644
+ cursor: not-allowed;
645
+ }
646
+
647
+ .ultra-settings-copy-btn .ultra-settings-copy-check { display: none; }
648
+
649
+ .ultra-settings-copy-btn.copied {
650
+ color: var(--active-color);
651
+ border-color: transparent;
652
+ background: var(--active-bg);
653
+ }
654
+
655
+ .ultra-settings-copy-btn.copied .ultra-settings-copy-icon { display: none; }
656
+ .ultra-settings-copy-btn.copied .ultra-settings-copy-check { display: inline; }
657
+
658
+ /* Stands in for the copy button on rows that have no value to copy. */
659
+ .ultra-settings-copy-placeholder {
660
+ flex-shrink: 0;
661
+ width: 26px;
662
+ height: 26px;
663
+ }
664
+
568
665
  /* Description */
569
666
  .ultra-settings-field-description {
570
667
  padding: 0 18px 14px 18px;
@@ -653,14 +750,13 @@
653
750
  color: var(--accent);
654
751
  text-decoration: none;
655
752
  margin-inline-start: 0.5rem;
656
- opacity: 0.7;
657
- transition: opacity 0.2s ease;
753
+ transition: color 0.2s ease;
658
754
  display: inline-flex;
659
755
  align-items: center;
660
756
  }
661
757
 
662
758
  .ultra-settings-edit-link:hover {
663
- opacity: 1;
759
+ color: var(--text-primary);
664
760
  }
665
761
 
666
762
  .ultra-settings-edit-link svg { width: 13px; height: 13px; }
@@ -714,7 +810,7 @@
714
810
  .ultra-settings-dp-close {
715
811
  width: 32px; height: 32px;
716
812
  border-radius: 7px;
717
- border: 1px solid var(--border-light);
813
+ border: 1px solid var(--border-input);
718
814
  background: transparent;
719
815
  cursor: pointer;
720
816
  display: flex;
@@ -800,7 +896,7 @@
800
896
  .ultra-settings-dialog-close {
801
897
  width: 32px; height: 32px;
802
898
  border-radius: 7px;
803
- border: 1px solid var(--border-light);
899
+ border: 1px solid var(--border-input);
804
900
  background: transparent;
805
901
  cursor: pointer;
806
902
  display: flex;
@@ -855,7 +951,7 @@
855
951
 
856
952
  .ultra-settings-ss-edit-btn:hover {
857
953
  background: var(--accent);
858
- color: #fff;
954
+ color: var(--btn-primary-text, #fff);
859
955
  }
860
956
 
861
957
  .ultra-settings-ss-panel-bg {
@@ -901,12 +997,10 @@
901
997
  font-size: 0.75rem;
902
998
  color: var(--accent);
903
999
  text-decoration: none;
904
- transition: opacity 0.15s;
905
1000
  }
906
1001
 
907
1002
  .ultra-settings-ss-external-link:hover {
908
1003
  text-decoration: underline;
909
- opacity: 0.85;
910
1004
  }
911
1005
 
912
1006
  .ultra-settings-ss-loading {
@@ -943,7 +1037,7 @@
943
1037
  font-family: var(--font-mono);
944
1038
  color: var(--text-primary);
945
1039
  background: var(--bg-code);
946
- border: 1px solid var(--border-light);
1040
+ border: 1px solid var(--border-input);
947
1041
  border-radius: 6px;
948
1042
  transition: border-color 0.15s;
949
1043
  box-sizing: border-box;
@@ -964,7 +1058,7 @@ select.ultra-settings-ss-input {
964
1058
  -webkit-appearance: none;
965
1059
  appearance: none;
966
1060
  padding-inline-end: 28px;
967
- background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6'%3E%3Cpath d='M0 0l5 6 5-6z' fill='%23999'/%3E%3C/svg%3E");
1061
+ background-image: var(--select-arrow);
968
1062
  background-repeat: no-repeat;
969
1063
  background-position: right 10px center;
970
1064
  }
@@ -1018,10 +1112,10 @@ input[type="number"].ultra-settings-ss-input::-webkit-outer-spin-button {
1018
1112
 
1019
1113
  .ultra-settings-ss-errors {
1020
1114
  padding: 10px 12px;
1021
- background: rgba(220,53,69,0.1);
1022
- border: 1px solid rgba(220,53,69,0.3);
1115
+ background: var(--danger-bg);
1116
+ border: 1px solid var(--danger-border);
1023
1117
  border-radius: 6px;
1024
- color: #dc3545;
1118
+ color: var(--danger-text);
1025
1119
  font-size: 0.8125rem;
1026
1120
  line-height: 1.4;
1027
1121
  }
@@ -1044,7 +1138,7 @@ input[type="number"].ultra-settings-ss-input::-webkit-outer-spin-button {
1044
1138
  font-size: 0.8125rem;
1045
1139
  font-weight: 600;
1046
1140
  border-radius: 6px;
1047
- border: 1px solid var(--border-light);
1141
+ border: 1px solid var(--border-input);
1048
1142
  cursor: pointer;
1049
1143
  transition: background 0.15s, border-color 0.15s, color 0.15s;
1050
1144
  font-family: var(--font-ui);
@@ -1096,12 +1190,10 @@ input[type="number"].ultra-settings-ss-input::-webkit-outer-spin-button {
1096
1190
  color: var(--accent);
1097
1191
  text-decoration: none;
1098
1192
  cursor: pointer;
1099
- transition: opacity 0.15s;
1100
1193
  }
1101
1194
 
1102
1195
  .ultra-settings-ss-history-link:hover {
1103
1196
  text-decoration: underline;
1104
- opacity: 0.85;
1105
1197
  }
1106
1198
 
1107
1199
  .ultra-settings-ss-history {
@@ -1153,9 +1245,9 @@ input[type="number"].ultra-settings-ss-input::-webkit-outer-spin-button {
1153
1245
  }
1154
1246
 
1155
1247
  .ultra-settings-ss-history-entry-badge.deleted {
1156
- background: rgba(220,53,69,0.1);
1157
- color: #dc3545;
1158
- border: 1px solid rgba(220,53,69,0.3);
1248
+ background: var(--danger-bg);
1249
+ color: var(--danger-text);
1250
+ border: 1px solid var(--danger-border);
1159
1251
  }
1160
1252
 
1161
1253
  .ultra-settings-ss-history-entry-badge.created {
@@ -13,17 +13,19 @@
13
13
  /* Borders */
14
14
  --border-light: #e2e5ea;
15
15
  --border-card: #dfe2e8;
16
+ /* Interactive control borders — 3:1 against every surface (WCAG 1.4.11) */
17
+ --border-input: #7e8492;
16
18
 
17
19
  /* Text */
18
20
  --text-primary: #111827;
19
21
  --text-secondary: #4b5563;
20
- --text-tertiary: #9ca3af;
22
+ --text-tertiary: #646b7a;
21
23
 
22
24
  /* Accent — teal */
23
- --accent: #0d9488;
24
- --accent-light: rgba(13, 148, 136, 0.06);
25
- --accent-lighter: rgba(13, 148, 136, 0.03);
26
- --accent-glow: rgba(13, 148, 136, 0.12);
25
+ --accent: #0f766e;
26
+ --accent-light: rgba(15, 118, 110, 0.06);
27
+ --accent-lighter: rgba(15, 118, 110, 0.03);
28
+ --accent-glow: rgba(15, 118, 110, 0.12);
27
29
  --btn-primary-text: #ffffff;
28
30
  --btn-secondary-text: #4b5563;
29
31
  --btn-secondary-hover-text: #111827;
@@ -32,24 +34,28 @@
32
34
  --src-env: #2563eb;
33
35
  --src-env-bg: #eff6ff;
34
36
  --src-env-border: #bfdbfe;
35
- --src-setting: #0d9488;
37
+ --src-setting: #0f766e;
36
38
  --src-setting-bg: #f0fdfa;
37
39
  --src-setting-border: #99f6e4;
38
40
  --src-yaml: #7c3aed;
39
41
  --src-yaml-bg: #f5f3ff;
40
42
  --src-yaml-border: #ddd6fe;
41
- --src-default: #6b7280;
43
+ --src-default: #5f6673;
42
44
  --src-default-bg: #f3f4f6;
43
45
  --src-default-border: #d1d5db;
44
46
 
45
47
  /* Status */
46
- --active-color: #059669;
47
- --active-bg: rgba(5, 150, 105, 0.05);
48
+ --active-color: #047857;
49
+ --active-bg: rgba(4, 120, 87, 0.05);
48
50
  --overridden-color: #6b7280;
51
+ --warn-color: #8a6508;
52
+ --danger-text: #b3261e;
53
+ --danger-bg: rgba(179, 38, 30, 0.10);
54
+ --danger-border: rgba(179, 38, 30, 0.35);
49
55
 
50
56
  /* Badges */
51
57
  --badge-secret-bg: #fef2f2;
52
- --badge-secret-text: #dc2626;
58
+ --badge-secret-text: #b91c1c;
53
59
  --badge-secret-border: #fecaca;
54
60
  --badge-static-bg: #f0f2f5;
55
61
  --badge-static-text: #4b5563;
@@ -71,6 +77,9 @@
71
77
 
72
78
  /* Source row divider */
73
79
  --border-source-row: rgba(0,0,0,0.025);
80
+
81
+ /* Select dropdown caret (must be 3:1 against --bg-code) */
82
+ --select-arrow: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6'%3E%3Cpath d='M0 0l5 6 5-6z' fill='%237e8492'/%3E%3C/svg%3E");
74
83
  }
75
84
  <% end %>
76
85
 
@@ -94,11 +103,13 @@
94
103
  /* Borders */
95
104
  --border-light: #2a2d3a;
96
105
  --border-card: #252836;
106
+ /* Interactive control borders — 3:1 against every surface (WCAG 1.4.11) */
107
+ --border-input: #666c86;
97
108
 
98
109
  /* Text */
99
110
  --text-primary: #e4e6f0;
100
111
  --text-secondary: #9ba1b5;
101
- --text-tertiary: #5c6178;
112
+ --text-tertiary: #8f95ad;
102
113
 
103
114
  /* Accent — teal */
104
115
  --accent: #14b8a6;
@@ -126,7 +137,11 @@
126
137
  /* Status */
127
138
  --active-color: #34d399;
128
139
  --active-bg: rgba(52, 211, 153, 0.08);
129
- --overridden-color: #6b7280;
140
+ --overridden-color: #8f95ad;
141
+ --warn-color: #d6ba1d;
142
+ --danger-text: #f87171;
143
+ --danger-bg: rgba(248, 113, 113, 0.12);
144
+ --danger-border: rgba(248, 113, 113, 0.30);
130
145
 
131
146
  /* Badges */
132
147
  --badge-secret-bg: rgba(220, 38, 38, 0.12);
@@ -152,6 +167,9 @@
152
167
 
153
168
  /* Source row divider */
154
169
  --border-source-row: rgba(255,255,255,0.04);
170
+
171
+ /* Select dropdown caret (must be 3:1 against --bg-code) */
172
+ --select-arrow: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6'%3E%3Cpath d='M0 0l5 6 5-6z' fill='%238f95ad'/%3E%3C/svg%3E");
155
173
  }
156
174
  <% end %>
157
175
  <% if color_scheme == :system || dark_mode_selector %>
@@ -1,4 +1,4 @@
1
- <div class="ultra-settings-block">
1
+ <div class="ultra-settings-block<%= " ultra-settings-yaml-hidden" if hide_yaml_keys?(configuration) %>">
2
2
  <%= render_partial "config_description", configuration: configuration %>
3
3
 
4
4
  <div class="ultra-settings-fields">
@@ -61,6 +61,24 @@
61
61
  tabindex="0"
62
62
  role="button"><%= html_escape(display_value(configuration[field.name])) %></code>
63
63
  <% end %>
64
+
65
+ <% if field.secret? %>
66
+ <button type="button"
67
+ class="ultra-settings-copy-btn"
68
+ disabled
69
+ title="<%= t('field.copy_disabled') %>"
70
+ aria-label="<%= t('field.copy_disabled') %>"><%= copy_icon %><%= check_icon %></button>
71
+ <% elsif configuration[field.name].nil? %>
72
+ <%# There is nothing to copy, but the placeholder keeps the values aligned with the other rows. %>
73
+ <span class="ultra-settings-copy-placeholder" aria-hidden="true"></span>
74
+ <% else %>
75
+ <button type="button"
76
+ class="ultra-settings-copy-btn"
77
+ onclick="<%= html_escape(copy_value_script) %>"
78
+ data-copy-value="<%= html_escape(copy_value(configuration[field.name])) %>"
79
+ title="<%= t('field.copy') %>"
80
+ aria-label="<%= t('field.copy') %>"><%= copy_icon %><%= check_icon %></button>
81
+ <% end %>
64
82
  </div>
65
83
  </div>
66
84
 
data/app/layout.css CHANGED
@@ -43,7 +43,7 @@ body {
43
43
  display: flex;
44
44
  align-items: center;
45
45
  justify-content: center;
46
- color: var(--accent, #0d9488);
46
+ color: var(--accent, #0f766e);
47
47
  flex-shrink: 0;
48
48
  }
49
49
 
@@ -66,7 +66,7 @@ body {
66
66
  font-family: var(--font-ui);
67
67
  font-size: 0.8125rem;
68
68
  font-weight: 400;
69
- color: var(--text-tertiary, #9ca3af);
69
+ color: var(--text-tertiary, #646b7a);
70
70
  line-height: 1.3;
71
71
  margin: 0;
72
72
  letter-spacing: 0.01em;
@@ -95,15 +95,15 @@ body {
95
95
  border: 1px solid transparent;
96
96
  border-radius: 8px;
97
97
  background: transparent;
98
- color: var(--text-tertiary, #9ca3af);
98
+ color: var(--text-tertiary, #646b7a);
99
99
  cursor: pointer;
100
100
  flex-shrink: 0;
101
101
  transition: color 0.2s, border-color 0.2s;
102
102
  }
103
103
 
104
104
  .ultra-settings-theme-toggle:hover {
105
- color: var(--accent, #0d9488);
106
- border-color: var(--accent, #0d9488);
105
+ color: var(--accent, #0f766e);
106
+ border-color: var(--accent, #0f766e);
107
107
  }
108
108
 
109
109
  .ultra-settings-theme-icon {
@@ -5,15 +5,15 @@
5
5
  --bg-page: #f4f5f7;
6
6
  --text-primary: #111827;
7
7
  --text-secondary: #4b5563;
8
- --text-tertiary: #9ca3af;
9
- --accent: #0d9488;
8
+ --text-tertiary: #646b7a;
9
+ --accent: #0f766e;
10
10
  --border-light: #e2e5ea;
11
11
  --bg-card: #ffffff;
12
12
  --border-card: #dfe2e8;
13
13
  --shadow-card: 0 1px 3px rgba(0,0,0,0.04), 0 0 0 1px rgba(0,0,0,0.03);
14
14
  --shadow-lg: 0 20px 48px rgba(0,0,0,0.12);
15
15
  --bg-code: #f0f2f5;
16
- --accent-light: rgba(13, 148, 136, 0.06);
16
+ --accent-light: rgba(15, 118, 110, 0.06);
17
17
  }
18
18
  <% end %>
19
19
 
@@ -31,7 +31,7 @@
31
31
  --bg-page: #0f1117;
32
32
  --text-primary: #eef0f8;
33
33
  --text-secondary: #adb3c8;
34
- --text-tertiary: #6e7490;
34
+ --text-tertiary: #8f95ad;
35
35
  --accent: #14b8a6;
36
36
  --border-light: #2a2d3a;
37
37
  --bg-card: #1a1c26;
data/app/locales/ar.json CHANGED
@@ -17,18 +17,21 @@
17
17
  "field.nil": "nil",
18
18
  "field.click_to_expand": "انقر للتوسيع",
19
19
  "field.not_set": "غير مضبوط",
20
+ "field.copy": "نسخ القيمة",
21
+ "field.copy_disabled": "لا يمكن نسخ القيم السرية",
20
22
  "source.env": "ENV",
21
23
  "source.setting": "إعداد",
22
24
  "source.yaml": "YAML",
23
25
  "source.default": "الافتراضي",
24
- "source.active": "نشط",
26
+ "source.active": "نشط",
25
27
  "source.overridden": "تم تجاوزه",
26
28
  "detail.field_detail": "تفاصيل الحقل",
27
29
  "detail.close_panel": "أغلق اللوحة",
28
30
  "detail.type_label": "النوع:",
29
31
  "detail.secret_badge": "سري",
30
32
  "detail.secret_value": "•••••••• (سري)",
31
- "config.file_not_found": "(غير موجود)",
33
+ "config.file_not_found": "الملف غير موجود",
34
+ "config.show_yaml_keys": "إظهار المفاتيح",
32
35
  "config.edit_tooltip": "حرّر ${name}",
33
36
  "edit.title": "تحرير إعداد وقت التشغيل",
34
37
  "edit.open_dashboard": "افتح في لوحة الإعدادات ↗",
data/app/locales/cs.json CHANGED
@@ -17,18 +17,21 @@
17
17
  "field.nil": "nil",
18
18
  "field.click_to_expand": "Kliknutím rozbalíte",
19
19
  "field.not_set": "nenastaveno",
20
+ "field.copy": "Kopírovat hodnotu",
21
+ "field.copy_disabled": "Tajné hodnoty nelze kopírovat",
20
22
  "source.env": "ENV",
21
23
  "source.setting": "NASTAVENÍ",
22
24
  "source.yaml": "YAML",
23
25
  "source.default": "VÝCHOZÍ",
24
- "source.active": "Aktivní",
26
+ "source.active": "Aktivní",
25
27
  "source.overridden": "Přepsáno",
26
28
  "detail.field_detail": "Detail pole",
27
29
  "detail.close_panel": "Zavřít panel",
28
30
  "detail.type_label": "Typ:",
29
31
  "detail.secret_badge": "TAJNÉ",
30
32
  "detail.secret_value": "•••••••• (tajné)",
31
- "config.file_not_found": "(nenalezeno)",
33
+ "config.file_not_found": "soubor neexistuje",
34
+ "config.show_yaml_keys": "zobrazit klíče",
32
35
  "config.edit_tooltip": "Upravit ${name}",
33
36
  "edit.title": "Upravit runtime nastavení",
34
37
  "edit.open_dashboard": "Otevřít v panelu nastavení ↗",