studio-engine 0.111.3.rc1 → 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 +4 -4
- data/CHANGELOG.md +18 -0
- data/README.md +30 -0
- data/app/helpers/studio_navbar_helper.rb +9 -0
- data/app/views/layouts/_navbar.html.erb +14 -0
- data/lib/studio/navbar_signed_out_cta.rb +102 -0
- data/lib/studio/version.rb +1 -1
- data/lib/studio.rb +26 -0
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: '09a16af7f2551278541c2fed1ac5a1c360b37f5de50cfdbd0316f531ba691406'
|
|
4
|
+
data.tar.gz: e8b69c9399401bea6e9efd2e231dc2100d89bfd2c9d9032a9f64f431df8ef0c8
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
data/lib/studio/version.rb
CHANGED
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.
|
|
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-
|
|
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
|