studio-engine 0.80.0 → 0.81.1

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: 7b9678d8f3c7e2fbb15531909b721ff80ccd9e933d15c283e0c7fa1861137b11
4
- data.tar.gz: c495551acf3d80d22f1c7760d65cabb0f949021680f94f152514e409c117400a
3
+ metadata.gz: 2737532cb60e04de35ec057552292f8307aa863eec2413751cdf4080b079d712
4
+ data.tar.gz: cd3a407b3e61463686f245a4e93a30fdbdd67250a2fd6ca09a9a2abbbd507e46
5
5
  SHA512:
6
- metadata.gz: 9c28aa14a0b3a28ca0dd876fc0835431f66588159ec3d11e79f6319f9350be50e4a49d4e6891253334a07f3a0c49db0974e74819b2d2d6f40a17460f5b8e68f9
7
- data.tar.gz: 1221cfc40625adeda5fda0293de168969e95d50e43ecb01f2dde1c21c2d0b0f41807b10118f63f291ca74c094f7b1eec1ae23dc4b107be6e6543de8c5b2274ca
6
+ metadata.gz: 912655e55dd7e8a93eace9c7a237390144087b72611d88a08075ea8190961a0ba3f0266e8c724c093505cf3fc69596bb6eacce5019347a1c4d9bffd02552d13a
7
+ data.tar.gz: cbf461654e119d53e2d7c626b34014d3769f0a536a0d46ecd32c4af25255ff981997e4a8c14ab2d784c27966c5f7085be608250b974e2f1d46f8e14b06c513e8
data/CHANGELOG.md CHANGED
@@ -4,6 +4,36 @@ 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.1 — 2026-09-30
8
+
9
+ ### Fixed
10
+
11
+ - **The engine caps `redis` below 6, as ActionCable's redis adapter requires.**
12
+ The gemspec declared `redis >= 4.0.1` with no ceiling, so a consumer that did not
13
+ pin redis itself floated to 6.0.0, and ActionCable's redis pubsub adapter (which
14
+ activates `redis >= 4, < 6` through Rails 8.1.3) failed to load. The dependency
15
+ is now `>= 4.0.1, < 6`. Consumers already on redis 5 resolve unchanged.
16
+
17
+ ## 0.81.0 — 2026-09-30
18
+
19
+ ### Changed
20
+
21
+ - **Default warning and danger buttons clear WCAG AA under their white label.**
22
+ While an app leaves its warning at the engine default `#FF7C47` (2.55:1 under
23
+ white), the resolver emits `--color-warning-fill` (`#B85933`, 4.65:1); while it
24
+ leaves danger at `#EF4444` (3.76:1), it emits `--color-danger-fill`
25
+ (`#D73D3D`, 4.54:1). `btn-warning` and `btn-danger` paint the fill and fall back
26
+ to `--color-warning` / `--color-danger`, so a configured colour paints as
27
+ before. The role colours themselves, their borders and rings, and the
28
+ `--color-warning-ink` / `--color-danger-ink` text inks are unchanged. Labels
29
+ stay white.
30
+ - **An admin sees one admin cog at every width.** `components/_user_nav` draws its
31
+ admin menu from `md` up only, as it already did the theme toggle: below `md` the
32
+ navbar's phone row draws the cog, so an admin's phone showed two.
33
+ `components/_admin_dropdown` takes an optional `display_class:` for this. Apps
34
+ that shadow `_user_nav` (mcritchie-studio, turf-monster, cyvasse) are unaffected
35
+ until they drop their copy.
36
+
7
37
  ## 0.80.0 — 2026-09-30
8
38
 
9
39
  ### Added
@@ -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" />
@@ -86,8 +86,10 @@
86
86
  <div class="flex items-center gap-2 ml-auto self-end">
87
87
  <%= studio_nav_slot.call(extra_icons_slot, extra_icons_html) %>
88
88
  <%# Skipped when the link sidebar carries the admin menu — two cog
89
- glyphs side by side read as a double gear. %>
90
- <%= 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? %>
91
93
  <%# Desktop only. Below md the engine navbar's phone row draws the
92
94
  theme toggle, so a second one here put two on every phone. The
93
95
  signed-out bar has always done it this way (layouts/_navbar). %>
@@ -219,7 +221,7 @@
219
221
  <span class="<%= account_classes %>" data-nav-account x-bind:style="$store.devMode && 'background: lightgreen'"><%= account_inner %></span>
220
222
  <% end %>
221
223
  <% else %>
222
- <%= 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? %>
223
225
  <%# Desktop only, as above: the phone row carries the toggle below md. %>
224
226
  <span class="hidden md:flex" data-nav-theme-toggle="desktop"><%= render "components/theme_toggle_morph" %></span>
225
227
  <div class="flex items-center gap-3 ml-auto">
@@ -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() {
@@ -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.80.0"
2
+ VERSION = "0.81.1"
3
3
  end
@@ -35,7 +35,15 @@ Gem::Specification.new do |spec|
35
35
  # Realtime: the redis cable/cache/Sidekiq adapter (Studio::Redis) + Turbo Streams
36
36
  # broadcasting (Studio::Broadcastable). `redis` is the dependency whose ABSENCE
37
37
  # 500'd a host app's task board — declaring it here makes that impossible to repeat.
38
- spec.add_dependency "redis", ">= 4.0.1"
38
+ #
39
+ # The `< 6` ceiling is ActionCable's, not ours. Its redis pubsub adapter
40
+ # (action_cable/subscription_adapter/redis.rb) activates `gem "redis", ">= 4",
41
+ # "< 6"` on Rails 7.2 through 8.1.3, so a consumer whose lock floats to redis
42
+ # 6.0.0 fails to load its cable adapter: Cyvasse shipped live chat broken that
43
+ # way, and mcritchie-studio pinned `redis ~> 5.4` after hitting it. Rails 8.1.4
44
+ # widens its bound to `< 7`; lift this cap only once every Rails line this gemspec
45
+ # allows accepts redis 6. test/lib/gemspec_redis_bound_test.rb pins it.
46
+ spec.add_dependency "redis", ">= 4.0.1", "< 6"
39
47
  spec.add_dependency "turbo-rails", ">= 1.0"
40
48
  # IP -> location for Studio::Geo. A dependency rather than a host concern
41
49
  # because the point of the geo primitive is that EVERY app has the capacity:
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.80.0
4
+ version: 0.81.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alex McRitchie
@@ -133,6 +133,9 @@ dependencies:
133
133
  - - ">="
134
134
  - !ruby/object:Gem::Version
135
135
  version: 4.0.1
136
+ - - "<"
137
+ - !ruby/object:Gem::Version
138
+ version: '6'
136
139
  type: :runtime
137
140
  prerelease: false
138
141
  version_requirements: !ruby/object:Gem::Requirement
@@ -140,6 +143,9 @@ dependencies:
140
143
  - - ">="
141
144
  - !ruby/object:Gem::Version
142
145
  version: 4.0.1
146
+ - - "<"
147
+ - !ruby/object:Gem::Version
148
+ version: '6'
143
149
  - !ruby/object:Gem::Dependency
144
150
  name: turbo-rails
145
151
  requirement: !ruby/object:Gem::Requirement