studio-engine 0.77.3 → 0.78.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 +4 -4
- data/CHANGELOG.md +29 -0
- data/README.md +28 -0
- data/app/controllers/studio/emails_controller.rb +8 -2
- data/app/helpers/studio_navbar_helper.rb +24 -0
- data/app/services/studio/email_catalog.rb +26 -1
- data/app/views/layouts/_navbar.html.erb +16 -3
- data/app/views/studio/emails/index.html.erb +19 -8
- data/app/views/studio/emails/orphan.html.erb +7 -3
- data/lib/studio/navbar_links.rb +92 -0
- data/lib/studio/version.rb +1 -1
- data/lib/studio.rb +23 -0
- metadata +4 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 12e1056052d4d49cd70e0ca81e8a39fc4f1c370166d73ea319315337fc648f5f
|
|
4
|
+
data.tar.gz: 0d94f90bff68c27a6c86d07ad53904b330a3717121f5f944244d0153a2e53a63
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: cb929bb288de471829b57f90cfbf01fff7d4e51d68607da51b32df832ad4946662a0617a85002ec6244c23fb3d3d17db069528b090690378ba68a7f53669141a
|
|
7
|
+
data.tar.gz: bb9eb9c389d8d89887ba72b0bff91b080944b501c3564aed06b26b317f013445f6fd9636d327ba20e70cfd7e4d3065928b6d091eed8c22160e6b819de69825ba
|
data/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,35 @@ The format is [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). This pro
|
|
|
4
4
|
|
|
5
5
|
## Unreleased
|
|
6
6
|
|
|
7
|
+
## 0.78.0 — 2026-09-29
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- **`Studio.navbar_links` puts app links in the engine navbar.** An Array, or a
|
|
12
|
+
callable receiving the view, of `{ label:, href:, active:, badge: }` renders in
|
|
13
|
+
the desktop bar and the phone row, with `aria-current` on the active link and
|
|
14
|
+
an optional badge such as `"#12"`. An app that registers nothing renders the
|
|
15
|
+
same navbar bytes as before. See the README, *Navigation*.
|
|
16
|
+
|
|
17
|
+
## 0.77.4 — 2026-09-28
|
|
18
|
+
|
|
19
|
+
### Fixed
|
|
20
|
+
|
|
21
|
+
- **An email banner stored on R2 with no public URL no longer breaks sign-in.**
|
|
22
|
+
With `Studio::S3` on an S3-compatible endpoint (Cloudflare R2) and no
|
|
23
|
+
`s3_public_url`, a banner upload on `/admin/emails` used to succeed, after which
|
|
24
|
+
`ImageCache#url` raised `NotConfigured` on every read: the magic-link sign-in
|
|
25
|
+
email, the newsletter and profile emails, and the orphan page all failed.
|
|
26
|
+
- `Studio::EmailCatalog.uploads_available?` is now false in that state, and the
|
|
27
|
+
new `uploads_blocked_reason` (`:no_storage` or `:no_public_url`) lets the page
|
|
28
|
+
name the setting that fixes it: `s3_public_url`, not `s3_bucket_prefix`.
|
|
29
|
+
- `EmailCatalog.url` degrades to `nil` when a stored image cannot be served, so
|
|
30
|
+
`resolved_url` and every host's `url(...) || its_own_asset` fall back to the
|
|
31
|
+
default banner. Other errors still raise, and `ImageCache#url` stays loud
|
|
32
|
+
everywhere else.
|
|
33
|
+
- The orphan page previews through the same safe read.
|
|
34
|
+
Apps on AWS, or on R2 with `s3_public_url` set, behave exactly as before.
|
|
35
|
+
|
|
7
36
|
## 0.77.3 — 2026-09-28
|
|
8
37
|
|
|
9
38
|
## 0.77.2 — 2026-09-28
|
data/README.md
CHANGED
|
@@ -583,6 +583,34 @@ undefined, so mount `studio/modals/host` before writing a trigger.
|
|
|
583
583
|
Full contract, including what the partial may assume: the doc comment at the top
|
|
584
584
|
of `app/views/style/_host.html.erb`.
|
|
585
585
|
|
|
586
|
+
### Navigation — `Studio.sidebar_sections` and `Studio.navbar_links`
|
|
587
|
+
|
|
588
|
+
Two seams, both empty by default, so the navbar is unchanged until an app opts in.
|
|
589
|
+
|
|
590
|
+
- `sidebar_sections` fills the slide-out link sidebar (the cog trigger in the icon
|
|
591
|
+
rail). Rules: `lib/studio/sidebar_sections.rb`.
|
|
592
|
+
- `navbar_links` fills the navbar's own link slots: the desktop bar beside the
|
|
593
|
+
logo and the phone row under it. Rules: `lib/studio/navbar_links.rb`.
|
|
594
|
+
|
|
595
|
+
```ruby
|
|
596
|
+
Studio.configure do |config|
|
|
597
|
+
# An Array, or a callable receiving the view (so a badge can read current_user).
|
|
598
|
+
config.navbar_links = ->(view) {
|
|
599
|
+
[ { label: "Contests", href: view.contests_path, active: %r{\A/contests} },
|
|
600
|
+
{ label: "Rank", href: "/rank",
|
|
601
|
+
badge: (view.logged_in? ? "##{view.current_user.rank}" : nil) } ]
|
|
602
|
+
}
|
|
603
|
+
end
|
|
604
|
+
```
|
|
605
|
+
|
|
606
|
+
- `label:` and `href:` are required; both are escaped.
|
|
607
|
+
- `active:` is `true`/`false`, a Regexp, or a callable taking `request.path`.
|
|
608
|
+
Omitted, a link is active when the path equals its href's path. The active
|
|
609
|
+
link gets `aria-current="page"` and the primary tone.
|
|
610
|
+
- `badge:` is optional short text, such as `"#12"`, shown as a pill in the link.
|
|
611
|
+
- A bad entry raises `Studio::NavbarLinks::InvalidLink` naming its index: at
|
|
612
|
+
assignment for a static Array, at render for a callable.
|
|
613
|
+
|
|
586
614
|
### User nav slots
|
|
587
615
|
|
|
588
616
|
`components/_user_nav.html.erb` renders the right-side navbar user section.
|
|
@@ -23,6 +23,7 @@ module Studio
|
|
|
23
23
|
def index
|
|
24
24
|
@entries = Studio::EmailCatalog.entries
|
|
25
25
|
@uploads_available = Studio::EmailCatalog.uploads_available?
|
|
26
|
+
@uploads_blocked_reason = Studio::EmailCatalog.uploads_blocked_reason
|
|
26
27
|
|
|
27
28
|
# The list renders every banner and subject AS THEY WOULD ARRIVE, which has
|
|
28
29
|
# no answer until someone is receiving them.
|
|
@@ -44,6 +45,7 @@ module Studio
|
|
|
44
45
|
@subject = Studio::EmailCatalog.preview_subject(@key)
|
|
45
46
|
@preview_error = Studio::EmailCatalog.preview_error(@key)
|
|
46
47
|
@uploads_available = Studio::EmailCatalog.uploads_available?
|
|
48
|
+
@uploads_blocked_reason = Studio::EmailCatalog.uploads_blocked_reason
|
|
47
49
|
|
|
48
50
|
# WHO this is previewed as. The banner greets by name, so "what does this
|
|
49
51
|
# email look like" has no answer until someone is receiving it — and the
|
|
@@ -241,8 +243,12 @@ module Studio
|
|
|
241
243
|
def require_uploads
|
|
242
244
|
return if Studio::EmailCatalog.uploads_available?
|
|
243
245
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
+
alert = if Studio::EmailCatalog.uploads_blocked_reason == :no_public_url
|
|
247
|
+
"This app's storage has no public URL yet, so email images can't be changed here."
|
|
248
|
+
else
|
|
249
|
+
"This app has no object storage configured, so email images can't be changed here yet."
|
|
250
|
+
end
|
|
251
|
+
redirect_to admin_emails_path, status: :see_other, alert: alert
|
|
246
252
|
end
|
|
247
253
|
|
|
248
254
|
# Shown INSIDE the preview iframe when the builder is missing or raised.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# The navbar-links view seam. Links resolve once per render pass, since the
|
|
4
|
+
# host lambda may read current_user or the database.
|
|
5
|
+
module StudioNavbarHelper
|
|
6
|
+
NAVBAR_LINK_CLASS = "inline-flex items-center gap-1.5 whitespace-nowrap font-medium transition"
|
|
7
|
+
NAVBAR_BADGE_CLASS = "rounded-full border border-subtle px-1.5 py-0.5 text-[10px] leading-none " \
|
|
8
|
+
"font-semibold text-muted tabular-nums"
|
|
9
|
+
|
|
10
|
+
def studio_navbar_links
|
|
11
|
+
@studio_navbar_links ||= Studio.navbar_links_for(self)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# One resolved link. size is the text utility: text-sm on desktop, text-xs
|
|
15
|
+
# in the phone row, matching the links apps hand-wrote in forked navbars.
|
|
16
|
+
def studio_navbar_link_tag(link, size:)
|
|
17
|
+
tone = link[:active] ? "text-primary" : "text-secondary hover:text-primary"
|
|
18
|
+
link_to link[:href], class: "#{NAVBAR_LINK_CLASS} #{size} #{tone}",
|
|
19
|
+
aria: { current: ("page" if link[:active]) } do
|
|
20
|
+
badge = content_tag(:span, link[:badge], class: NAVBAR_BADGE_CLASS) if link[:badge]
|
|
21
|
+
safe_join([link[:label], badge].compact)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -712,6 +712,13 @@ module Studio
|
|
|
712
712
|
# caller keeps the behavior it was written against until its app adopts.
|
|
713
713
|
def url(key)
|
|
714
714
|
record(key)&.url
|
|
715
|
+
rescue Studio::S3::NotConfigured => e
|
|
716
|
+
# A stored override this app can no longer serve publicly (Studio::S3 on
|
|
717
|
+
# an S3-compatible endpoint with no s3_public_url). Every caller already
|
|
718
|
+
# treats nil as "use the default", so degrade to that instead of raising
|
|
719
|
+
# out of a mailer: this method feeds the magic-link sign-in email.
|
|
720
|
+
warn_unservable(key, e)
|
|
721
|
+
nil
|
|
715
722
|
end
|
|
716
723
|
|
|
717
724
|
# What ACTUALLY SHIPS on this email — the two-layer resolution. Absolute, so
|
|
@@ -845,13 +852,31 @@ module Studio
|
|
|
845
852
|
host ? "#{host}#{path}" : path
|
|
846
853
|
end
|
|
847
854
|
|
|
855
|
+
def warn_unservable(key, error)
|
|
856
|
+
message = "[email_catalog] #{key}: stored image not publicly servable, using default (#{error.message})"
|
|
857
|
+
defined?(Rails) && Rails.respond_to?(:logger) && Rails.logger ? Rails.logger.warn(message) : warn(message)
|
|
858
|
+
end
|
|
859
|
+
|
|
848
860
|
# --- Upload ------------------------------------------------------------
|
|
849
861
|
|
|
850
862
|
# Whether THIS app can accept an upload. False when the host never set
|
|
851
863
|
# Studio.s3_bucket_prefix — /admin/emails then shows inherited defaults
|
|
852
864
|
# read-only rather than 500ing on the first upload.
|
|
853
865
|
def uploads_available?
|
|
854
|
-
|
|
866
|
+
uploads_blocked_reason.nil?
|
|
867
|
+
end
|
|
868
|
+
|
|
869
|
+
# Why uploads are off, or nil when they are on:
|
|
870
|
+
# :no_storage the host never set Studio.s3_bucket_prefix (or the table
|
|
871
|
+
# is not migrated yet)
|
|
872
|
+
# :no_public_url storage is an S3-compatible endpoint (Cloudflare R2) with
|
|
873
|
+
# no Studio.s3_public_url, so an uploaded image could be
|
|
874
|
+
# stored but never served to an inbox
|
|
875
|
+
def uploads_blocked_reason
|
|
876
|
+
return :no_storage unless Studio::S3.configured? && table_ready?
|
|
877
|
+
return :no_public_url unless Studio::S3.public_url?
|
|
878
|
+
|
|
879
|
+
nil
|
|
855
880
|
end
|
|
856
881
|
|
|
857
882
|
# Upload bytes to this app's bucket + upsert its ImageCache row (replacing
|
|
@@ -26,6 +26,9 @@
|
|
|
26
26
|
Preview renders skip it too: the /admin/navbar page renders this partial
|
|
27
27
|
repeatedly and the panels carry page-unique ids. %>
|
|
28
28
|
<% sidebar_active = !is_preview && respond_to?(:studio_sidebar?) && studio_sidebar? %>
|
|
29
|
+
<%# Studio.navbar_links: links for the desktop bar and phone row.
|
|
30
|
+
Empty by default, which renders the same bytes as before. %>
|
|
31
|
+
<% navbar_links = respond_to?(:studio_navbar_links) ? studio_navbar_links : [] %>
|
|
29
32
|
<% if sidebar_active %>
|
|
30
33
|
<% sidebar_trigger_desktop = capture { render "components/link_sidebar_trigger", class_name: "hidden md:inline-flex" } %>
|
|
31
34
|
<% extra_icons_html = safe_join([extra_icons_html, sidebar_trigger_desktop].compact) %>
|
|
@@ -141,8 +144,11 @@
|
|
|
141
144
|
spans drawing outside it, which is the bug rather than the fix. %>
|
|
142
145
|
<h1 class="nav-title font-extrabold text-heading tracking-tight min-w-0"><span class="truncate"><%= prefix %></span><span class="text-primary truncate"><%= last_word %></span></h1>
|
|
143
146
|
<% end %>
|
|
144
|
-
<%# Desktop nav —
|
|
145
|
-
<nav class="hidden md:flex items-center gap-4"
|
|
147
|
+
<%# Desktop nav — Studio.navbar_links %>
|
|
148
|
+
<nav class="hidden md:flex items-center gap-4"<%= ' aria-label="Main"'.html_safe if navbar_links.any? %>>
|
|
149
|
+
<% navbar_links.each do |link| %>
|
|
150
|
+
<%= studio_navbar_link_tag(link, size: "text-sm") %>
|
|
151
|
+
<% end %>
|
|
146
152
|
</nav>
|
|
147
153
|
</div>
|
|
148
154
|
</div>
|
|
@@ -161,8 +167,15 @@
|
|
|
161
167
|
<% end %>
|
|
162
168
|
</div>
|
|
163
169
|
</div>
|
|
164
|
-
<%# Mobile sub-navbar: compact row hidden on desktop —
|
|
170
|
+
<%# Mobile sub-navbar: compact row hidden on desktop — Studio.navbar_links on the left %>
|
|
165
171
|
<div class="flex md:hidden items-center gap-3 px-4 py-1.5 border-t border-subtle bg-surface-alt">
|
|
172
|
+
<% if navbar_links.any? %>
|
|
173
|
+
<nav aria-label="Main" class="flex items-center gap-3 overflow-x-auto">
|
|
174
|
+
<% navbar_links.each do |link| %>
|
|
175
|
+
<%= studio_navbar_link_tag(link, size: "text-xs") %>
|
|
176
|
+
<% end %>
|
|
177
|
+
</nav>
|
|
178
|
+
<% end %>
|
|
166
179
|
<span class="ml-auto flex items-center gap-3">
|
|
167
180
|
<%= render "components/link_sidebar_trigger" if sidebar_active %>
|
|
168
181
|
<%= render "components/admin_dropdown" unless sidebar_active && studio_sidebar_replaces_admin_menu? %>
|
|
@@ -76,14 +76,25 @@
|
|
|
76
76
|
<% unless @uploads_available %>
|
|
77
77
|
<div class="mb-6 rounded-xl border border-subtle bg-inset px-4 py-3">
|
|
78
78
|
<p class="text-sm font-semibold text-heading">Read-only on this app</p>
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
79
|
+
<% if @uploads_blocked_reason == :no_public_url %>
|
|
80
|
+
<p class="text-sm text-body mt-1">
|
|
81
|
+
<%= Studio.app_name %> stores files where they cannot be served to an inbox yet,
|
|
82
|
+
so the images below can be viewed but not replaced. They are what this app is
|
|
83
|
+
sending right now. Set
|
|
84
|
+
<code class="font-mono text-2xs bg-surface px-1.5 py-0.5 rounded">config.s3_public_url</code>
|
|
85
|
+
in <code class="font-mono text-2xs bg-surface px-1.5 py-0.5 rounded">config/initializers/studio.rb</code>
|
|
86
|
+
to turn on uploads.
|
|
87
|
+
</p>
|
|
88
|
+
<% else %>
|
|
89
|
+
<p class="text-sm text-body mt-1">
|
|
90
|
+
<%= Studio.app_name %> has no object storage configured, so the images below
|
|
91
|
+
can be viewed but not replaced yet. They are the shared defaults that ship
|
|
92
|
+
with the engine, and they are what this app is sending right now. Set
|
|
93
|
+
<code class="font-mono text-2xs bg-surface px-1.5 py-0.5 rounded">config.s3_bucket_prefix</code>
|
|
94
|
+
in <code class="font-mono text-2xs bg-surface px-1.5 py-0.5 rounded">config/initializers/studio.rb</code>
|
|
95
|
+
to turn on uploads.
|
|
96
|
+
</p>
|
|
97
|
+
<% end %>
|
|
87
98
|
</div>
|
|
88
99
|
<% end %>
|
|
89
100
|
|
|
@@ -28,9 +28,13 @@
|
|
|
28
28
|
</p>
|
|
29
29
|
|
|
30
30
|
<% if (record = Studio::EmailCatalog.record(@key)) %>
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
31
|
+
<%# EmailCatalog.url, not record.url: it degrades to nil when the stored image
|
|
32
|
+
cannot be served publicly (R2 with no s3_public_url) instead of 500ing. %>
|
|
33
|
+
<% if (preview = Studio::EmailCatalog.url(@key)) %>
|
|
34
|
+
<div class="rounded-lg overflow-hidden border border-subtle w-56 mt-4">
|
|
35
|
+
<%= image_tag preview, class: "w-full h-auto", alt: "Stored artwork for #{@key}" %>
|
|
36
|
+
</div>
|
|
37
|
+
<% end %>
|
|
34
38
|
<p class="font-mono text-2xs text-muted mt-2 break-all"><%= record.s3_key %></p>
|
|
35
39
|
<% end %>
|
|
36
40
|
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Resolves Studio.navbar_links, the links the engine navbar renders in its
|
|
4
|
+
# desktop bar and phone row. Pure Ruby, so the unit suite covers it without
|
|
5
|
+
# the dummy app.
|
|
6
|
+
#
|
|
7
|
+
# Declared as an Array or a callable (receives the view context, so a badge can
|
|
8
|
+
# read current_user). Each entry resolves to:
|
|
9
|
+
#
|
|
10
|
+
# { label: "Contests", href: "/contests", active: true, badge: "#12" }
|
|
11
|
+
#
|
|
12
|
+
# `active` may be true/false, a Regexp or a callable matched against
|
|
13
|
+
# request.path; omitted, it is true when the path equals the href's path.
|
|
14
|
+
# `badge` is optional short text. A bad entry raises InvalidLink by index.
|
|
15
|
+
module Studio
|
|
16
|
+
module NavbarLinks
|
|
17
|
+
class InvalidLink < ArgumentError; end
|
|
18
|
+
|
|
19
|
+
KEYS = %i[label href active badge].freeze
|
|
20
|
+
|
|
21
|
+
module_function
|
|
22
|
+
|
|
23
|
+
def resolve(declared, view)
|
|
24
|
+
links = declared.respond_to?(:call) ? declared.call(view) : declared
|
|
25
|
+
path = request_path(view)
|
|
26
|
+
entries(links).each_with_index.map do |link, index|
|
|
27
|
+
link = check!(link, index)
|
|
28
|
+
{ label: link[:label].to_s, href: link[:href].to_s,
|
|
29
|
+
active: active?(link, path), badge: badge(link[:badge]) }
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Shape check without a request: a static list fails at boot, not on the
|
|
34
|
+
# first page render. A callable is checked when it resolves.
|
|
35
|
+
def validate!(declared)
|
|
36
|
+
return nil if declared.respond_to?(:call)
|
|
37
|
+
|
|
38
|
+
entries(declared).each_with_index { |link, index| check!(link, index) }
|
|
39
|
+
nil
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def entries(links)
|
|
43
|
+
return [] if links.nil?
|
|
44
|
+
return links if links.is_a?(Array)
|
|
45
|
+
|
|
46
|
+
raise InvalidLink, "Studio.navbar_links must be an Array or a callable returning an Array " \
|
|
47
|
+
"(got #{links.class})"
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def check!(link, index)
|
|
51
|
+
where = "Studio.navbar_links[#{index}]"
|
|
52
|
+
raise InvalidLink, "#{where} must be a Hash (got #{link.class})" unless link.is_a?(Hash)
|
|
53
|
+
|
|
54
|
+
link = link.to_h { |key, value| [key.to_sym, value] }
|
|
55
|
+
unknown = link.keys - KEYS
|
|
56
|
+
raise InvalidLink, "#{where} has unknown key :#{unknown.first}" if unknown.any?
|
|
57
|
+
|
|
58
|
+
%i[label href].each do |key|
|
|
59
|
+
raise InvalidLink, "#{where} needs a :#{key}" if link[key].to_s.strip.empty?
|
|
60
|
+
end
|
|
61
|
+
unless [nil, true, false].include?(link[:active]) || link[:active].is_a?(Regexp) ||
|
|
62
|
+
link[:active].respond_to?(:call)
|
|
63
|
+
raise InvalidLink, "#{where} :active must be true, false, a Regexp or a callable"
|
|
64
|
+
end
|
|
65
|
+
unless link[:badge].nil? || [String, Symbol, Numeric].any? { |type| link[:badge].is_a?(type) }
|
|
66
|
+
raise InvalidLink, "#{where} :badge must be short text"
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
link
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def active?(link, path)
|
|
73
|
+
case (rule = link[:active])
|
|
74
|
+
when true, false then rule
|
|
75
|
+
when nil then !path.nil? && path == link[:href].to_s.split(/[?#]/).first
|
|
76
|
+
when Regexp then !path.nil? && rule.match?(path)
|
|
77
|
+
else !path.nil? && rule.call(path) ? true : false
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def badge(value)
|
|
82
|
+
text = value.to_s.strip
|
|
83
|
+
text.empty? ? nil : text
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def request_path(view)
|
|
87
|
+
return nil unless view.respond_to?(:request)
|
|
88
|
+
|
|
89
|
+
view.request&.path
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
data/lib/studio/version.rb
CHANGED
data/lib/studio.rb
CHANGED
|
@@ -12,6 +12,7 @@ require "studio/js_literal"
|
|
|
12
12
|
require "studio/js_identifier"
|
|
13
13
|
require "studio/partial_block"
|
|
14
14
|
require "studio/sidebar_sections"
|
|
15
|
+
require "studio/navbar_links"
|
|
15
16
|
require "studio/profile_sections"
|
|
16
17
|
require "studio/profile_image"
|
|
17
18
|
require "studio/oauth_identity"
|
|
@@ -174,6 +175,23 @@ module Studio
|
|
|
174
175
|
# end
|
|
175
176
|
mattr_accessor :sidebar_sections, default: []
|
|
176
177
|
|
|
178
|
+
# ---- Navbar links ----
|
|
179
|
+
# Links for the engine navbar's desktop bar and phone row. The default []
|
|
180
|
+
# renders nothing, so the navbar stays byte-identical until an app opts in.
|
|
181
|
+
# An Array, or a callable receiving the view, of
|
|
182
|
+
# { label:, href:, active: (bool, Regexp or ->(path)), badge: (optional) }.
|
|
183
|
+
# Rules: lib/studio/navbar_links.rb.
|
|
184
|
+
#
|
|
185
|
+
# config.navbar_links = ->(view) {
|
|
186
|
+
# [ { label: "Contests", href: view.contests_path, active: %r{\A/contests} },
|
|
187
|
+
# { label: "Rank", href: "/rank", badge: ("#12" if view.logged_in?) } ] }
|
|
188
|
+
mattr_reader :navbar_links, default: []
|
|
189
|
+
|
|
190
|
+
def self.navbar_links=(links)
|
|
191
|
+
NavbarLinks.validate!(links)
|
|
192
|
+
@@navbar_links = links
|
|
193
|
+
end
|
|
194
|
+
|
|
177
195
|
# ---- The shared profile page (/profile) ----
|
|
178
196
|
# The rows that make up /profile. `nil` — the default — means "the engine's
|
|
179
197
|
# standard page", NOT "no rows": a brand-new app with an empty initializer gets
|
|
@@ -877,6 +895,11 @@ module Studio
|
|
|
877
895
|
SidebarSections.resolve(sidebar_sections, view)
|
|
878
896
|
end
|
|
879
897
|
|
|
898
|
+
# Navbar links resolved for a view context (lib/studio/navbar_links.rb).
|
|
899
|
+
def self.navbar_links_for(view)
|
|
900
|
+
NavbarLinks.resolve(navbar_links, view)
|
|
901
|
+
end
|
|
902
|
+
|
|
880
903
|
# The engine's standard /profile rows. Hosts compose against this rather than
|
|
881
904
|
# restating a literal list, so a later release that adds a standard row
|
|
882
905
|
# delivers it to every app that used the seam as intended.
|
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.78.0
|
|
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-09-
|
|
11
|
+
date: 2026-09-29 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rails
|
|
@@ -345,6 +345,7 @@ files:
|
|
|
345
345
|
- app/helpers/studio/fizz_helper.rb
|
|
346
346
|
- app/helpers/studio/geo_helper.rb
|
|
347
347
|
- app/helpers/studio_email_delivery_helper.rb
|
|
348
|
+
- app/helpers/studio_navbar_helper.rb
|
|
348
349
|
- app/helpers/studio_sidebar_helper.rb
|
|
349
350
|
- app/helpers/studio_theme_helper.rb
|
|
350
351
|
- app/jobs/error_log_cleanup_job.rb
|
|
@@ -572,6 +573,7 @@ files:
|
|
|
572
573
|
- lib/studio/log_rotation.rb
|
|
573
574
|
- lib/studio/mail_transport.rb
|
|
574
575
|
- lib/studio/name_parts.rb
|
|
576
|
+
- lib/studio/navbar_links.rb
|
|
575
577
|
- lib/studio/newsletter.rb
|
|
576
578
|
- lib/studio/oauth_identity.rb
|
|
577
579
|
- lib/studio/partial_block.rb
|