ultra_settings 2.9.0 → 2.10.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d7b197136c21486ad05e4477bba4f7c0b9765dd3e073421ecfc327b3ed16f8bb
4
- data.tar.gz: af08178f5b3b47ccab7f7b7aaafba64bbf606759774fe8fdf4054d91be72b52e
3
+ metadata.gz: 013b301669011dc685fc7016e1b66c12a6c8890f7b8dc9ba5bdb63c188eee191
4
+ data.tar.gz: 1d558756ea2014cea76c2c98747ae0dd760e32a9526cb6548d61e93ea0e11e1b
5
5
  SHA512:
6
- metadata.gz: dde91f729527a598d414a162011b4d296884d9253d1616d7b38d0aa3f419f93666a7852ac7e9422ecd453e3e2629dc376584e1fa4e46955a33b36927d7567028
7
- data.tar.gz: db199c5cdb6945b13557641dee89d3ee9ae154632a38b3719d6fcd6c0614ad4b7c07a87e24144567c3363515febb7249b08786acfc76fe9dbf2338757573e381
6
+ metadata.gz: 866378e50cdd9016084f5f6b1db66d97a32c3f9108c2ac44aaff59e7412a4980e439f254b6aed704d7f64ee955b9c33e23c4e698a5d179d2f072452fff37c1c2
7
+ data.tar.gz: f262427eb43bb6a78bcb034ef276e98de5517eeb77283022fcd25a40af947de4dc6a7c123c1c45a6067b21aa36beaeca086a04b46ca7bc14b0d6fbe4590520d6
data/CHANGELOG.md CHANGED
@@ -4,6 +4,23 @@ 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.0
8
+
9
+ ### Added
10
+
11
+ - 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.
12
+
13
+ ### Fixed
14
+
15
+ - Fixed configurations showing up multiple times in the web UI when the configuration class is reloaded in Rails development mode.
16
+
17
+ ## 2.9.1
18
+
19
+ ### Fixed
20
+
21
+ - Fixed a bug where the web UI showed runtime settings as valid data sources for secret fields even when `UltraSettings.runtime_settings_secure` was set to `false`.
22
+ - Back button now works correctly in the web UI when navigating between list of configuations and individual configuration pages.
23
+
7
24
  ## 2.9.0
8
25
 
9
26
  ### Added
data/README.md CHANGED
@@ -453,6 +453,12 @@ You can specify the color scheme by setting by providing the `color_scheme` opti
453
453
  UltraSettings::ApplicationView.new(color_scheme: :dark)
454
454
  ```
455
455
 
456
+ 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:
457
+
458
+ ```ruby
459
+ UltraSettings::ApplicationView.new(dark_mode_selector: "[data-theme=dark]")
460
+ ```
461
+
456
462
  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
463
 
458
464
  ```ruby
data/VERSION CHANGED
@@ -1 +1 @@
1
- 2.9.0
1
+ 2.10.0
data/app/application.css CHANGED
@@ -1052,16 +1052,17 @@ input[type="number"].ultra-settings-ss-input::-webkit-outer-spin-button {
1052
1052
 
1053
1053
  .ultra-settings-ss-btn-cancel {
1054
1054
  background: transparent;
1055
- color: var(--text-secondary);
1055
+ color: var(--btn-secondary-text, var(--text-secondary));
1056
1056
  }
1057
1057
 
1058
1058
  .ultra-settings-ss-btn-cancel:hover {
1059
1059
  background: var(--bg-code);
1060
+ color: var(--btn-secondary-hover-text, var(--text-primary));
1060
1061
  }
1061
1062
 
1062
1063
  .ultra-settings-ss-btn-save {
1063
1064
  background: var(--accent);
1064
- color: #fff;
1065
+ color: var(--btn-primary-text, #fff);
1065
1066
  border-color: var(--accent);
1066
1067
  }
1067
1068
 
data/app/application.js CHANGED
@@ -64,7 +64,7 @@ document.addEventListener("DOMContentLoaded", () => {
64
64
  };
65
65
 
66
66
  // ── Config Selection ──
67
- const selectConfig = (configId) => {
67
+ const selectConfig = (configId, pushState) => {
68
68
  selectedConfigId = configId;
69
69
 
70
70
  // Show only the selected section
@@ -83,7 +83,11 @@ document.addEventListener("DOMContentLoaded", () => {
83
83
  // Update hash
84
84
  const configName = configId.replace(/^section-/, "");
85
85
  if (window.location.hash !== "#" + configName) {
86
- history.replaceState(null, "", "#" + configName);
86
+ if (pushState === false) {
87
+ history.replaceState(null, "", "#" + configName);
88
+ } else {
89
+ history.pushState(null, "", "#" + configName);
90
+ }
87
91
  }
88
92
 
89
93
  // Scroll to top
@@ -104,7 +108,7 @@ document.addEventListener("DOMContentLoaded", () => {
104
108
  }
105
109
  };
106
110
 
107
- const clearSelection = () => {
111
+ const clearSelection = (pushState) => {
108
112
  selectedConfigId = null;
109
113
 
110
114
  // Hide all sections
@@ -121,7 +125,11 @@ document.addEventListener("DOMContentLoaded", () => {
121
125
  configListItems.forEach(item => item.classList.remove("hidden"));
122
126
 
123
127
  // Clear hash
124
- history.replaceState(null, "", window.location.pathname + window.location.search);
128
+ if (pushState === false) {
129
+ history.replaceState(null, "", window.location.pathname + window.location.search);
130
+ } else {
131
+ history.pushState(null, "", window.location.pathname + window.location.search);
132
+ }
125
133
 
126
134
  // Animate detail → list
127
135
  if (configDetail && configList) {
@@ -165,23 +173,23 @@ document.addEventListener("DOMContentLoaded", () => {
165
173
  }
166
174
 
167
175
  // ── Hash-based Navigation ──
168
- const handleHash = () => {
176
+ const handleHash = (pushState) => {
169
177
  const hash = window.location.hash.replace(/^#/, "");
170
178
  if (hash) {
171
179
  const configId = "section-" + hash;
172
180
  const exists = Array.from(sections).some(s => s.dataset.configId === configId);
173
181
  if (exists) {
174
- selectConfig(configId);
182
+ selectConfig(configId, pushState);
175
183
  return;
176
184
  }
177
185
  }
178
186
  // No valid hash — if not single config, show list
179
187
  if (!singleConfig && selectedConfigId) {
180
- clearSelection();
188
+ clearSelection(pushState);
181
189
  }
182
190
  };
183
191
 
184
- window.addEventListener("hashchange", handleHash);
192
+ window.addEventListener("popstate", () => handleHash(false));
185
193
 
186
194
  // ── Single Config Auto-Select ──
187
195
  if (singleConfig) {
@@ -196,7 +204,7 @@ document.addEventListener("DOMContentLoaded", () => {
196
204
  selectConfig(storedConfig);
197
205
  }
198
206
  } else {
199
- handleHash();
207
+ handleHash(false);
200
208
  }
201
209
  }
202
210
 
@@ -24,6 +24,9 @@
24
24
  --accent-light: rgba(13, 148, 136, 0.06);
25
25
  --accent-lighter: rgba(13, 148, 136, 0.03);
26
26
  --accent-glow: rgba(13, 148, 136, 0.12);
27
+ --btn-primary-text: #ffffff;
28
+ --btn-secondary-text: #4b5563;
29
+ --btn-secondary-hover-text: #111827;
27
30
 
28
31
  /* Source palette */
29
32
  --src-env: #2563eb;
@@ -73,8 +76,10 @@
73
76
 
74
77
  <% if color_scheme == :system %>
75
78
  @media (prefers-color-scheme: dark) {
79
+ <% elsif dark_mode_selector %>
80
+ <%= dark_mode_selector %> {
76
81
  <% end %>
77
- <% if color_scheme == :system || color_scheme == :dark %>
82
+ <% if color_scheme == :system || color_scheme == :dark || dark_mode_selector %>
78
83
  .ultra-settings, .ultra-settings-block {
79
84
  --font-ui: system-ui, -apple-system, sans-serif;
80
85
  --font-mono: "SF Mono", Menlo, Consolas, monospace;
@@ -100,6 +105,9 @@
100
105
  --accent-light: rgba(20, 184, 166, 0.10);
101
106
  --accent-lighter: rgba(20, 184, 166, 0.05);
102
107
  --accent-glow: rgba(20, 184, 166, 0.15);
108
+ --btn-primary-text: #04211d;
109
+ --btn-secondary-text: #c8cede;
110
+ --btn-secondary-hover-text: #e8ecf8;
103
111
 
104
112
  /* Source palette */
105
113
  --src-env: #60a5fa;
@@ -146,6 +154,6 @@
146
154
  --border-source-row: rgba(255,255,255,0.04);
147
155
  }
148
156
  <% end %>
149
- <% if color_scheme == :system %>
157
+ <% if color_scheme == :system || dark_mode_selector %>
150
158
  }
151
159
  <% end %>
data/app/layout.css CHANGED
@@ -84,6 +84,45 @@ body {
84
84
  justify-content: flex-end;
85
85
  }
86
86
 
87
+ /* ── Theme toggle ── */
88
+
89
+ .ultra-settings-theme-toggle {
90
+ display: flex;
91
+ align-items: center;
92
+ justify-content: center;
93
+ width: 36px;
94
+ height: 36px;
95
+ border: 1px solid transparent;
96
+ border-radius: 8px;
97
+ background: transparent;
98
+ color: var(--text-tertiary, #9ca3af);
99
+ cursor: pointer;
100
+ flex-shrink: 0;
101
+ transition: color 0.2s, border-color 0.2s;
102
+ }
103
+
104
+ .ultra-settings-theme-toggle:hover {
105
+ color: var(--accent, #0d9488);
106
+ border-color: var(--accent, #0d9488);
107
+ }
108
+
109
+ .ultra-settings-theme-icon {
110
+ width: 18px;
111
+ height: 18px;
112
+ }
113
+
114
+ .ultra-settings-theme-icon-moon {
115
+ display: none;
116
+ }
117
+
118
+ [data-theme="dark"] .ultra-settings-theme-icon-sun {
119
+ display: none;
120
+ }
121
+
122
+ [data-theme="dark"] .ultra-settings-theme-icon-moon {
123
+ display: block;
124
+ }
125
+
87
126
  @media (max-width: 640px) {
88
127
  .ultra-settings-page-header-inner {
89
128
  padding: 16px;
data/app/layout.html.erb CHANGED
@@ -6,6 +6,15 @@
6
6
  <meta charset="utf-8">
7
7
  <meta name="viewport" content="width=device-width, initial-scale=1">
8
8
  <meta name="format-detection" content="telephone=no email=no date=no address=no">
9
+ <% if @dark_mode_selector %>
10
+ <script>
11
+ (function() {
12
+ if (localStorage.getItem("ultra_settings_theme") === "dark") {
13
+ document.documentElement.setAttribute("data-theme", "dark");
14
+ }
15
+ })();
16
+ </script>
17
+ <% end %>
9
18
  <style type="text/css">
10
19
  <%= layout_css %>
11
20
  </style>
@@ -25,6 +34,24 @@
25
34
  <p class="ultra-settings-page-header-subtitle"><%= t("page.brand_subtitle") %></p>
26
35
  </div>
27
36
  </div>
37
+ <% if @dark_mode_selector %>
38
+ <button type="button" class="ultra-settings-theme-toggle" id="ultra-settings-theme-toggle" aria-label="Toggle dark mode">
39
+ <svg class="ultra-settings-theme-icon ultra-settings-theme-icon-sun" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
40
+ <circle cx="12" cy="12" r="4"></circle>
41
+ <path d="M12 2v2"></path>
42
+ <path d="M12 20v2"></path>
43
+ <path d="m4.93 4.93 1.41 1.41"></path>
44
+ <path d="m17.66 17.66 1.41 1.41"></path>
45
+ <path d="M2 12h2"></path>
46
+ <path d="M20 12h2"></path>
47
+ <path d="m6.34 17.66-1.41 1.41"></path>
48
+ <path d="m19.07 4.93-1.41 1.41"></path>
49
+ </svg>
50
+ <svg class="ultra-settings-theme-icon ultra-settings-theme-icon-moon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
51
+ <path d="M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z"></path>
52
+ </svg>
53
+ </button>
54
+ <% end %>
28
55
  </div>
29
56
  </header>
30
57
  <main class="ultra-settings-page-content">
@@ -79,5 +106,24 @@
79
106
  </details>
80
107
  </footer>
81
108
  <% end %>
109
+ <% if @dark_mode_selector %>
110
+ <script>
111
+ (function() {
112
+ var toggle = document.getElementById("ultra-settings-theme-toggle");
113
+ if (!toggle) return;
114
+ var html = document.documentElement;
115
+ toggle.addEventListener("click", function() {
116
+ var isDark = html.getAttribute("data-theme") === "dark";
117
+ if (isDark) {
118
+ html.removeAttribute("data-theme");
119
+ localStorage.removeItem("ultra_settings_theme");
120
+ } else {
121
+ html.setAttribute("data-theme", "dark");
122
+ localStorage.setItem("ultra_settings_theme", "dark");
123
+ }
124
+ });
125
+ })();
126
+ </script>
127
+ <% end %>
82
128
  </body>
83
129
  </html>
@@ -19,15 +19,19 @@
19
19
 
20
20
  <% if color_scheme == :system %>
21
21
  @media (prefers-color-scheme: dark) {
22
- <% end %>
23
- <% if color_scheme == :system || color_scheme == :dark %>
22
+ :root {
23
+ <% elsif dark_mode_selector %>
24
+ :root<%= dark_mode_selector %> {
25
+ <% elsif color_scheme == :dark %>
24
26
  :root {
27
+ <% end %>
28
+ <% if color_scheme == :system || color_scheme == :dark || dark_mode_selector %>
25
29
  --font-ui: system-ui, -apple-system, sans-serif;
26
30
  --font-mono: "SF Mono", Menlo, Consolas, monospace;
27
31
  --bg-page: #0f1117;
28
- --text-primary: #e4e6f0;
29
- --text-secondary: #9ba1b5;
30
- --text-tertiary: #5c6178;
32
+ --text-primary: #eef0f8;
33
+ --text-secondary: #adb3c8;
34
+ --text-tertiary: #6e7490;
31
35
  --accent: #14b8a6;
32
36
  --border-light: #2a2d3a;
33
37
  --bg-card: #1a1c26;
@@ -18,9 +18,10 @@ module UltraSettings
18
18
  # Initialize the application view with a color scheme.
19
19
  #
20
20
  # @param color_scheme [Symbol] The color scheme to use (:light, :dark, or :system).
21
+ # @param dark_mode_selector [String, nil] The CSS selector for dark mode.
21
22
  # @param locale [String] The locale code for translations.
22
- def initialize(color_scheme: :light, locale: UltraSettings::MiniI18n::DEFAULT_LOCALE)
23
- @css = application_css(color_scheme)
23
+ def initialize(color_scheme: :light, dark_mode_selector: nil, locale: UltraSettings::MiniI18n::DEFAULT_LOCALE)
24
+ @css = application_css(color_scheme, dark_mode_selector)
24
25
  @css = @css.html_safe if @css.respond_to?(:html_safe)
25
26
  @locale = locale
26
27
  end
@@ -74,7 +75,7 @@ module UltraSettings
74
75
  ViewHelper.read_app_file("application.js")
75
76
  end
76
77
 
77
- def application_css(color_scheme)
78
+ def application_css(color_scheme, dark_mode_selector)
78
79
  vars = ViewHelper.erb_template("application_vars.css.erb").result(binding).strip
79
80
  css = ViewHelper.read_app_file("application.css").strip
80
81
  "#{vars}\n#{css}"
@@ -560,7 +560,7 @@ module UltraSettings
560
560
 
561
561
  sources = []
562
562
  sources << :env if field.env_var
563
- sources << :settings if !field.static? && field.runtime_setting && UltraSettings.__runtime_settings__
563
+ sources << :settings if __runtime_setting_allowed?(field)
564
564
  sources << :yaml if field.yaml_key && self.class.configuration_file
565
565
  sources << :default unless field.default.nil?
566
566
  sources
@@ -646,5 +646,14 @@ module UltraSettings
646
646
  def __yaml_config__
647
647
  @ultra_settings_yaml_config ||= self.class.load_yaml_config || {}
648
648
  end
649
+
650
+ def __runtime_setting_allowed?(field)
651
+ return false unless UltraSettings.__runtime_settings__
652
+ return false if field.static?
653
+ return false unless field.runtime_setting
654
+ return false if field.secret? && !UltraSettings.runtime_settings_secure?
655
+
656
+ true
657
+ end
649
658
  end
650
659
  end
@@ -9,12 +9,14 @@ module UltraSettings
9
9
 
10
10
  # Initialize a new WebView with the specified color scheme.
11
11
  #
12
- # @param color_scheme [Symbol] The color scheme to use in the UI. This can be `:light`,
13
- # `:dark`, or `:system`. The default is `:light`.
12
+ # @param color_scheme [Symbol, nil] The color scheme to use in the UI. This can be `:light`,
13
+ # `:dark`, or `:system`. When `nil`, a toggle control is rendered and
14
+ # `[data-theme=dark]` is used as the dark mode CSS selector.
14
15
  def initialize(color_scheme: :light)
15
- @color_scheme = (color_scheme || :light).to_sym
16
+ @color_scheme = color_scheme&.to_sym
17
+ @dark_mode_selector = @color_scheme.nil? ? "[data-theme=dark]" : nil
16
18
  @layout_template = ViewHelper.erb_template("layout.html.erb")
17
- @layout_css = scheme_layout_css(@color_scheme)
19
+ @layout_css = scheme_layout_css(@color_scheme, @dark_mode_selector)
18
20
  end
19
21
 
20
22
  # Render the complete settings page HTML.
@@ -34,7 +36,8 @@ module UltraSettings
34
36
  # @return [String] The HTML content for the settings.
35
37
  def content
36
38
  UltraSettings::ApplicationView.new(
37
- color_scheme: @color_scheme,
39
+ color_scheme: @color_scheme || :light,
40
+ dark_mode_selector: @dark_mode_selector,
38
41
  locale: @locale || UltraSettings::MiniI18n::DEFAULT_LOCALE
39
42
  ).render
40
43
  end
@@ -56,7 +59,7 @@ module UltraSettings
56
59
 
57
60
  private
58
61
 
59
- def scheme_layout_css(color_scheme)
62
+ def scheme_layout_css(color_scheme, dark_mode_selector)
60
63
  vars = ViewHelper.erb_template("layout_vars.css.erb").result(binding)
61
64
  css = ViewHelper.read_app_file("layout.css")
62
65
  "#{vars}\n#{css}"
@@ -299,7 +299,15 @@ module UltraSettings
299
299
  __load_config__(name, class_name)
300
300
  end
301
301
 
302
- config_classes = ObjectSpace.each_object(Class).select { |klass| klass < Configuration }
302
+ config_classes = ObjectSpace.each_object(Class).select do |klass|
303
+ next false unless klass < Configuration
304
+ next false if klass.name.nil?
305
+ begin
306
+ constantize(klass.name).equal?(klass)
307
+ rescue NameError
308
+ false
309
+ end
310
+ end
303
311
  config_classes.collect(&:instance)
304
312
  end
305
313
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ultra_settings
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.9.0
4
+ version: 2.10.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Brian Durand