studio-engine 0.31.0 → 0.32.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 +4 -4
- data/CHANGELOG.md +18 -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: 445252feafe8e5767f83fcfc37ef8278a39e3cf02c10f050e1e89eb0f1dc779e
|
|
4
|
+
data.tar.gz: f99cd313622b6eaa1b94a4d20bd4d2b2bca026f9c44899e0a94cdfef2c18a19c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 67d422357f8bfe80704d7c61dcdfce194f86dd902d460837ec85424db4fe3a804d41516213fe2c8e171c304bca120c3b70af460f8b9df5c8bbc4f39993dafabc
|
|
7
|
+
data.tar.gz: cbcd30b10f5f9680ad79f60f541eca0afbc93e824522c914350e2f63cb4b4f47ba3bc48c4cd25afa9bc73ada4a9dbba08321b82c094651a63998eebf7dfbdf1a
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,24 @@
|
|
|
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.0 — Unreleased
|
|
6
|
+
|
|
7
|
+
**Readable ink on every theme, and the `btn-primary` label joins the token
|
|
8
|
+
contract.** (The token work merged after 0.31.0 was cut, so both land here.)
|
|
9
|
+
|
|
10
|
+
Dark- and light-mode `--color-text-muted` / `--color-text-secondary` no longer
|
|
11
|
+
hardcode slate grays: they derive from the theme base by a bounded contrast
|
|
12
|
+
search (`ColorScale.contrast_ratio` is new public API) that walks the blend
|
|
13
|
+
until the ink clears 3:1 / 4.5:1 on every emitted background — for any
|
|
14
|
+
operator-picked base, not just the fleet's. Fixed grays measured as low as
|
|
15
|
+
2.05:1 on themed surfaces.
|
|
16
|
+
|
|
17
|
+
Consumers can now also set `--btn-primary-fg` (and `--btn-primary-fg-hover`
|
|
18
|
+
for the darker hover fill) on `:root` exactly like the existing
|
|
19
|
+
`--btn-secondary-*` tokens; a light brand primary (McRitchie Industries'
|
|
20
|
+
furnace orange) fails WCAG under the hardcoded white label. Defaults resolve
|
|
21
|
+
to the same white as before, so tokenless consumers are unchanged.
|
|
22
|
+
|
|
5
23
|
## 0.31.0 — 2026-08-08
|
|
6
24
|
|
|
7
25
|
**One magic-link token format, one door, and a click that stops breaking your
|
|
@@ -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