studio-engine 0.79.0 → 0.81.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: 1a41688fc9d2fd9a08b96ea00d90e971d3be7618577acf83887d7c64b71076d3
4
- data.tar.gz: 6930fd3598433185b2c578ebc2f287a54c972828ab73f068800521f76793a82a
3
+ metadata.gz: 3ba5fe314a5d1bbb6b2b95d12a2561bf3f87c2e302ced48eb365e2587c7ba7ee
4
+ data.tar.gz: de2c065be4ed44da716d10331c23edfda0ca08c05f333b0324457b860d4db073
5
5
  SHA512:
6
- metadata.gz: c089670e6c2ba85bac8a7851f13f4fa033801e738583974d7207d1960ebccf181530feec5ef3c4cd8f4ae5300d793b5d54eaeb91f46cd9732287f2d44d31ab13
7
- data.tar.gz: c6e24cd7de1298e73e58309da359ffcdae06d150e0b13afba52f90f8864782d6c197b22cf7b61279d88a1d98e52b562eafb7f4e76cd968f6088bb77c3fb3b011
6
+ metadata.gz: 1eac79405823182fa089630acf41324de6238af3dead7f97136a97db2a0062f1443ae2538446cecace96bbe30655c3b889dbf1bba05478492d59c514bbcb3238
7
+ data.tar.gz: 56637c4be0db6c8701eaab6397bd6631f34f609ba986574d48382b458e52e415b3854598efc24b1894b1cfd43fdb8e81298754b57026657d8816a4e351a2bdad
data/CHANGELOG.md CHANGED
@@ -4,6 +4,39 @@ The format is [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). This pro
4
4
 
5
5
  ## Unreleased
6
6
 
7
+ ## 0.81.0 — 2026-09-30
8
+
9
+ ### Changed
10
+
11
+ - **Default warning and danger buttons clear WCAG AA under their white label.**
12
+ While an app leaves its warning at the engine default `#FF7C47` (2.55:1 under
13
+ white), the resolver emits `--color-warning-fill` (`#B85933`, 4.65:1); while it
14
+ leaves danger at `#EF4444` (3.76:1), it emits `--color-danger-fill`
15
+ (`#D73D3D`, 4.54:1). `btn-warning` and `btn-danger` paint the fill and fall back
16
+ to `--color-warning` / `--color-danger`, so a configured colour paints as
17
+ before. The role colours themselves, their borders and rings, and the
18
+ `--color-warning-ink` / `--color-danger-ink` text inks are unchanged. Labels
19
+ stay white.
20
+ - **An admin sees one admin cog at every width.** `components/_user_nav` draws its
21
+ admin menu from `md` up only, as it already did the theme toggle: below `md` the
22
+ navbar's phone row draws the cog, so an admin's phone showed two.
23
+ `components/_admin_dropdown` takes an optional `display_class:` for this. Apps
24
+ that shadow `_user_nav` (mcritchie-studio, turf-monster, cyvasse) are unaffected
25
+ until they drop their copy.
26
+
27
+ ## 0.80.0 — 2026-09-30
28
+
29
+ ### Added
30
+
31
+ - **`Studio.navbar_user_name` and `Studio.sign_in_label` set the navbar's two
32
+ identity words.** `navbar_user_name` takes a method name (`:player_name`) or a
33
+ callable `->(user, view)` and replaces `display_name` in the signed-in user
34
+ nav; a blank answer or a raise falls back to `display_name`, and a raise is
35
+ reported once rather than 500ing the layout. `sign_in_label` (default
36
+ `"Log in"`) labels the signed-out button in `layouts/_navbar` and
37
+ `components/_user_nav`. An app that sets neither renders the same navbar bytes
38
+ as before. See the README, *Navbar identity*.
39
+
7
40
  ## 0.79.0 — 2026-09-30
8
41
 
9
42
  ### Changed
data/README.md CHANGED
@@ -611,6 +611,36 @@ end
611
611
  - A bad entry raises `Studio::NavbarLinks::InvalidLink` naming its index: at
612
612
  assignment for a static Array, at render for a callable.
613
613
 
614
+ ### Navbar identity — `Studio.navbar_user_name` and `Studio.sign_in_label`
615
+
616
+ Two words the navbar says about the viewer. Both defaults render the navbar
617
+ byte-identical to before, so an app that sets neither sees no change. Rules:
618
+ `lib/studio/navbar_identity.rb`.
619
+
620
+ ```ruby
621
+ Studio.configure do |config|
622
+ # The signed-in name in components/_user_nav. nil (the default) is display_name.
623
+ config.navbar_user_name = :player_name # a method on the user
624
+ # config.navbar_user_name = ->(user, view) { user.player_name } # or a callable
625
+
626
+ # The signed-out button in layouts/_navbar and components/_user_nav.
627
+ config.sign_in_label = "Sign in" # default "Log in"
628
+ end
629
+ ```
630
+
631
+ - `navbar_user_name` is `nil`, a method name (Symbol or String), or a callable.
632
+ A two-argument callable gets `(user, view)`; a one-argument callable gets the
633
+ user alone.
634
+ - The name is always escaped, even when the method or callable returns an
635
+ `html_safe` string.
636
+ - A method or callable that answers blank falls back to `display_name`. One
637
+ that raises also falls back, and the error is reported once per process per
638
+ error class (to `ErrorLog` when the app has it, else the Rails log), so a
639
+ broken setting never 500s the layout.
640
+ - The engine has no I18n catalogue, so the label is plain config, not a locale
641
+ key. A blank or non-String label, or a `navbar_user_name` of any other type,
642
+ raises `Studio::NavbarIdentity::InvalidConfig` at assignment.
643
+
614
644
  ### User nav slots
615
645
 
616
646
  `components/_user_nav.html.erb` renders the right-side navbar user section.
@@ -265,17 +265,23 @@ body.modal-open .studio-app-banner {
265
265
  }
266
266
  }
267
267
 
268
+ /* The fill is --color-danger-fill when the resolver emits one (only while the
269
+ app leaves its danger colour at the engine default, which fails AA under the
270
+ white label), otherwise the app's own --color-danger. */
268
271
  @utility btn-danger {
269
272
  @apply text-white;
270
- background-color: var(--color-danger);
273
+ background-color: var(--color-danger-fill, var(--color-danger));
271
274
  &:hover {
272
275
  filter: brightness(0.9);
273
276
  }
274
277
  }
275
278
 
279
+ /* The fill is --color-warning-fill when the resolver emits one (only while the
280
+ app leaves its warning colour at the engine default, which fails AA under the
281
+ white label), otherwise the app's own --color-warning. */
276
282
  @utility btn-warning {
277
283
  @apply text-white;
278
- background-color: var(--color-warning);
284
+ background-color: var(--color-warning-fill, var(--color-warning));
279
285
  &:hover {
280
286
  filter: brightness(0.9);
281
287
  }
@@ -1,8 +1,12 @@
1
1
  <%# ADMIN chrome: renders only for admin? viewers. Signed-out and
2
2
  non-admin visitors get nothing — this partial once leaked admin
3
3
  links onto a public landing page. %>
4
+ <%# display_class: optional display utilities for the root. The user nav
5
+ passes "hidden md:block" because below md the navbar's phone row draws
6
+ its own cog, and two read as a double gear (engine-button-contrast-admin-cog). %>
7
+ <% display_class = local_assigns.fetch(:display_class, nil) %>
4
8
  <% if respond_to?(:admin?) && admin? %>
5
- <div x-data="{ open: false }" class="relative leading-none">
9
+ <div x-data="{ open: false }" class="<%= ["relative leading-none", display_class].compact.join(" ") %>">
6
10
  <button @click="open = !open" class="flex text-secondary hover:text-heading transition" title="Admin">
7
11
  <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4">
8
12
  <path stroke-linecap="round" stroke-linejoin="round" d="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.325.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 0 1 1.37.49l1.296 2.247a1.125 1.125 0 0 1-.26 1.431l-1.003.827c-.293.241-.438.613-.43.992a7.723 7.723 0 0 1 0 .255c-.008.378.137.75.43.991l1.004.827c.424.35.534.955.26 1.43l-1.298 2.247a1.125 1.125 0 0 1-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.47 6.47 0 0 1-.22.128c-.331.183-.581.495-.644.869l-.213 1.281c-.09.543-.56.941-1.11.941h-2.594c-.55 0-1.019-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 0 1-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 0 1-1.369-.49l-1.297-2.247a1.125 1.125 0 0 1 .26-1.431l1.004-.827c.292-.24.437-.613.43-.991a6.932 6.932 0 0 1 0-.255c.007-.38-.138-.751-.43-.992l-1.004-.827a1.125 1.125 0 0 1-.26-1.43l1.297-2.247a1.125 1.125 0 0 1 1.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.086.22-.128.332-.183.582-.495.644-.869l.214-1.28Z" />
@@ -11,6 +11,10 @@
11
11
  address + level progress bar) entirely
12
12
  (e.g. a seeds bar)
13
13
 
14
+ Config, not locals: the signed-in name is Studio.navbar_user_name
15
+ (default display_name) and the signed-out button reads
16
+ Studio.sign_in_label (default "Log in"); lib/studio/navbar_identity.rb.
17
+
14
18
  Other locals:
15
19
  show_logout_link — boolean, show explicit "Log out" link in the
16
20
  second row (Studio uses this)
@@ -82,8 +86,10 @@
82
86
  <div class="flex items-center gap-2 ml-auto self-end">
83
87
  <%= studio_nav_slot.call(extra_icons_slot, extra_icons_html) %>
84
88
  <%# Skipped when the link sidebar carries the admin menu — two cog
85
- glyphs side by side read as a double gear. %>
86
- <%= render "components/admin_dropdown" unless respond_to?(:studio_sidebar_replaces_admin_menu?) && studio_sidebar_replaces_admin_menu? %>
89
+ glyphs side by side read as a double gear. Desktop only, like
90
+ the theme toggle: below md the navbar's phone row draws the cog,
91
+ so a second one here put two on an admin's phone. %>
92
+ <%= render "components/admin_dropdown", display_class: "hidden md:block" unless respond_to?(:studio_sidebar_replaces_admin_menu?) && studio_sidebar_replaces_admin_menu? %>
87
93
  <%# Desktop only. Below md the engine navbar's phone row draws the
88
94
  theme toggle, so a second one here put two on every phone. The
89
95
  signed-out bar has always done it this way (layouts/_navbar). %>
@@ -204,7 +210,7 @@
204
210
  the link's accessible name. %>
205
211
  <% account_classes = "group flex items-center gap-2 flex-shrink-0" %>
206
212
  <% account_inner = capture do %>
207
- <span class="sr-only md:not-sr-only md:max-w-40"><span class="block truncate text-right text-heading font-semibold text-base leading-none transition<%= " group-hover:text-primary" if studio_nav_destination %>" data-nav-name><%= current_user.display_name %></span></span>
213
+ <span class="sr-only md:not-sr-only md:max-w-40"><span class="block truncate text-right text-heading font-semibold text-base leading-none transition<%= " group-hover:text-primary" if studio_nav_destination %>" data-nav-name><%= Studio.navbar_user_name_for(current_user, self) %></span></span>
208
214
  <span class="flex-shrink-0 flex items-center" aria-hidden="true">
209
215
  <%= render "components/avatar", user: current_user, size: "nav" %>
210
216
  </span>
@@ -215,11 +221,11 @@
215
221
  <span class="<%= account_classes %>" data-nav-account x-bind:style="$store.devMode && 'background: lightgreen'"><%= account_inner %></span>
216
222
  <% end %>
217
223
  <% else %>
218
- <%= render "components/admin_dropdown" unless respond_to?(:studio_sidebar_replaces_admin_menu?) && studio_sidebar_replaces_admin_menu? %>
224
+ <%= render "components/admin_dropdown", display_class: "hidden md:block" unless respond_to?(:studio_sidebar_replaces_admin_menu?) && studio_sidebar_replaces_admin_menu? %>
219
225
  <%# Desktop only, as above: the phone row carries the toggle below md. %>
220
226
  <span class="hidden md:flex" data-nav-theme-toggle="desktop"><%= render "components/theme_toggle_morph" %></span>
221
227
  <div class="flex items-center gap-3 ml-auto">
222
- <%= link_to "Log in", login_path, class: "text-heading hover:text-primary text-sm font-semibold transition" %>
228
+ <%= link_to Studio.sign_in_label, login_path, class: "text-heading hover:text-primary text-sm font-semibold transition" %>
223
229
  <%= link_to "Sign up", signup_path, class: "btn btn-primary btn-sm" %>
224
230
  </div>
225
231
  <% end %>
@@ -162,7 +162,7 @@
162
162
  <div class="flex items-center justify-end gap-3">
163
163
  <%= sidebar_trigger_desktop if sidebar_active %>
164
164
  <span class="hidden md:flex"><%= render "components/theme_toggle_morph" %></span>
165
- <%= link_to "Log in", login_path, class: "btn btn-primary" %>
165
+ <%= link_to Studio.sign_in_label, login_path, class: "btn btn-primary" %>
166
166
  </div>
167
167
  <% end %>
168
168
  </div>
@@ -191,8 +191,8 @@
191
191
  d.setProperty('--color-cta', this.colors.primary);
192
192
  d.setProperty('--color-cta-fill', this.colors.primary);
193
193
  if (this.colors.success) d.setProperty('--color-success', this.colors.success);
194
- if (this.colors.warning) d.setProperty('--color-warning', this.colors.warning);
195
- if (this.colors.danger) d.setProperty('--color-danger', this.colors.danger);
194
+ if (this.colors.warning) { d.setProperty('--color-warning', this.colors.warning); d.setProperty('--color-warning-fill', this.colors.warning); }
195
+ if (this.colors.danger) { d.setProperty('--color-danger', this.colors.danger); d.setProperty('--color-danger-fill', this.colors.danger); }
196
196
  },
197
197
  persist(form, label) {
198
198
  if (this.saving) return;
@@ -337,6 +337,13 @@
337
337
  document.documentElement.style.setProperty('--color-success', this.colors.success);
338
338
  document.documentElement.style.setProperty('--color-warning', this.colors.warning || '#FF7C47');
339
339
  document.documentElement.style.setProperty('--color-danger', this.colors.danger || '#EF4444');
340
+ // btn-warning/btn-danger paint --color-<role>-fill first (the resolver
341
+ // emits it for the engine defaults), so a typed colour must set it too
342
+ // or the specimens keep the default fill. Cleared: drop the override.
343
+ ['warning', 'danger'].forEach((role) => {
344
+ if (this.colors[role]) document.documentElement.style.setProperty(`--color-${role}-fill`, this.colors[role]);
345
+ else document.documentElement.style.removeProperty(`--color-${role}-fill`);
346
+ });
340
347
  },
341
348
 
342
349
  darkPreviewStyle() {
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Resolves the two words the engine navbar says about who is (or is not) signed
4
+ # in: the signed-in user's name (Studio.navbar_user_name) and the signed-out
5
+ # button's label (Studio.sign_in_label). Pure Ruby, so the unit suite covers it
6
+ # without the dummy app.
7
+ #
8
+ # Studio.navbar_user_name is one of:
9
+ #
10
+ # nil the default: user.display_name, as before
11
+ # :player_name a method called on the user ("player_name" works too)
12
+ # ->(user, view) { ... } a callable; a one-argument callable gets the user only
13
+ #
14
+ # A layout must not 500 over a name. When the configured method or callable
15
+ # RAISES, the navbar prints display_name and the failure is reported once per
16
+ # process per error class (ErrorLog when the host has it, else Rails.logger),
17
+ # not once per page view. A BLANK answer falls back to display_name quietly:
18
+ # a user with no player name yet is data, not a fault.
19
+ #
20
+ # The name comes back as a plain String, never an html_safe buffer, so ERB
21
+ # escapes whatever a host's method or callable returned.
22
+ module Studio
23
+ module NavbarIdentity
24
+ class InvalidConfig < ArgumentError; end
25
+
26
+ DEFAULT_SIGN_IN_LABEL = "Log in"
27
+
28
+ @reported = {}
29
+ @reported_lock = Mutex.new
30
+
31
+ module_function
32
+
33
+ def user_name(user, view = nil, config: Studio.navbar_user_name)
34
+ fallback = user.display_name # outside the rescue: the default path is not ours to swallow
35
+ return fallback if config.nil?
36
+
37
+ begin
38
+ text = plain_text(configured_name(config, user, view))
39
+ rescue StandardError => e
40
+ report_once(e)
41
+ return fallback
42
+ end
43
+ text.empty? ? fallback : text
44
+ end
45
+
46
+ def validate_name!(config)
47
+ return nil if config.nil? || config.respond_to?(:call)
48
+ return nil if (config.is_a?(Symbol) || config.is_a?(String)) && !config.to_s.strip.empty?
49
+
50
+ raise InvalidConfig, "Studio.navbar_user_name must be nil, a method name (Symbol) or a callable " \
51
+ "->(user, view) (got #{config.inspect})"
52
+ end
53
+
54
+ def validate_label!(label)
55
+ return nil if label.is_a?(String) && !label.strip.empty?
56
+
57
+ raise InvalidConfig, "Studio.sign_in_label must be non-blank text (got #{label.inspect})"
58
+ end
59
+
60
+ # Forget what was reported, so a reconfigured name reports its own failure.
61
+ def reset_reported!
62
+ @reported_lock.synchronize { @reported.clear }
63
+ end
64
+
65
+ def configured_name(config, user, view)
66
+ return user.public_send(config.to_sym) unless config.respond_to?(:call)
67
+
68
+ config.respond_to?(:arity) && config.arity == 1 ? config.call(user) : config.call(user, view)
69
+ end
70
+
71
+ # String.new drops an html_safe flag: SafeBuffer#to_s returns the buffer
72
+ # itself, which ERB would print raw.
73
+ def plain_text(value)
74
+ String.new(value.to_s).strip
75
+ end
76
+
77
+ def report_once(error)
78
+ first = @reported_lock.synchronize { @reported[error.class.name] ? false : (@reported[error.class.name] = true) }
79
+ return unless first
80
+
81
+ if defined?(::ErrorLog) && ::ErrorLog.respond_to?(:capture!)
82
+ ::ErrorLog.capture!(error)
83
+ elsif defined?(::Rails) && ::Rails.respond_to?(:logger) && ::Rails.logger
84
+ ::Rails.logger.warn("[studio-navbar] Studio.navbar_user_name failed, showing display_name: " \
85
+ "#{error.class}: #{error.message}")
86
+ end
87
+ rescue StandardError
88
+ nil # reporting is best-effort: the navbar still renders
89
+ end
90
+ end
91
+ end
@@ -9,6 +9,12 @@ module Studio
9
9
  # green measured 2.78:1 under the white label btn-success draws on it. This
10
10
  # is the same hue at 5.0:1 (the shade cyvasse chose for its own success).
11
11
  DEFAULT_SUCCESS = "#367E3A"
12
+ # The default warning and danger stay these hexes as ROLE colours (borders,
13
+ # rings, the -ink derivations start from them). Their button fills are
14
+ # derived separately, see #fill_vars: #FF7C47 is 2.55:1 and #EF4444 3.76:1
15
+ # under the white label btn-warning and btn-danger draw.
16
+ DEFAULT_WARNING = "#FF7C47"
17
+ DEFAULT_DANGER = "#EF4444"
12
18
 
13
19
  # WCAG AA for normal-size text. Every filled button draws a 14px bold white
14
20
  # label, which is not "large" text, so its fill owes 4.5:1 against white.
@@ -44,8 +50,8 @@ module Studio
44
50
  primary = colors[:primary] || DEFAULT_PRIMARY
45
51
  border_rgb = ColorScale.lighten(dark_base, 0.30)
46
52
  success = colors[:success] || DEFAULT_SUCCESS
47
- warning = colors[:warning] || "#FF7C47"
48
- danger = colors[:danger] || "#EF4444"
53
+ warning = colors[:warning] || DEFAULT_WARNING
54
+ danger = colors[:danger] || DEFAULT_DANGER
49
55
  surfaces = dark_surfaces(dark_base)
50
56
 
51
57
  secondary_ink = contrast_ink(dark_base, direction: :lighten, start: 0.70, target: 4.5, against: surfaces)
@@ -114,7 +120,7 @@ module Studio
114
120
  "--color-warning-ink" => status_ink(warning, surfaces, direction: :lighten),
115
121
  "--color-success-ink" => status_ink(success, surfaces, direction: :lighten),
116
122
  "--color-accent" => colors[:accent] || "#F72585"
117
- }.merge(fill_vars(primary, success))
123
+ }.merge(fill_vars(primary, warning, danger))
118
124
  end
119
125
 
120
126
  # Every dark-mode background a text var can sit on (page, surface,
@@ -197,13 +203,23 @@ module Studio
197
203
  # label with its orange (a darkened fill would drop it to 3.3:1), and
198
204
  # cyvasse sets --color-cta itself.
199
205
  #
206
+ # --color-warning-fill and --color-danger-fill are the same contract for
207
+ # btn-warning and btn-danger (engine-button-contrast-admin-cog): emitted
208
+ # only while the role is at its engine default (#FF7C47 at 2.55:1, #EF4444
209
+ # at 3.76:1 under white), so a CONFIGURED colour paints as-is through the
210
+ # var(--color-<role>) fallback — mcritchie-industries' yellow warning is
211
+ # its own call. The role colour itself is not darkened: it is also a
212
+ # border, a ring and the start of its -ink search, which text reads.
213
+ #
200
214
  # Success needs no fill var: its DEFAULT is already AA under white
201
215
  # (DEFAULT_SUCCESS), and a configured success colour is the app's own —
202
216
  # turf-monster's suite pins that a configured #4BAF50 still paints as-is.
203
- def fill_vars(primary, _success)
204
- return {} unless primary.to_s.casecmp?(DEFAULT_PRIMARY)
205
-
206
- { "--color-cta-fill" => white_label_fill(primary) }
217
+ def fill_vars(primary, warning, danger)
218
+ {
219
+ "--color-cta-fill" => (white_label_fill(primary) if primary.to_s.casecmp?(DEFAULT_PRIMARY)),
220
+ "--color-warning-fill" => (white_label_fill(warning) if warning.to_s.casecmp?(DEFAULT_WARNING)),
221
+ "--color-danger-fill" => (white_label_fill(danger) if danger.to_s.casecmp?(DEFAULT_DANGER))
222
+ }.compact
207
223
  end
208
224
 
209
225
  # The hover fill is always derived, so it is always held to AA under white:
@@ -251,8 +267,8 @@ module Studio
251
267
  light_base = colors[:light] || "#f8fafc"
252
268
  primary = colors[:primary] || DEFAULT_PRIMARY
253
269
  success = colors[:success] || DEFAULT_SUCCESS
254
- warning = colors[:warning] || "#FF7C47"
255
- danger = colors[:danger] || "#EF4444"
270
+ warning = colors[:warning] || DEFAULT_WARNING
271
+ danger = colors[:danger] || DEFAULT_DANGER
256
272
  surfaces = light_surfaces(light_base)
257
273
 
258
274
  secondary_ink = contrast_ink(light_base, direction: :darken, start: 0.55, target: 4.5, against: surfaces)
@@ -303,7 +319,7 @@ module Studio
303
319
  "--color-warning-ink" => status_ink(warning, surfaces, direction: :darken),
304
320
  "--color-success-ink" => status_ink(success, surfaces, direction: :darken),
305
321
  "--color-accent" => colors[:accent] || "#F72585"
306
- }.merge(fill_vars(primary, success))
322
+ }.merge(fill_vars(primary, warning, danger))
307
323
  end
308
324
  end
309
325
  end
@@ -1,3 +1,3 @@
1
1
  module Studio
2
- VERSION = "0.79.0"
2
+ VERSION = "0.81.0"
3
3
  end
data/lib/studio.rb CHANGED
@@ -13,6 +13,7 @@ require "studio/js_identifier"
13
13
  require "studio/partial_block"
14
14
  require "studio/sidebar_sections"
15
15
  require "studio/navbar_links"
16
+ require "studio/navbar_identity"
16
17
  require "studio/profile_sections"
17
18
  require "studio/profile_image"
18
19
  require "studio/oauth_identity"
@@ -192,6 +193,33 @@ module Studio
192
193
  @@navbar_links = links
193
194
  end
194
195
 
196
+ # ---- Navbar identity ----
197
+ # The name the signed-in user nav prints, and the signed-out button's label.
198
+ # Both defaults render the navbar byte-identical to before. The engine has no
199
+ # I18n catalogue, so the label is plain config rather than a locale key.
200
+ # Rules: lib/studio/navbar_identity.rb.
201
+ #
202
+ # config.navbar_user_name = :player_name # a method on the user
203
+ # config.navbar_user_name = ->(user, view) { user.player_name } # or a callable
204
+ # config.sign_in_label = "Sign in"
205
+ #
206
+ # nil means user.display_name. A method or callable that raises, or answers
207
+ # blank, falls back to display_name; a raise is reported once, never a 500.
208
+ mattr_reader :navbar_user_name, default: nil
209
+
210
+ def self.navbar_user_name=(config)
211
+ NavbarIdentity.validate_name!(config)
212
+ NavbarIdentity.reset_reported!
213
+ @@navbar_user_name = config
214
+ end
215
+
216
+ mattr_reader :sign_in_label, default: NavbarIdentity::DEFAULT_SIGN_IN_LABEL
217
+
218
+ def self.sign_in_label=(label)
219
+ NavbarIdentity.validate_label!(label)
220
+ @@sign_in_label = label
221
+ end
222
+
195
223
  # ---- The shared profile page (/profile) ----
196
224
  # The rows that make up /profile. `nil` — the default — means "the engine's
197
225
  # standard page", NOT "no rows": a brand-new app with an empty initializer gets
@@ -902,6 +930,11 @@ module Studio
902
930
  NavbarLinks.resolve(navbar_links, view)
903
931
  end
904
932
 
933
+ # The signed-in user's navbar name for a view (lib/studio/navbar_identity.rb).
934
+ def self.navbar_user_name_for(user, view = nil)
935
+ NavbarIdentity.user_name(user, view, config: navbar_user_name)
936
+ end
937
+
905
938
  # The engine's standard /profile rows. Hosts compose against this rather than
906
939
  # restating a literal list, so a later release that adds a standard row
907
940
  # delivers it to every app that used the seam as intended.
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: studio-engine
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.79.0
4
+ version: 0.81.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alex McRitchie
@@ -573,6 +573,7 @@ files:
573
573
  - lib/studio/log_rotation.rb
574
574
  - lib/studio/mail_transport.rb
575
575
  - lib/studio/name_parts.rb
576
+ - lib/studio/navbar_identity.rb
576
577
  - lib/studio/navbar_links.rb
577
578
  - lib/studio/newsletter.rb
578
579
  - lib/studio/oauth_identity.rb