studio-engine 0.111.3 → 0.112.0.rc1

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: 2ab9a9a4e4e6a34bf0a48ef2c2f06b9d1f6b3c2f9f0bbc17730e0cfb22180b0d
4
- data.tar.gz: d8a4142dbe8272eab927353dbd6885b143f6130b32b38bb20cdb2f720bb603b6
3
+ metadata.gz: '09a16af7f2551278541c2fed1ac5a1c360b37f5de50cfdbd0316f531ba691406'
4
+ data.tar.gz: e8b69c9399401bea6e9efd2e231dc2100d89bfd2c9d9032a9f64f431df8ef0c8
5
5
  SHA512:
6
- metadata.gz: bff9c7c9e6e8f4f10822fece60dcf57d18f2f5de4beabeedb2f0e55afafbc2faeda6421e455b9eca9e96faf400bac471a99789ba9b4cc2274b345cd453802ec1
7
- data.tar.gz: b6978c646770e0606fab504ba0f8700aeb86355bf0bcb7465a7c22a5d401d508f588feb4ccbefc3f917122fb9f992336e5efe35b687d23d5e0fb324e78aaaa21
6
+ metadata.gz: f25be26a1ef2ffc0a4684787878e221ddaf03b9ac5839a988780e9c1ad38d1199a755e36e6acfa9cff516fac27a1ceb7184724ce34f70ec158d8e7fa4f407049
7
+ data.tar.gz: 64ac19247ffaddbe3f2d9095c41ab19aeddad3f011f27b03894f659dd573aba832bedc781c27df9047e806021ef3a789e1d223f214a695e01da186b5e1c1970c
data/CHANGELOG.md CHANGED
@@ -4,6 +4,24 @@ The format is [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). This pro
4
4
 
5
5
  ## Unreleased
6
6
 
7
+ ## 0.112.0 — 2026-10-19
8
+
9
+ ### Added
10
+
11
+ - **`Studio.navbar_signed_out_cta`: a call to action on the signed-out navbar.**
12
+ Set it to `{ label: "Build App", path: "/build" }` (or a callable receiving
13
+ the view) and the signed-out primary button carries that label and path,
14
+ while sign-in stays one tap away as a quiet `Studio.sign_in_label` link to
15
+ `login_path`: beside the button from `md`, and in the mobile sub-navbar below
16
+ `md`, where the top row has no room for both. It is separate from
17
+ `Studio.sign_in_label`, which also words the engine's auth pages. Unset (the
18
+ default) the navbar renders byte-identical to before; signed in it changes
19
+ nothing. A bad Hash raises `Studio::NavbarSignedOutCta::InvalidConfig` at
20
+ assignment; a raising callable falls back to the sign-in button and is
21
+ reported once. `components/_user_nav`'s own signed-out branch (a sign-in
22
+ link and Sign up, reached through `show_logged_in: true`) is unchanged.
23
+ README, *Signed-out call to action*.
24
+
7
25
  ## 0.111.3 — 2026-10-18
8
26
 
9
27
  ### Fixed
data/README.md CHANGED
@@ -736,6 +736,36 @@ end
736
736
  *log-on link*. The "Sign in with Google" and "Sign up" buttons are not
737
737
  sign-in labels and do not change.
738
738
 
739
+ ### Signed-out call to action — `Studio.navbar_signed_out_cta`
740
+
741
+ What the signed-out navbar's primary button does. Unset (`nil`, the default),
742
+ it is the sign-in button, `Studio.sign_in_label` to `login_path`, and the navbar
743
+ renders the same bytes as before the setting. Rules:
744
+ `lib/studio/navbar_signed_out_cta.rb`.
745
+
746
+ ```ruby
747
+ Studio.configure do |config|
748
+ config.navbar_signed_out_cta = { label: "Build App", path: "/build" }
749
+ # or a callable receiving the view; nil keeps the sign-in button for that render
750
+ # config.navbar_signed_out_cta = ->(view) { { label: "Play", path: view.contests_path } }
751
+ end
752
+ ```
753
+
754
+ - Set, the primary button carries `label:` to `path:`, and sign-in stays one tap
755
+ away as a quiet text link (`Studio.sign_in_label` to `login_path`): beside the
756
+ button from `md`, and in the phone row under the header below `md`, where the
757
+ top row has no room for two controls beside the brand. Both sign-in links
758
+ carry `data-nav-sign-in`; the button carries `data-nav-signed-out-cta`.
759
+ - It is a separate setting from `sign_in_label` on purpose: that label also
760
+ words the engine's auth pages, so it cannot double as a marketing CTA.
761
+ - Signed in, the setting changes nothing.
762
+ - `label:` and `path:` are required non-blank text, both escaped. A bad Hash
763
+ raises `Studio::NavbarSignedOutCta::InvalidConfig` at assignment, a callable's
764
+ bad answer at render. A callable that raises falls back to the sign-in button
765
+ and is reported once per process per error class, never a 500.
766
+ - A host on an older engine can set it safely with
767
+ `Studio.navbar_signed_out_cta = … if Studio.respond_to?(:navbar_signed_out_cta=)`.
768
+
739
769
  ### User nav slots
740
770
 
741
771
  `components/_user_nav.html.erb` renders the right-side navbar user section.
@@ -11,6 +11,15 @@ module StudioNavbarHelper
11
11
  @studio_navbar_links ||= Studio.navbar_links_for(self)
12
12
  end
13
13
 
14
+ # The signed-out CTA ({ label:, path: }) or nil for the sign-in button.
15
+ # Resolved once per render pass: the desktop button and the phone row's
16
+ # sign-in link both ask, and a host callable may read the request.
17
+ def studio_navbar_signed_out_cta
18
+ return @studio_navbar_signed_out_cta if defined?(@studio_navbar_signed_out_cta)
19
+
20
+ @studio_navbar_signed_out_cta = Studio.navbar_signed_out_cta_for(self)
21
+ end
22
+
14
23
  # One resolved link. size is the text utility: text-sm on desktop, text-xs
15
24
  # in the phone row, matching the links apps hand-wrote in forked navbars.
16
25
  def studio_navbar_link_tag(link, size:)
@@ -31,6 +31,12 @@
31
31
  <%# Studio.navbar_links: links for the desktop bar and phone row.
32
32
  Empty by default, which renders the same bytes as before. %>
33
33
  <% navbar_links = respond_to?(:studio_navbar_links) ? studio_navbar_links : [] %>
34
+ <%# Studio.navbar_signed_out_cta: signed out, the primary button can carry a
35
+ call to action instead of sign-in. nil (the default) renders the same bytes
36
+ as before. Set, sign-in stays one tap away as a quiet text link: beside the
37
+ button from md, and in the phone row below md, where the top row has no
38
+ room for two controls beside the brand. %>
39
+ <% signed_out_cta = !show_user && respond_to?(:studio_navbar_signed_out_cta) ? studio_navbar_signed_out_cta : nil %>
34
40
  <% if sidebar_active %>
35
41
  <% sidebar_trigger_desktop = capture { render "components/link_sidebar_trigger", class_name: "hidden md:inline-flex" } %>
36
42
  <% extra_icons_html = safe_join([extra_icons_html, sidebar_trigger_desktop].compact) %>
@@ -196,7 +202,12 @@
196
202
  <div class="flex items-center justify-end gap-3">
197
203
  <%= sidebar_trigger_desktop if sidebar_active %>
198
204
  <span class="hidden md:flex"><%= render "components/theme_toggle_morph" %></span>
205
+ <% if signed_out_cta %>
206
+ <%= link_to Studio.sign_in_label, login_path, class: "hidden md:inline text-sm font-semibold text-secondary hover:text-primary whitespace-nowrap transition", data: { nav_sign_in: "" } %>
207
+ <%= link_to signed_out_cta[:label], signed_out_cta[:path], class: "btn btn-primary whitespace-nowrap", data: { nav_signed_out_cta: "" } %>
208
+ <% else %>
199
209
  <%= link_to Studio.sign_in_label, login_path, class: "btn btn-primary" %>
210
+ <% end %>
200
211
  </div>
201
212
  <% end %>
202
213
  </div>
@@ -211,6 +222,9 @@
211
222
  </nav>
212
223
  <% end %>
213
224
  <span class="ml-auto flex items-center gap-3">
225
+ <% if signed_out_cta %>
226
+ <%= link_to Studio.sign_in_label, login_path, class: "text-xs font-semibold text-secondary hover:text-primary whitespace-nowrap transition", data: { nav_sign_in: "" } %>
227
+ <% end %>
214
228
  <%= render "components/link_sidebar_trigger" if sidebar_active %>
215
229
  <%= render "components/admin_dropdown" unless sidebar_active && studio_sidebar_replaces_admin_menu? %>
216
230
  <%= render "components/theme_toggle_morph" %>
@@ -0,0 +1,102 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Resolves Studio.navbar_signed_out_cta, the call to action the engine navbar's
4
+ # signed-out primary button carries in place of sign-in. Pure Ruby, so the unit
5
+ # suite covers it without the dummy app.
6
+ #
7
+ # Studio.navbar_signed_out_cta is one of:
8
+ #
9
+ # nil the default: the button is the sign-in
10
+ # button (Studio.sign_in_label -> login_path),
11
+ # byte-identical to before the setting
12
+ # { label: "Build App", path: "/build" }
13
+ # the button carries this label and path, and
14
+ # a quiet sign-in text link sits beside it
15
+ # ->(view) { { label:, path: } } a callable receiving the view; answering nil
16
+ # keeps the default for that render
17
+ #
18
+ # It is separate from Studio.sign_in_label on purpose: that label also words the
19
+ # engine's own auth pages (lib/studio/auth_labels.rb), so it cannot be bent into
20
+ # a marketing CTA. The sign-in link always keeps it.
21
+ #
22
+ # A bad static Hash raises InvalidConfig at assignment; a callable's answer is
23
+ # checked when it resolves and raises the same error. A callable that RAISES
24
+ # is a runtime fault, not a shape fault: the navbar falls back to the sign-in
25
+ # button and the failure is reported once per process per error class, so a
26
+ # layout never 500s over a CTA.
27
+ module Studio
28
+ module NavbarSignedOutCta
29
+ class InvalidConfig < ArgumentError; end
30
+
31
+ KEYS = %i[label path].freeze
32
+
33
+ @reported = {}
34
+ @reported_lock = Mutex.new
35
+
36
+ module_function
37
+
38
+ # The CTA for one render, as { label:, path: } with plain String values, or
39
+ # nil for the default sign-in button.
40
+ def resolve(config, view)
41
+ return nil if config.nil?
42
+ return normalize(config) unless config.respond_to?(:call)
43
+
44
+ begin
45
+ answer = config.call(view)
46
+ rescue StandardError => e
47
+ report_once(e)
48
+ return nil
49
+ end
50
+ answer.nil? ? nil : normalize(answer)
51
+ end
52
+
53
+ # Shape check without a view: a static Hash fails at boot, not on the first
54
+ # signed-out page. A callable is checked when it resolves.
55
+ def validate!(config)
56
+ return nil if config.nil? || config.respond_to?(:call)
57
+
58
+ normalize(config)
59
+ nil
60
+ end
61
+
62
+ def normalize(config)
63
+ unless config.is_a?(Hash)
64
+ raise InvalidConfig, "Studio.navbar_signed_out_cta must be nil, a Hash { label:, path: } " \
65
+ "or a callable returning one (got #{config.class})"
66
+ end
67
+
68
+ config = config.to_h { |key, value| [key.to_sym, value] }
69
+ unknown = config.keys - KEYS
70
+ raise InvalidConfig, "Studio.navbar_signed_out_cta has unknown key :#{unknown.first}" if unknown.any?
71
+
72
+ KEYS.to_h do |key|
73
+ value = config[key]
74
+ unless (value.is_a?(String) || value.is_a?(Symbol)) && !value.to_s.strip.empty?
75
+ raise InvalidConfig, "Studio.navbar_signed_out_cta needs a non-blank :#{key} (got #{value.inspect})"
76
+ end
77
+
78
+ # String.new drops an html_safe flag, so ERB escapes whatever came back.
79
+ [key, String.new(value.to_s).strip]
80
+ end
81
+ end
82
+
83
+ # Forget what was reported, so a reconfigured callable reports its own failure.
84
+ def reset_reported!
85
+ @reported_lock.synchronize { @reported.clear }
86
+ end
87
+
88
+ def report_once(error)
89
+ first = @reported_lock.synchronize { @reported[error.class.name] ? false : (@reported[error.class.name] = true) }
90
+ return unless first
91
+
92
+ if defined?(::ErrorLog) && ::ErrorLog.respond_to?(:capture!)
93
+ ::ErrorLog.capture!(error)
94
+ elsif defined?(::Rails) && ::Rails.respond_to?(:logger) && ::Rails.logger
95
+ ::Rails.logger.warn("[studio-navbar] Studio.navbar_signed_out_cta failed, showing the sign-in button: " \
96
+ "#{error.class}: #{error.message}")
97
+ end
98
+ rescue StandardError
99
+ nil # reporting is best-effort: the navbar still renders
100
+ end
101
+ end
102
+ end
@@ -1,3 +1,3 @@
1
1
  module Studio
2
- VERSION = "0.111.3"
2
+ VERSION = "0.112.0.rc1"
3
3
  end
data/lib/studio.rb CHANGED
@@ -33,6 +33,7 @@ require "studio/sidebar_sections"
33
33
  require "studio/site_footer"
34
34
  require "studio/booking"
35
35
  require "studio/navbar_links"
36
+ require "studio/navbar_signed_out_cta"
36
37
  require "studio/navbar_identity"
37
38
  require "studio/navbar_wordmark"
38
39
  require "studio/auth_labels"
@@ -279,6 +280,25 @@ module Studio
279
280
  @@sign_in_label = label
280
281
  end
281
282
 
283
+ # ---- Navbar signed-out CTA ----
284
+ # What the signed-out navbar's primary button does. nil, the default, keeps
285
+ # the sign-in button (sign_in_label -> login_path) byte-identical to before.
286
+ # Set, the button carries this label and path and a quiet sign-in text link
287
+ # (sign_in_label -> login_path) sits beside it on desktop and in the phone row
288
+ # under it. A Hash, or a callable receiving the view (nil keeps the default).
289
+ # Separate from sign_in_label, which also words the engine's auth pages.
290
+ # Rules: lib/studio/navbar_signed_out_cta.rb.
291
+ #
292
+ # config.navbar_signed_out_cta = { label: "Build App", path: "/build" }
293
+ # config.navbar_signed_out_cta = ->(view) { { label: "Play", path: view.contests_path } }
294
+ mattr_reader :navbar_signed_out_cta, default: nil
295
+
296
+ def self.navbar_signed_out_cta=(config)
297
+ NavbarSignedOutCta.validate!(config)
298
+ NavbarSignedOutCta.reset_reported!
299
+ @@navbar_signed_out_cta = config
300
+ end
301
+
282
302
  # ---- The shared profile page (/profile) ----
283
303
  # The rows that make up /profile. `nil` — the default — means "the engine's
284
304
  # standard page", NOT "no rows": a brand-new app with an empty initializer gets
@@ -1414,6 +1434,12 @@ module Studio
1414
1434
  NavbarLinks.resolve(navbar_links, view)
1415
1435
  end
1416
1436
 
1437
+ # The signed-out CTA for a view, or nil for the sign-in button
1438
+ # (lib/studio/navbar_signed_out_cta.rb).
1439
+ def self.navbar_signed_out_cta_for(view)
1440
+ NavbarSignedOutCta.resolve(navbar_signed_out_cta, view)
1441
+ end
1442
+
1417
1443
  # The signed-in user's navbar name for a view (lib/studio/navbar_identity.rb).
1418
1444
  def self.navbar_user_name_for(user, view = nil)
1419
1445
  NavbarIdentity.user_name(user, view, config: navbar_user_name)
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: studio-engine
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.111.3
4
+ version: 0.112.0.rc1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alex McRitchie
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-10-19 00:00:00.000000000 Z
11
+ date: 2026-10-20 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -763,6 +763,7 @@ files:
763
763
  - lib/studio/name_parts.rb
764
764
  - lib/studio/navbar_identity.rb
765
765
  - lib/studio/navbar_links.rb
766
+ - lib/studio/navbar_signed_out_cta.rb
766
767
  - lib/studio/navbar_wordmark.rb
767
768
  - lib/studio/newsletter.rb
768
769
  - lib/studio/oauth_identity.rb