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 +4 -4
- data/CHANGELOG.md +65 -0
- data/app/assets/tailwind/studio_engine/engine.css +11 -1
- data/lib/studio/color_scale.rb +13 -0
- data/lib/studio/theme_resolver.rb +49 -4
- data/lib/studio/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e04fa2a8f860281a68cd8534487c64308bc35b0e7fa36911e0b57b6a518be0fc
|
|
4
|
+
data.tar.gz: 774c6f65e6e08cd2204960973dea33f2e44c8e299d9683679dab6e9394e2dc8e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
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
|
}
|
data/lib/studio/color_scale.rb
CHANGED
|
@@ -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
|
-
|
|
43
|
-
|
|
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
|
-
|
|
88
|
-
|
|
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)",
|
data/lib/studio/version.rb
CHANGED