studio-engine 0.112.0 → 0.113.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 +21 -0
- data/README.md +56 -0
- data/app/assets/tailwind/studio_engine/engine.css +34 -1
- data/app/helpers/studio_navbar_helper.rb +21 -2
- data/app/views/components/_user_nav.html.erb +15 -0
- data/app/views/layouts/_navbar.html.erb +11 -2
- data/lib/studio/navbar_slots.rb +127 -0
- data/lib/studio/version.rb +1 -1
- data/lib/studio.rb +46 -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: 4fdd2e298951ce853f0cccdf203c26d3e060c7de30c3541d4fb07bb00310e167
|
|
4
|
+
data.tar.gz: 1fecbb7d380d3d80c000d08b54a70b8407962ef0495cb7d41b5579f8a390673d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6c7e8e894beb7fd5bdcfd73a83e459a244fa7f2e858923382f9ba378e64b764e9c9f1adb7e22a633e22a7fb937d98990ce8056a1f69d6265a911684e71229b31
|
|
7
|
+
data.tar.gz: 1bc3712f6953dbb9ca9d3c2ce6bb4713a1c9ed99897864d5f6381c9b5e87edf15e8c4fb591864af48ccb02af45b80c39e0843a0da00528c8b13ff268033ce7a0
|
data/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,27 @@ The format is [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). This pro
|
|
|
4
4
|
|
|
5
5
|
## Unreleased
|
|
6
6
|
|
|
7
|
+
## 0.113.0 — 2026-10-24
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- **`Studio.navbar_items` and `Studio.navbar_avatar_badge`: partial slots in
|
|
12
|
+
the engine navbar.** `navbar_items` is a list of partials (a path, or
|
|
13
|
+
`{ partial:, locals: }`) the navbar renders after `navbar_links` in both the
|
|
14
|
+
desktop `<nav>` and the phone row, for what an app shows in the bar that is
|
|
15
|
+
not a link, such as Turf Monster's geo badge and wallet signal.
|
|
16
|
+
`navbar_avatar_badge` is one partial the signed-in user nav draws over the
|
|
17
|
+
avatar's upper-left corner, such as Turf Monster's free-entry sparkle. Its
|
|
18
|
+
seat is the account control's next sibling, so the control stays one tab stop
|
|
19
|
+
and the badge may carry its own button, and `engine.css` (*THE AVATAR BADGE*)
|
|
20
|
+
places it out of flow over the avatar, so the row gives it no width; it
|
|
21
|
+
tracks the one-row 40px avatar and scales with it on the scroll timeline.
|
|
22
|
+
Either setting may be a callable receiving the view. Unset (the defaults)
|
|
23
|
+
the navbar renders byte-identical to before, signed in and out. A bad static
|
|
24
|
+
setting raises `Studio::NavbarSlots::InvalidConfig` at assignment; a raising
|
|
25
|
+
callable renders nothing and is reported once. README, *Navbar items* and
|
|
26
|
+
*Avatar badge*.
|
|
27
|
+
|
|
7
28
|
## 0.112.0 — 2026-10-19
|
|
8
29
|
|
|
9
30
|
### Added
|
data/README.md
CHANGED
|
@@ -766,6 +766,62 @@ end
|
|
|
766
766
|
- A host on an older engine can set it safely with
|
|
767
767
|
`Studio.navbar_signed_out_cta = … if Studio.respond_to?(:navbar_signed_out_cta=)`.
|
|
768
768
|
|
|
769
|
+
### Navbar items — `Studio.navbar_items`
|
|
770
|
+
|
|
771
|
+
Partials the navbar renders after its links, for the things an app shows in
|
|
772
|
+
the bar that are not links: a location badge, a wallet signal. Empty (`[]`, the
|
|
773
|
+
default), the navbar renders the same bytes as before. Rules:
|
|
774
|
+
`lib/studio/navbar_slots.rb`.
|
|
775
|
+
|
|
776
|
+
```ruby
|
|
777
|
+
Studio.configure do |config|
|
|
778
|
+
# A partial path, or { partial:, locals: }.
|
|
779
|
+
config.navbar_items = ["components/geo_badge",
|
|
780
|
+
{ partial: "shared/wallet_signal", locals: { tone: "muted" } }]
|
|
781
|
+
# or a callable receiving the view; nil renders none for that render
|
|
782
|
+
# config.navbar_items = ->(view) { view.logged_in? ? ["shared/wallet_signal"] : [] }
|
|
783
|
+
end
|
|
784
|
+
```
|
|
785
|
+
|
|
786
|
+
- Each item renders in BOTH rows: inside the desktop `<nav>` after the links
|
|
787
|
+
(from `md`), and in the phone row after its links (below `md`). So every item
|
|
788
|
+
is on the page twice; give its markup no `id`, and let it size itself for
|
|
789
|
+
either row.
|
|
790
|
+
- Items render signed in and signed out, and in `preview: true` renders, as the
|
|
791
|
+
links do.
|
|
792
|
+
- A bad static list raises `Studio::NavbarSlots::InvalidConfig` at assignment,
|
|
793
|
+
naming its index; a callable's bad answer raises it at render. A callable
|
|
794
|
+
that raises renders no items and is reported once per process per error
|
|
795
|
+
class, never a 500.
|
|
796
|
+
- A host on an older engine can set it safely with
|
|
797
|
+
`Studio.navbar_items = … if Studio.respond_to?(:navbar_items=)`.
|
|
798
|
+
|
|
799
|
+
### Avatar badge — `Studio.navbar_avatar_badge`
|
|
800
|
+
|
|
801
|
+
One partial the signed-in user nav draws over the avatar's upper-left corner,
|
|
802
|
+
the way a notification dot rides a profile picture. `nil`, the default, renders
|
|
803
|
+
the user nav byte-identical to before. Rules: `lib/studio/navbar_slots.rb`.
|
|
804
|
+
|
|
805
|
+
```ruby
|
|
806
|
+
Studio.configure do |config|
|
|
807
|
+
config.navbar_avatar_badge = "components/entry_token_badge"
|
|
808
|
+
# or { partial:, locals: }, or a callable receiving the view (nil for no badge)
|
|
809
|
+
end
|
|
810
|
+
```
|
|
811
|
+
|
|
812
|
+
- The badge renders in a `[data-nav-avatar-badge]` seat right after the
|
|
813
|
+
account control, never inside it: the name and avatar stay one control and
|
|
814
|
+
one tab stop, and the badge may carry its own button.
|
|
815
|
+
- `engine.css` (*THE AVATAR BADGE*) seats it out of flow, so the row gives it
|
|
816
|
+
no width: an avatar-sized box over the avatar, z-index 20, the badge's root
|
|
817
|
+
8px left and 4px up from the avatar's corner. The seat tracks the one-row
|
|
818
|
+
40px avatar and scales with it on the scroll timeline. Only the badge takes
|
|
819
|
+
the pointer; the rest of the avatar still opens the account.
|
|
820
|
+
- Signed out it never renders. Validation and the raising-callable fallback
|
|
821
|
+
are `navbar_items`'s.
|
|
822
|
+
- A host on an older engine can set it safely with
|
|
823
|
+
`Studio.navbar_avatar_badge = … if Studio.respond_to?(:navbar_avatar_badge=)`.
|
|
824
|
+
|
|
769
825
|
### User nav slots
|
|
770
826
|
|
|
771
827
|
`components/_user_nav.html.erb` renders the right-side navbar user section.
|
|
@@ -698,6 +698,32 @@ body.modal-open .studio-app-banner {
|
|
|
698
698
|
}
|
|
699
699
|
}
|
|
700
700
|
|
|
701
|
+
/* THE AVATAR BADGE (Studio.navbar_avatar_badge, components/_user_nav).
|
|
702
|
+
The seat is the account control's next sibling, and the account control
|
|
703
|
+
ends the user nav's flex row with the avatar at its right edge. So the
|
|
704
|
+
seat is an avatar-sized box pinned to the row's right edge and centred
|
|
705
|
+
on it, exactly over the avatar, and the badge rides its upper-left
|
|
706
|
+
corner, 8px out and 4px up. Absolute, so the row gives it no width and
|
|
707
|
+
no gap. z-index 20 lifts it over the avatar. The seat lets the pointer
|
|
708
|
+
through to the avatar under it; only the badge itself takes it. */
|
|
709
|
+
:has(> [data-nav-avatar-badge]) { position: relative; }
|
|
710
|
+
[data-nav-avatar-badge] {
|
|
711
|
+
position: absolute;
|
|
712
|
+
top: 0;
|
|
713
|
+
bottom: 0;
|
|
714
|
+
right: 0;
|
|
715
|
+
margin-block: auto;
|
|
716
|
+
width: 2rem;
|
|
717
|
+
height: 2rem;
|
|
718
|
+
z-index: 20;
|
|
719
|
+
pointer-events: none;
|
|
720
|
+
}
|
|
721
|
+
[data-nav-avatar-badge] > * {
|
|
722
|
+
position: absolute;
|
|
723
|
+
left: -0.5rem;
|
|
724
|
+
top: -0.25rem;
|
|
725
|
+
pointer-events: auto;
|
|
726
|
+
}
|
|
701
727
|
/* THE ACCOUNT ROW (components/_user_nav), from 768px. Every app whose
|
|
702
728
|
navbar renders the engine's user nav gets this; Turf Monster forks the
|
|
703
729
|
partial, carries none of these hooks, and keeps its own two-row nav.
|
|
@@ -748,6 +774,11 @@ body.modal-open .studio-app-banner {
|
|
|
748
774
|
height: var(--nav-avatar);
|
|
749
775
|
font-size: 0.875rem;
|
|
750
776
|
}
|
|
777
|
+
/* The badge seat tracks the one-row avatar it sits over. */
|
|
778
|
+
.nav-shell [data-nav-rows="1"] + [data-nav-account] + [data-nav-avatar-badge] {
|
|
779
|
+
width: var(--nav-avatar);
|
|
780
|
+
height: var(--nav-avatar);
|
|
781
|
+
}
|
|
751
782
|
.nav-shell [data-nav-rows="1"] + [data-nav-account] [data-nav-name] {
|
|
752
783
|
font-size: calc(var(--nav-avatar) / 3 / 0.7);
|
|
753
784
|
}
|
|
@@ -829,6 +860,7 @@ body.modal-open .studio-app-banner {
|
|
|
829
860
|
}
|
|
830
861
|
.nav-shell[data-nav-timeline] .nav-logo-link,
|
|
831
862
|
.nav-shell[data-nav-timeline] [data-nav-account] > [aria-hidden],
|
|
863
|
+
.nav-shell[data-nav-timeline] [data-nav-avatar-badge],
|
|
832
864
|
.nav-shell[data-nav-timeline] [data-nav-account] > span:first-child,
|
|
833
865
|
.nav-shell[data-nav-timeline] [data-nav-tools],
|
|
834
866
|
.nav-shell[data-nav-timeline] [data-nav-links] {
|
|
@@ -838,7 +870,8 @@ body.modal-open .studio-app-banner {
|
|
|
838
870
|
animation-range: var(--nav-start, 0px) calc(var(--nav-start, 0px) + var(--nav-ramp));
|
|
839
871
|
}
|
|
840
872
|
.nav-shell[data-nav-timeline] .nav-logo-link { transform-origin: left center; animation-name: nav-tl-brand; }
|
|
841
|
-
.nav-shell[data-nav-timeline] [data-nav-account] > [aria-hidden]
|
|
873
|
+
.nav-shell[data-nav-timeline] [data-nav-account] > [aria-hidden],
|
|
874
|
+
.nav-shell[data-nav-timeline] [data-nav-avatar-badge] { transform-origin: right center; animation-name: nav-tl-avatar; }
|
|
842
875
|
.nav-shell[data-nav-timeline] [data-nav-account] > span:first-child { transform-origin: right center; animation-name: nav-tl-name; }
|
|
843
876
|
.nav-shell[data-nav-timeline] [data-nav-tools] { animation-name: nav-tl-tools; }
|
|
844
877
|
.nav-shell[data-nav-timeline] [data-nav-links] { animation-name: nav-tl-links; }
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
# The navbar
|
|
4
|
-
# host lambda may read
|
|
3
|
+
# The navbar's view seams: links, items, the avatar badge and the signed-out
|
|
4
|
+
# CTA. Each resolves once per render pass, since a host lambda may read
|
|
5
|
+
# current_user or the database.
|
|
5
6
|
module StudioNavbarHelper
|
|
6
7
|
NAVBAR_LINK_CLASS = "inline-flex items-center gap-1.5 whitespace-nowrap font-medium transition"
|
|
7
8
|
NAVBAR_BADGE_CLASS = "rounded-full border border-subtle px-1.5 py-0.5 text-[10px] leading-none " \
|
|
@@ -20,6 +21,24 @@ module StudioNavbarHelper
|
|
|
20
21
|
@studio_navbar_signed_out_cta = Studio.navbar_signed_out_cta_for(self)
|
|
21
22
|
end
|
|
22
23
|
|
|
24
|
+
# Studio.navbar_items as partial specs ({ partial:, locals: }), [] for none.
|
|
25
|
+
# Resolved once per render pass: the desktop bar and the phone row both ask.
|
|
26
|
+
def studio_navbar_items
|
|
27
|
+
@studio_navbar_items ||= Studio.navbar_items_for(self)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Studio.navbar_avatar_badge as one partial spec, or nil for no badge.
|
|
31
|
+
def studio_navbar_avatar_badge
|
|
32
|
+
return @studio_navbar_avatar_badge if defined?(@studio_navbar_avatar_badge)
|
|
33
|
+
|
|
34
|
+
@studio_navbar_avatar_badge = Studio.navbar_avatar_badge_for(self)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# One resolved partial spec, rendered through the normal partial pipeline.
|
|
38
|
+
def studio_navbar_slot_tag(spec)
|
|
39
|
+
render partial: spec[:partial], locals: spec[:locals]
|
|
40
|
+
end
|
|
41
|
+
|
|
23
42
|
# One resolved link. size is the text utility: text-sm on desktop, text-xs
|
|
24
43
|
# in the phone row, matching the links apps hand-wrote in forked navbars.
|
|
25
44
|
def studio_navbar_link_tag(link, size:)
|
|
@@ -14,6 +14,8 @@
|
|
|
14
14
|
Config, not locals: the signed-in name is Studio.navbar_user_name
|
|
15
15
|
(default display_name) and the signed-out button reads
|
|
16
16
|
Studio.sign_in_label (default "Log in"); lib/studio/navbar_identity.rb.
|
|
17
|
+
Studio.navbar_avatar_badge (default nil) draws one partial over the
|
|
18
|
+
avatar; lib/studio/navbar_slots.rb.
|
|
17
19
|
|
|
18
20
|
Legacy string locals (deprecated): balance_html, extra_icons_html,
|
|
19
21
|
div2_html — pre-rendered HTML injected via raw. Still honored when
|
|
@@ -235,6 +237,19 @@
|
|
|
235
237
|
<% else %>
|
|
236
238
|
<span class="<%= account_classes %>" data-nav-account x-bind:style="$store.devMode && 'background: lightgreen'"><%= account_inner %></span>
|
|
237
239
|
<% end %>
|
|
240
|
+
<%# Studio.navbar_avatar_badge: one partial over the avatar's upper-left
|
|
241
|
+
corner, the way a notification dot rides a profile picture. Its seat
|
|
242
|
+
is a SIBLING of the account control, never inside it: the control stays
|
|
243
|
+
one tab stop, and the badge may carry its own button (a button inside a
|
|
244
|
+
link or button is invalid and reaches a screen reader as nothing).
|
|
245
|
+
The seat comes right after the control, and engine.css (THE AVATAR
|
|
246
|
+
BADGE) places it out of flow over the avatar, which ends the row, so the
|
|
247
|
+
row gives it no width. nil (the default) renders the same bytes as
|
|
248
|
+
before. %>
|
|
249
|
+
<% avatar_badge = respond_to?(:studio_navbar_avatar_badge) ? studio_navbar_avatar_badge : nil %>
|
|
250
|
+
<% if avatar_badge %>
|
|
251
|
+
<div data-nav-avatar-badge><%= studio_navbar_slot_tag(avatar_badge) %></div>
|
|
252
|
+
<% end %>
|
|
238
253
|
<% else %>
|
|
239
254
|
<%= render "components/admin_dropdown", display_class: "hidden md:block" unless respond_to?(:studio_sidebar_replaces_admin_menu?) && studio_sidebar_replaces_admin_menu? %>
|
|
240
255
|
<%# Desktop only, as above: the phone row carries the toggle below md. %>
|
|
@@ -31,6 +31,9 @@
|
|
|
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_items: partials after the links, in both rows. Empty by
|
|
35
|
+
default, which renders the same bytes as before. %>
|
|
36
|
+
<% navbar_items = respond_to?(:studio_navbar_items) ? studio_navbar_items : [] %>
|
|
34
37
|
<%# Studio.navbar_signed_out_cta: signed out, the primary button can carry a
|
|
35
38
|
call to action instead of sign-in. nil (the default) renders the same bytes
|
|
36
39
|
as before. Set, sign-in stays one tap away as a quiet text link: beside the
|
|
@@ -181,7 +184,7 @@
|
|
|
181
184
|
<<%= brand_tag %> 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></<%= brand_tag %>>
|
|
182
185
|
<% end %>
|
|
183
186
|
<% end %>
|
|
184
|
-
<%# Desktop nav — Studio.navbar_links. data-nav-links, only where the
|
|
187
|
+
<%# Desktop nav — Studio.navbar_links, then Studio.navbar_items. data-nav-links, only where the
|
|
185
188
|
header is on the scroll timeline (so an opted-out header stays
|
|
186
189
|
byte-identical): it slides left by what the scaled brand gives up
|
|
187
190
|
(engine.css, THE SCROLL-DRIVEN COLLAPSE). %>
|
|
@@ -189,6 +192,9 @@
|
|
|
189
192
|
<% navbar_links.each do |link| %>
|
|
190
193
|
<%= studio_navbar_link_tag(link, size: "text-sm") %>
|
|
191
194
|
<% end %>
|
|
195
|
+
<% navbar_items.each do |item| %>
|
|
196
|
+
<%= studio_navbar_slot_tag(item) %>
|
|
197
|
+
<% end %>
|
|
192
198
|
</nav>
|
|
193
199
|
</div>
|
|
194
200
|
</div>
|
|
@@ -212,7 +218,7 @@
|
|
|
212
218
|
<% end %>
|
|
213
219
|
</div>
|
|
214
220
|
</div>
|
|
215
|
-
<%# Mobile sub-navbar: compact row hidden on desktop — Studio.navbar_links on the left %>
|
|
221
|
+
<%# Mobile sub-navbar: compact row hidden on desktop — Studio.navbar_links, then Studio.navbar_items, on the left %>
|
|
216
222
|
<div class="flex md:hidden items-center gap-3 px-4 py-1.5 border-t border-subtle bg-surface-alt">
|
|
217
223
|
<% if navbar_links.any? %>
|
|
218
224
|
<nav aria-label="Main" class="flex items-center gap-3 overflow-x-auto">
|
|
@@ -221,6 +227,9 @@
|
|
|
221
227
|
<% end %>
|
|
222
228
|
</nav>
|
|
223
229
|
<% end %>
|
|
230
|
+
<% navbar_items.each do |item| %>
|
|
231
|
+
<%= studio_navbar_slot_tag(item) %>
|
|
232
|
+
<% end %>
|
|
224
233
|
<span class="ml-auto flex items-center gap-3">
|
|
225
234
|
<% if signed_out_cta %>
|
|
226
235
|
<%= 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: "" } %>
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Resolves the engine navbar's two partial slots. Pure Ruby, so the unit suite
|
|
4
|
+
# covers it without the dummy app.
|
|
5
|
+
#
|
|
6
|
+
# Studio.navbar_items partials rendered after Studio.navbar_links, in
|
|
7
|
+
# the desktop bar AND the phone row
|
|
8
|
+
# Studio.navbar_avatar_badge one partial rendered over the signed-in avatar's
|
|
9
|
+
# upper-left corner (components/_user_nav, Div 3)
|
|
10
|
+
#
|
|
11
|
+
# A partial spec is one of:
|
|
12
|
+
#
|
|
13
|
+
# "components/geo_badge" a partial path
|
|
14
|
+
# { partial: "shared/wallet_signal", a path with locals
|
|
15
|
+
# locals: { tone: "muted" } }
|
|
16
|
+
#
|
|
17
|
+
# navbar_items is an Array of specs, navbar_avatar_badge one spec or nil, and
|
|
18
|
+
# either may instead be a callable receiving the view that answers the same
|
|
19
|
+
# (nil meaning nothing for that render). Both defaults render nothing, so the
|
|
20
|
+
# navbar stays byte-identical until an app opts in.
|
|
21
|
+
#
|
|
22
|
+
# A bad static setting raises InvalidConfig at assignment; a callable's answer
|
|
23
|
+
# is checked when it resolves and raises the same error. A callable that RAISES
|
|
24
|
+
# is a runtime fault, not a shape fault: the slot renders nothing and the
|
|
25
|
+
# failure is reported once per process per setting per error class, so a layout
|
|
26
|
+
# never 500s over a badge. (Studio.navbar_signed_out_cta follows the same rules.)
|
|
27
|
+
module Studio
|
|
28
|
+
module NavbarSlots
|
|
29
|
+
class InvalidConfig < ArgumentError; end
|
|
30
|
+
|
|
31
|
+
SPEC_KEYS = %i[partial locals].freeze
|
|
32
|
+
ITEMS = "Studio.navbar_items"
|
|
33
|
+
BADGE = "Studio.navbar_avatar_badge"
|
|
34
|
+
|
|
35
|
+
@reported = {}
|
|
36
|
+
@reported_lock = Mutex.new
|
|
37
|
+
|
|
38
|
+
module_function
|
|
39
|
+
|
|
40
|
+
# The item specs for one render, in order; [] renders nothing.
|
|
41
|
+
def resolve_items(config, view)
|
|
42
|
+
answer = call(config, view, ITEMS) { return [] }
|
|
43
|
+
items(answer)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# The badge spec for one render, or nil for no badge.
|
|
47
|
+
def resolve_badge(config, view)
|
|
48
|
+
answer = call(config, view, BADGE) { return nil }
|
|
49
|
+
answer.nil? ? nil : spec(answer, BADGE)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Shape checks without a view: a static setting fails at boot, not on the
|
|
53
|
+
# first page render. A callable is checked when it resolves.
|
|
54
|
+
def validate_items!(config)
|
|
55
|
+
items(config) unless config.respond_to?(:call)
|
|
56
|
+
nil
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def validate_badge!(config)
|
|
60
|
+
spec(config, BADGE) unless config.nil? || config.respond_to?(:call)
|
|
61
|
+
nil
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def items(list)
|
|
65
|
+
return [] if list.nil?
|
|
66
|
+
unless list.is_a?(Array)
|
|
67
|
+
raise InvalidConfig, "#{ITEMS} must be an Array of partial specs or a callable returning one " \
|
|
68
|
+
"(got #{list.class})"
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
list.each_with_index.map { |entry, index| spec(entry, "#{ITEMS}[#{index}]") }
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# One spec, as { partial: String, locals: Hash }.
|
|
75
|
+
def spec(entry, where)
|
|
76
|
+
entry = { partial: entry } if entry.is_a?(String) || entry.is_a?(Symbol)
|
|
77
|
+
unless entry.is_a?(Hash)
|
|
78
|
+
raise InvalidConfig, "#{where} must be a partial path or a Hash { partial:, locals: } (got #{entry.class})"
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
entry = entry.to_h { |key, value| [key.to_sym, value] }
|
|
82
|
+
unknown = entry.keys - SPEC_KEYS
|
|
83
|
+
raise InvalidConfig, "#{where} has unknown key :#{unknown.first}" if unknown.any?
|
|
84
|
+
|
|
85
|
+
partial = entry[:partial]
|
|
86
|
+
unless (partial.is_a?(String) || partial.is_a?(Symbol)) && !partial.to_s.strip.empty?
|
|
87
|
+
raise InvalidConfig, "#{where} needs a non-blank :partial (got #{partial.inspect})"
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
locals = entry.fetch(:locals, nil) || {}
|
|
91
|
+
raise InvalidConfig, "#{where} :locals must be a Hash (got #{locals.class})" unless locals.is_a?(Hash)
|
|
92
|
+
|
|
93
|
+
{ partial: partial.to_s.strip, locals: locals.to_h { |key, value| [key.to_sym, value] } }
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Forget what was reported, so a reconfigured callable reports its own failure.
|
|
97
|
+
def reset_reported!
|
|
98
|
+
@reported_lock.synchronize { @reported.clear }
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# The setting itself, or a callable's answer for this view. A raise is
|
|
102
|
+
# reported and hands control to the block, which returns the fallback.
|
|
103
|
+
def call(config, view, setting)
|
|
104
|
+
return config unless config.respond_to?(:call)
|
|
105
|
+
|
|
106
|
+
config.call(view)
|
|
107
|
+
rescue StandardError => e
|
|
108
|
+
report_once(e, setting)
|
|
109
|
+
yield
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def report_once(error, setting)
|
|
113
|
+
key = "#{setting} #{error.class.name}"
|
|
114
|
+
first = @reported_lock.synchronize { @reported[key] ? false : (@reported[key] = true) }
|
|
115
|
+
return unless first
|
|
116
|
+
|
|
117
|
+
if defined?(::ErrorLog) && ::ErrorLog.respond_to?(:capture!)
|
|
118
|
+
::ErrorLog.capture!(error)
|
|
119
|
+
elsif defined?(::Rails) && ::Rails.respond_to?(:logger) && ::Rails.logger
|
|
120
|
+
::Rails.logger.warn("[studio-navbar] #{setting} failed, rendering nothing in its place: " \
|
|
121
|
+
"#{error.class}: #{error.message}")
|
|
122
|
+
end
|
|
123
|
+
rescue StandardError
|
|
124
|
+
nil # reporting is best-effort: the navbar still renders
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
data/lib/studio/version.rb
CHANGED
data/lib/studio.rb
CHANGED
|
@@ -34,6 +34,7 @@ require "studio/site_footer"
|
|
|
34
34
|
require "studio/booking"
|
|
35
35
|
require "studio/navbar_links"
|
|
36
36
|
require "studio/navbar_signed_out_cta"
|
|
37
|
+
require "studio/navbar_slots"
|
|
37
38
|
require "studio/navbar_identity"
|
|
38
39
|
require "studio/navbar_wordmark"
|
|
39
40
|
require "studio/auth_labels"
|
|
@@ -299,6 +300,41 @@ module Studio
|
|
|
299
300
|
@@navbar_signed_out_cta = config
|
|
300
301
|
end
|
|
301
302
|
|
|
303
|
+
# ---- Navbar items ----
|
|
304
|
+
# Partials the engine navbar renders after Studio.navbar_links, in the desktop
|
|
305
|
+
# bar AND the phone row (so each renders twice per page: give its markup no
|
|
306
|
+
# id). The default [] renders nothing, byte-identical to before. An Array, or
|
|
307
|
+
# a callable receiving the view, of partial specs: a partial path, or
|
|
308
|
+
# { partial:, locals: }. Rules: lib/studio/navbar_slots.rb.
|
|
309
|
+
#
|
|
310
|
+
# config.navbar_items = ["components/geo_badge", "shared/wallet_signal"]
|
|
311
|
+
# config.navbar_items = ->(view) { view.logged_in? ? ["shared/wallet_signal"] : [] }
|
|
312
|
+
mattr_reader :navbar_items, default: []
|
|
313
|
+
|
|
314
|
+
def self.navbar_items=(config)
|
|
315
|
+
NavbarSlots.validate_items!(config)
|
|
316
|
+
NavbarSlots.reset_reported!
|
|
317
|
+
@@navbar_items = config
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
# ---- Navbar avatar badge ----
|
|
321
|
+
# One partial the signed-in user nav renders over the avatar's upper-left
|
|
322
|
+
# corner, above it and out of flow, so the row gives it no width. It sits
|
|
323
|
+
# beside the account control, never inside it, so the control stays one tab
|
|
324
|
+
# stop and the badge may carry its own button. nil, the default, renders
|
|
325
|
+
# byte-identical to before. A partial spec, or a callable receiving the view
|
|
326
|
+
# (nil for no badge that render). Rules: lib/studio/navbar_slots.rb.
|
|
327
|
+
#
|
|
328
|
+
# config.navbar_avatar_badge = "components/entry_token_badge"
|
|
329
|
+
# config.navbar_avatar_badge = { partial: "components/entry_token_badge", locals: { size: :sm } }
|
|
330
|
+
mattr_reader :navbar_avatar_badge, default: nil
|
|
331
|
+
|
|
332
|
+
def self.navbar_avatar_badge=(config)
|
|
333
|
+
NavbarSlots.validate_badge!(config)
|
|
334
|
+
NavbarSlots.reset_reported!
|
|
335
|
+
@@navbar_avatar_badge = config
|
|
336
|
+
end
|
|
337
|
+
|
|
302
338
|
# ---- The shared profile page (/profile) ----
|
|
303
339
|
# The rows that make up /profile. `nil` — the default — means "the engine's
|
|
304
340
|
# standard page", NOT "no rows": a brand-new app with an empty initializer gets
|
|
@@ -1440,6 +1476,16 @@ module Studio
|
|
|
1440
1476
|
NavbarSignedOutCta.resolve(navbar_signed_out_cta, view)
|
|
1441
1477
|
end
|
|
1442
1478
|
|
|
1479
|
+
# The navbar item partial specs for a view (lib/studio/navbar_slots.rb).
|
|
1480
|
+
def self.navbar_items_for(view)
|
|
1481
|
+
NavbarSlots.resolve_items(navbar_items, view)
|
|
1482
|
+
end
|
|
1483
|
+
|
|
1484
|
+
# The avatar badge partial spec for a view, or nil (lib/studio/navbar_slots.rb).
|
|
1485
|
+
def self.navbar_avatar_badge_for(view)
|
|
1486
|
+
NavbarSlots.resolve_badge(navbar_avatar_badge, view)
|
|
1487
|
+
end
|
|
1488
|
+
|
|
1443
1489
|
# The signed-in user's navbar name for a view (lib/studio/navbar_identity.rb).
|
|
1444
1490
|
def self.navbar_user_name_for(user, view = nil)
|
|
1445
1491
|
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.113.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-25 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rails
|
|
@@ -764,6 +764,7 @@ files:
|
|
|
764
764
|
- lib/studio/navbar_identity.rb
|
|
765
765
|
- lib/studio/navbar_links.rb
|
|
766
766
|
- lib/studio/navbar_signed_out_cta.rb
|
|
767
|
+
- lib/studio/navbar_slots.rb
|
|
767
768
|
- lib/studio/navbar_wordmark.rb
|
|
768
769
|
- lib/studio/newsletter.rb
|
|
769
770
|
- lib/studio/oauth_identity.rb
|