studio-engine 0.31.0 → 0.32.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: c36ecfe3e213e5621f205743728461c24d02912ed7e7bbf5109e0c15afd3225e
4
- data.tar.gz: d92755cdb2ed3f7b8dca941bb514e2c725314056afbc83ee7fb190a4cc4a993c
3
+ metadata.gz: e04fa2a8f860281a68cd8534487c64308bc35b0e7fa36911e0b57b6a518be0fc
4
+ data.tar.gz: 774c6f65e6e08cd2204960973dea33f2e44c8e299d9683679dab6e9394e2dc8e
5
5
  SHA512:
6
- metadata.gz: 17c9f349695792402a9cbcb245159ebcba107b355edcdc9c5ae3b3d89d23303d0bd3a4bd96846d1d23c23417fbff47a0f73e0db887bd8880faeb4687e7e48c12
7
- data.tar.gz: c65f117c47ced22be63a3747f0cd2b283f00a80c40802947a1003ada976749242ebd8340a511d89c9806d9449c6a3525eb1cbebb5fdd47d37cf334465d2e2644
6
+ metadata.gz: de4d5bcc12a5bc890655a3d49d7d88fdcf3e0a44de5951f0adbf7e6153eb146e93c6645600e642a7624ab74c8da5fed361f288a8b55b0c797603fdfd258eda8f
7
+ data.tar.gz: 761e8c0e87a81e9797164d44000c1e6e94dd2341bfbecf8ad7f60ef830c1f5e528f5fb09d6e73d502c13f420a9b06794c294440887d637b1df90e51840fe28a3
data/CHANGELOG.md CHANGED
@@ -2,6 +2,70 @@
2
2
 
3
3
  The format is [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). This project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html) — `MAJOR.MINOR.PATCH`. Consumer Rails apps install the released RubyGems package with `gem "studio-engine", "~> 0.6"`; bumping the gem version and updating consumer lockfiles is a release.
4
4
 
5
+ ## 0.32.1 — Unreleased
6
+
7
+ **Setup-guide corrections.** Docs only — no code, no behavior change.
8
+
9
+ ### Docs
10
+
11
+ - **`NEW_APP_SETUP.md` § 15 "Lint And CI" — the two things a fresh app gets wrong
12
+ by default.** Both surface as a red CI lane on the app's first push and both
13
+ read like code defects when they aren't.
14
+
15
+ *Lint:* the generated `.rubocop.yml` inherits `rubocop-rails-omakase`, which
16
+ enforces `[ a, b ]`, while scaffolded code routinely writes `[a, b]` — so a new
17
+ app is usually born with a red `lint` lane. Run `bin/rubocop -a` before the
18
+ first commit, and either format to the cop or disable it deliberately. The
19
+ section tabulates which apps did which, stamped with the `origin/main` ref it
20
+ was surveyed at; that table lives in the guide rather than here because
21
+ `studio-engine.gemspec:21` packages this CHANGELOG but excludes `docs/`, so the
22
+ guide can be corrected without an erratum and this file cannot.
23
+
24
+ *CI:* the generated workflow installs a short fixed list and nothing else, so an
25
+ app that shells out to any other binary must add it to the install step in EVERY
26
+ test-running job. How many jobs that is depends on your Rails version — 8.1
27
+ generates separate `test` and `system-test` jobs with separate install steps,
28
+ 7.2 a single job running both suites — and the guide tabulates both. The
29
+ failure is misleading: the test raises inside its own setup, so the lane reads
30
+ as a code defect rather than a missing package. moms-app's `BookStitcher` shells
31
+ out to `ffmpeg`, its README had listed `ffmpeg` as a requirement all along, and
32
+ the requirement was simply never told to CI.
33
+ - **`NEW_APP_SETUP.md` § 15 also covers the system-test lane that tests nothing.**
34
+ `rails new` wires a `test:system` CI invocation and the capybara/selenium gems,
35
+ and what you get behind it depends on your Rails version. On **7.2**
36
+ (`app_generator.rb:254-258`, called at `:459` under `depends_on_system_test?` —
37
+ true for a plain `rails new`) `test/system/.keep` and
38
+ `application_system_test_case.rb` are created, so the lane
39
+ runs, finds zero tests, and reports **GREEN** — a pass that means nothing. On
40
+ **8.1** (`:257-262`) that scaffolding sits behind a
41
+ `devcontainer? && depends_on_system_test?` guard, so a plain `rails new` gets the
42
+ invocation without the directory and the lane goes **RED** on
43
+ `cannot load such file -- test/system`. Either way the lane is not testing your
44
+ app; the green one is the harder to notice. The section tabulates where each
45
+ house app actually stands.
46
+ - **§ 16 Verify** no longer asks you to confirm a "yellow Development Environment
47
+ bar" — that hand-rolled strip was replaced by the shared environment banner in
48
+ 0.30.0, so the checklist now names the banner, DEV MODE, and the Local Inbox link.
49
+
50
+
51
+ ## 0.32.0 — 2026-08-09
52
+
53
+ **Readable ink on every theme, and the `btn-primary` label joins the token
54
+ contract.** (The token work merged after 0.31.0 was cut, so both land here.)
55
+
56
+ Dark- and light-mode `--color-text-muted` / `--color-text-secondary` no longer
57
+ hardcode slate grays: they derive from the theme base by a bounded contrast
58
+ search (`ColorScale.contrast_ratio` is new public API) that walks the blend
59
+ until the ink clears 3:1 / 4.5:1 on every emitted background — for any
60
+ operator-picked base, not just the fleet's. Fixed grays measured as low as
61
+ 2.05:1 on themed surfaces.
62
+
63
+ Consumers can now also set `--btn-primary-fg` (and `--btn-primary-fg-hover`
64
+ for the darker hover fill) on `:root` exactly like the existing
65
+ `--btn-secondary-*` tokens; a light brand primary (McRitchie Industries'
66
+ furnace orange) fails WCAG under the hardcoded white label. Defaults resolve
67
+ to the same white as before, so tokenless consumers are unchanged.
68
+
5
69
  ## 0.31.0 — 2026-08-08
6
70
 
7
71
  **One magic-link token format, one door, and a click that stops breaking your
@@ -117,6 +181,7 @@ Do these two in this order; they do not commute.
117
181
  `consume_magic_link` / `preview_magic_link` and overriding hooks. In
118
182
  particular, move any `reset_session` into `sign_in_existing` only — the
119
183
  `:continue` path must not reach it.
184
+
120
185
  ## 0.30.1 — 2026-08-08
121
186
 
122
187
  **`NEW_APP_SETUP.md` § 5 gave a command that does not exist.** 0.30.0 documented
@@ -71,6 +71,8 @@
71
71
  ADDITIVE in Tailwind v4 (both definitions emit and the engine's rules leak in),
72
72
  so forking is not a clean override. Set these on :root in the consumer's CSS:
73
73
 
74
+ --btn-primary-fg label color (default: #fff)
75
+ --btn-primary-fg-hover hover label (default: same as --btn-primary-fg)
74
76
  --btn-secondary-bg base fill (default: var(--color-success))
75
77
  --btn-secondary-bg-hover hover fill (default: same as base)
76
78
  --btn-secondary-hover-filter hover filter (default: brightness(0.9))
@@ -97,10 +99,18 @@
97
99
  }
98
100
  }
99
101
 
102
+ /* btn-primary's label is token-overridable like btn-secondary's fill: a light
103
+ brand primary (e.g. industries' furnace orange) fails contrast under a white
104
+ label, so such apps set --btn-primary-fg (and, when the darker hover fill
105
+ flips the readable choice, --btn-primary-fg-hover) instead of fighting the
106
+ @utility with unlayered overrides. Defaults resolve to the same white as
107
+ before (a literal #fff — the engine's both-modes guard bars raw
108
+ --color-white references, which only exist in Tailwind's compiled theme). */
100
109
  @utility btn-primary {
101
- @apply text-white;
110
+ color: var(--btn-primary-fg, #fff);
102
111
  background-color: var(--color-cta);
103
112
  &:hover {
113
+ color: var(--btn-primary-fg-hover, var(--btn-primary-fg, #fff));
104
114
  background-color: var(--color-cta-hover);
105
115
  }
106
116
  }
@@ -72,6 +72,19 @@ module Studio
72
72
  "#%02X%02X%02X" % [r.clamp(0, 255), g.clamp(0, 255), b.clamp(0, 255)]
73
73
  end
74
74
 
75
+ # WCAG 2.x relative luminance (sRGB linearized).
76
+ def self.relative_luminance(hex)
77
+ r, g, b = hex_to_rgb(hex).map { |c| c / 255.0 }
78
+ lin = [r, g, b].map { |c| c <= 0.03928 ? c / 12.92 : ((c + 0.055) / 1.055)**2.4 }
79
+ (0.2126 * lin[0]) + (0.7152 * lin[1]) + (0.0722 * lin[2])
80
+ end
81
+
82
+ # WCAG contrast ratio between two hex colors (1.0..21.0).
83
+ def self.contrast_ratio(a, b)
84
+ hi, lo = [relative_luminance(a), relative_luminance(b)].minmax.reverse
85
+ (hi + 0.05) / (lo + 0.05)
86
+ end
87
+
75
88
  def self.with_opacity(hex, opacity)
76
89
  r, g, b = hex_to_rgb(hex)
77
90
  "rgba(#{r},#{g},#{b},#{opacity})"
@@ -39,8 +39,18 @@ module Studio
39
39
  "--color-inset" => ColorScale.darken(dark_base, 0.43),
40
40
  "--color-text" => "#ffffff",
41
41
  "--color-text-body" => "#e2e8f0",
42
- "--color-text-secondary" => "#94a3b8",
43
- "--color-text-muted" => "#64748b",
42
+ # Derived by a bounded contrast search, not hardcoded: fixed slate
43
+ # grays fail WCAG on themes whose dark base lifts the surface (a
44
+ # hardcoded #64748b measured 2.14:1 on a navy surface), and any FIXED
45
+ # blend amount is tuned to particular hexes — the operator can pick an
46
+ # arbitrary base in the theme editor. contrast_ink walks the blend up
47
+ # until the ink clears its target on every emitted dark surface
48
+ # (clamped at pure white for pathological bases). Note the blend
49
+ # DESATURATES toward gray; only strongly-tinted bases keep a cast.
50
+ "--color-text-secondary" => contrast_ink(dark_base, direction: :lighten, start: 0.70, target: 4.5,
51
+ against: dark_surfaces(dark_base)),
52
+ "--color-text-muted" => contrast_ink(dark_base, direction: :lighten, start: 0.55, target: 3.0,
53
+ against: dark_surfaces(dark_base)),
44
54
  "--color-border" => ColorScale.with_opacity(border_rgb, 0.2),
45
55
  "--color-border-strong" => ColorScale.with_opacity(border_rgb, 0.4),
46
56
  "--color-shadow" => "transparent",
@@ -53,6 +63,36 @@ module Studio
53
63
  }
54
64
  end
55
65
 
66
+ # Every dark-mode background a text var can sit on (page, surface,
67
+ # surface-alt, inset) — the ink must clear its target on ALL of them.
68
+ def dark_surfaces(dark_base)
69
+ [ dark_base,
70
+ ColorScale.lighten(dark_base, 0.15),
71
+ ColorScale.darken(dark_base, 0.14),
72
+ ColorScale.darken(dark_base, 0.43) ]
73
+ end
74
+
75
+ def light_surfaces(light_base)
76
+ [ light_base,
77
+ "#ffffff",
78
+ ColorScale.darken(light_base, 0.03),
79
+ ColorScale.darken(light_base, 0.08) ]
80
+ end
81
+
82
+ # Bounded, clamped search: raise the blend amount from `start` until the
83
+ # ink clears `target` contrast against every background in `against`.
84
+ # Clamps at 1.0 (pure white/black), so a pathological base degrades to the
85
+ # best achievable ink instead of looping.
86
+ def contrast_ink(base, direction:, start:, target:, against:)
87
+ amount = start
88
+ loop do
89
+ ink = direction == :lighten ? ColorScale.lighten(base, amount) : ColorScale.darken(base, amount)
90
+ return ink if against.all? { |bg| ColorScale.contrast_ratio(ink, bg) >= target } || amount >= 1.0
91
+
92
+ amount = [amount + 0.02, 1.0].min
93
+ end
94
+ end
95
+
56
96
  # Generate --color-primary-{50..900} + RGB variants for Tailwind opacity support
57
97
  def primary_palette_vars
58
98
  primary = colors[:primary] || "#8E82FE"
@@ -84,8 +124,13 @@ module Studio
84
124
  "--color-inset" => ColorScale.darken(light_base, 0.08),
85
125
  "--color-text" => "#0f172a",
86
126
  "--color-text-body" => "#334155",
87
- "--color-text-secondary" => "#64748b",
88
- "--color-text-muted" => "#94a3b8",
127
+ # Same bounded search as dark mode: the old fixed grays measured as
128
+ # low as 2.05:1 (muted on --color-inset) — below the very defect this
129
+ # derivation exists to prevent. Ink darkens away from the light base.
130
+ "--color-text-secondary" => contrast_ink(light_base, direction: :darken, start: 0.55, target: 4.5,
131
+ against: light_surfaces(light_base)),
132
+ "--color-text-muted" => contrast_ink(light_base, direction: :darken, start: 0.40, target: 3.0,
133
+ against: light_surfaces(light_base)),
89
134
  "--color-border" => ColorScale.darken(light_base, 0.08),
90
135
  "--color-border-strong" => ColorScale.darken(light_base, 0.15),
91
136
  "--color-shadow" => "rgba(0,0,0,0.05)",
@@ -1,3 +1,3 @@
1
1
  module Studio
2
- VERSION = "0.31.0"
2
+ VERSION = "0.32.1"
3
3
  end
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.31.0
4
+ version: 0.32.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alex McRitchie