ultra_settings 2.9.1 → 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 (55) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +36 -0
  3. data/README.md +24 -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 +119 -26
  8. data/app/application_vars.css.erb +40 -14
  9. data/app/configuration.html.erb +19 -1
  10. data/app/layout.css +41 -2
  11. data/app/layout.html.erb +46 -0
  12. data/app/layout_vars.css.erb +12 -8
  13. data/app/locales/ar.json +5 -2
  14. data/app/locales/cs.json +5 -2
  15. data/app/locales/da.json +5 -2
  16. data/app/locales/de.json +5 -2
  17. data/app/locales/el.json +5 -2
  18. data/app/locales/en.json +5 -2
  19. data/app/locales/es.json +5 -2
  20. data/app/locales/fa.json +5 -2
  21. data/app/locales/fr.json +5 -2
  22. data/app/locales/gd.json +5 -2
  23. data/app/locales/he.json +5 -2
  24. data/app/locales/hi.json +5 -2
  25. data/app/locales/it.json +5 -2
  26. data/app/locales/ja.json +5 -2
  27. data/app/locales/ko.json +5 -2
  28. data/app/locales/lt.json +5 -2
  29. data/app/locales/nb.json +5 -2
  30. data/app/locales/nl.json +5 -2
  31. data/app/locales/pl.json +5 -2
  32. data/app/locales/pt-br.json +5 -2
  33. data/app/locales/pt.json +5 -2
  34. data/app/locales/ru.json +5 -2
  35. data/app/locales/sv.json +5 -2
  36. data/app/locales/ta.json +5 -2
  37. data/app/locales/tr.json +5 -2
  38. data/app/locales/uk.json +5 -2
  39. data/app/locales/ur.json +5 -2
  40. data/app/locales/vi.json +5 -2
  41. data/app/locales/zh-cn.json +5 -2
  42. data/app/locales/zh-tw.json +5 -2
  43. data/lib/ultra_settings/application_view.rb +17 -7
  44. data/lib/ultra_settings/audit_data_sources.rb +1 -1
  45. data/lib/ultra_settings/coerce.rb +1 -1
  46. data/lib/ultra_settings/configuration.rb +36 -10
  47. data/lib/ultra_settings/configuration_view.rb +166 -9
  48. data/lib/ultra_settings/mini_i18n.rb +14 -10
  49. data/lib/ultra_settings/rack_app.rb +4 -4
  50. data/lib/ultra_settings/view_helper.rb +27 -5
  51. data/lib/ultra_settings/web_view.rb +26 -16
  52. data/lib/ultra_settings/yaml_config.rb +10 -2
  53. data/lib/ultra_settings.rb +66 -2
  54. data/ultra_settings.gemspec +1 -1
  55. metadata +2 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 802faa3f09b5b24f1d0cad36e7014bf84f092b48e7b81fa0b6f73798fbd071ff
4
- data.tar.gz: 3dd890b1e59e1ae946edd1eda0a3b509386d6d50a30d269a437ae982b73bb7ba
3
+ metadata.gz: 92ad90432ce22bfa615bda0956cbd80d8f06115c925b99b0a250dc9c76be7dcf
4
+ data.tar.gz: 3163c675d4ef57edab7cc438f0d1553bf160dd8003b03f14c59f30b7f39d79fb
5
5
  SHA512:
6
- metadata.gz: 0a1dd9d6c83b3999f8559a7b52c9c7195d106dfb5073e1f6366376a27c04a1e9bd27ef846e207b8041b7ad6b1863283959bf8c07d8cc8a90f1ef0178641e0948
7
- data.tar.gz: ddef9a41588c9aa97c4d8ad0da56e4e55089ea3a9e45d8edd3dcc8e9710fda83a156fcee36d9592a096497e0e24a2c32e255c0dafb52cf6d9771258abaef4b10
6
+ metadata.gz: b938c8d2275dfc72319485edde8bc36d5ae866d31225201967d98c9c78873256b838f682cd8709a52ca645b713eea578820baaf96cf8ebb8242debcdb355a9a0
7
+ data.tar.gz: e689fd1da5cc1a647475a61efb7586544ae6ae383c9bd1d057c738e7067f055341ab8e83c7f3b5103d0b49dd0700972d45ad4cbea9be2873b28611c68676d90b
data/CHANGELOG.md CHANGED
@@ -4,6 +4,42 @@ 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
+
33
+ ## 2.10.0
34
+
35
+ ### Added
36
+
37
+ - Added `dark_mode_selector` option to `UltraSettings::Application` to allow the web UI to automatically switch to dark mode based on a CSS selector in the host application.
38
+
39
+ ### Fixed
40
+
41
+ - Fixed configurations showing up multiple times in the web UI when the configuration class is reloaded in Rails development mode.
42
+
7
43
  ## 2.9.1
8
44
 
9
45
  ### Fixed
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`.
@@ -453,6 +466,12 @@ You can specify the color scheme by setting by providing the `color_scheme` opti
453
466
  UltraSettings::ApplicationView.new(color_scheme: :dark)
454
467
  ```
455
468
 
469
+ If your application already controls dark mode with a CSS selector (for example, a `data` attribute on the `<html>` element), you can pass a `dark_mode_selector` instead of a fixed color scheme. The settings UI will automatically switch to dark mode whenever the selector matches:
470
+
471
+ ```ruby
472
+ UltraSettings::ApplicationView.new(dark_mode_selector: "[data-theme=dark]")
473
+ ```
474
+
456
475
  You can specify the language to render the application in with the `locale` option to the `UltraSettings::ApplicationView` constructor. The default language is English but it can be changed to any language that has a corresponding JSON file in the `app/locales` directory.
457
476
 
458
477
  ```ruby
@@ -476,6 +495,11 @@ All authorization is handled by the SuperSettings API — the same permissions t
476
495
  > [!IMPORTANT]
477
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.
478
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
+
479
503
  ##### Embedding with Edit Support
480
504
 
481
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.9.1
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);
@@ -1052,16 +1146,17 @@ input[type="number"].ultra-settings-ss-input::-webkit-outer-spin-button {
1052
1146
 
1053
1147
  .ultra-settings-ss-btn-cancel {
1054
1148
  background: transparent;
1055
- color: var(--text-secondary);
1149
+ color: var(--btn-secondary-text, var(--text-secondary));
1056
1150
  }
1057
1151
 
1058
1152
  .ultra-settings-ss-btn-cancel:hover {
1059
1153
  background: var(--bg-code);
1154
+ color: var(--btn-secondary-hover-text, var(--text-primary));
1060
1155
  }
1061
1156
 
1062
1157
  .ultra-settings-ss-btn-save {
1063
1158
  background: var(--accent);
1064
- color: #fff;
1159
+ color: var(--btn-primary-text, #fff);
1065
1160
  border-color: var(--accent);
1066
1161
  }
1067
1162
 
@@ -1095,12 +1190,10 @@ input[type="number"].ultra-settings-ss-input::-webkit-outer-spin-button {
1095
1190
  color: var(--accent);
1096
1191
  text-decoration: none;
1097
1192
  cursor: pointer;
1098
- transition: opacity 0.15s;
1099
1193
  }
1100
1194
 
1101
1195
  .ultra-settings-ss-history-link:hover {
1102
1196
  text-decoration: underline;
1103
- opacity: 0.85;
1104
1197
  }
1105
1198
 
1106
1199
  .ultra-settings-ss-history {
@@ -1152,9 +1245,9 @@ input[type="number"].ultra-settings-ss-input::-webkit-outer-spin-button {
1152
1245
  }
1153
1246
 
1154
1247
  .ultra-settings-ss-history-entry-badge.deleted {
1155
- background: rgba(220,53,69,0.1);
1156
- color: #dc3545;
1157
- 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);
1158
1251
  }
1159
1252
 
1160
1253
  .ultra-settings-ss-history-entry-badge.created {
@@ -13,40 +13,49 @@
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);
29
+ --btn-primary-text: #ffffff;
30
+ --btn-secondary-text: #4b5563;
31
+ --btn-secondary-hover-text: #111827;
27
32
 
28
33
  /* Source palette */
29
34
  --src-env: #2563eb;
30
35
  --src-env-bg: #eff6ff;
31
36
  --src-env-border: #bfdbfe;
32
- --src-setting: #0d9488;
37
+ --src-setting: #0f766e;
33
38
  --src-setting-bg: #f0fdfa;
34
39
  --src-setting-border: #99f6e4;
35
40
  --src-yaml: #7c3aed;
36
41
  --src-yaml-bg: #f5f3ff;
37
42
  --src-yaml-border: #ddd6fe;
38
- --src-default: #6b7280;
43
+ --src-default: #5f6673;
39
44
  --src-default-bg: #f3f4f6;
40
45
  --src-default-border: #d1d5db;
41
46
 
42
47
  /* Status */
43
- --active-color: #059669;
44
- --active-bg: rgba(5, 150, 105, 0.05);
48
+ --active-color: #047857;
49
+ --active-bg: rgba(4, 120, 87, 0.05);
45
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);
46
55
 
47
56
  /* Badges */
48
57
  --badge-secret-bg: #fef2f2;
49
- --badge-secret-text: #dc2626;
58
+ --badge-secret-text: #b91c1c;
50
59
  --badge-secret-border: #fecaca;
51
60
  --badge-static-bg: #f0f2f5;
52
61
  --badge-static-text: #4b5563;
@@ -68,13 +77,18 @@
68
77
 
69
78
  /* Source row divider */
70
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");
71
83
  }
72
84
  <% end %>
73
85
 
74
86
  <% if color_scheme == :system %>
75
87
  @media (prefers-color-scheme: dark) {
88
+ <% elsif dark_mode_selector %>
89
+ <%= dark_mode_selector %> {
76
90
  <% end %>
77
- <% if color_scheme == :system || color_scheme == :dark %>
91
+ <% if color_scheme == :system || color_scheme == :dark || dark_mode_selector %>
78
92
  .ultra-settings, .ultra-settings-block {
79
93
  --font-ui: system-ui, -apple-system, sans-serif;
80
94
  --font-mono: "SF Mono", Menlo, Consolas, monospace;
@@ -89,17 +103,22 @@
89
103
  /* Borders */
90
104
  --border-light: #2a2d3a;
91
105
  --border-card: #252836;
106
+ /* Interactive control borders — 3:1 against every surface (WCAG 1.4.11) */
107
+ --border-input: #666c86;
92
108
 
93
109
  /* Text */
94
110
  --text-primary: #e4e6f0;
95
111
  --text-secondary: #9ba1b5;
96
- --text-tertiary: #5c6178;
112
+ --text-tertiary: #8f95ad;
97
113
 
98
114
  /* Accent — teal */
99
115
  --accent: #14b8a6;
100
116
  --accent-light: rgba(20, 184, 166, 0.10);
101
117
  --accent-lighter: rgba(20, 184, 166, 0.05);
102
118
  --accent-glow: rgba(20, 184, 166, 0.15);
119
+ --btn-primary-text: #04211d;
120
+ --btn-secondary-text: #c8cede;
121
+ --btn-secondary-hover-text: #e8ecf8;
103
122
 
104
123
  /* Source palette */
105
124
  --src-env: #60a5fa;
@@ -118,7 +137,11 @@
118
137
  /* Status */
119
138
  --active-color: #34d399;
120
139
  --active-bg: rgba(52, 211, 153, 0.08);
121
- --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);
122
145
 
123
146
  /* Badges */
124
147
  --badge-secret-bg: rgba(220, 38, 38, 0.12);
@@ -144,8 +167,11 @@
144
167
 
145
168
  /* Source row divider */
146
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");
147
173
  }
148
174
  <% end %>
149
- <% if color_scheme == :system %>
175
+ <% if color_scheme == :system || dark_mode_selector %>
150
176
  }
151
177
  <% end %>
@@ -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