@crossworks/share-ui 0.232.66 → 0.232.67
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.
- package/package.json +3 -3
- package/src/neat-background.test.ts +58 -0
- package/src/neat-background.ts +41 -8
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@crossworks/share-ui",
|
|
3
|
-
"version": "0.232.
|
|
3
|
+
"version": "0.232.67",
|
|
4
4
|
"description": "The server-rendered share surface — the /s/<token> presenters, view-payload contract, mini-app sandbox, and the few primitives they need. Lives in MANTLE (the server renders these), published for jackdaw to consume; may depend only on @mantle/{client-types,content-core} (the jackdaw-repo-split boundary).",
|
|
5
5
|
"exports": {
|
|
6
6
|
"./app-presenter": "./src/app-presenter.tsx",
|
|
@@ -36,8 +36,8 @@
|
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
38
|
"@firecms/neat": "1.0.2",
|
|
39
|
-
"@mantle/client-types": "npm:@crossworks/client-types@0.232.
|
|
40
|
-
"@mantle/content-core": "npm:@crossworks/content-core@0.232.
|
|
39
|
+
"@mantle/client-types": "npm:@crossworks/client-types@0.232.67",
|
|
40
|
+
"@mantle/content-core": "npm:@crossworks/content-core@0.232.67",
|
|
41
41
|
"@radix-ui/react-label": "^2.1.12",
|
|
42
42
|
"@radix-ui/react-slot": "^1.3.0",
|
|
43
43
|
"class-variance-authority": "^0.7.1",
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { decodeNeatSpec, encodeNeatSpec, neatConfigFromSpec } from './neat-background';
|
|
3
|
+
|
|
4
|
+
const SPEC = { v: 1 as const, seed: 2325271021, tone: 'auto' as const, speed: 2 };
|
|
5
|
+
|
|
6
|
+
describe('decodeNeatSpec', () => {
|
|
7
|
+
it('round-trips the canonical encoding', () => {
|
|
8
|
+
expect(decodeNeatSpec(encodeNeatSpec(SPEC))).toEqual(SPEC);
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it('rejects garbage as null, never a throw', () => {
|
|
12
|
+
for (const bad of [null, '', '{}', '{"v":2}', 'not json', '{"v":1,"seed":-1}']) {
|
|
13
|
+
expect(decodeNeatSpec(bad)).toBeNull();
|
|
14
|
+
}
|
|
15
|
+
});
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
describe('neatConfigFromSpec — the wash', () => {
|
|
19
|
+
const long = {
|
|
20
|
+
background: '#ffffff',
|
|
21
|
+
primary: '#b85c23',
|
|
22
|
+
accent: '#eeeeee',
|
|
23
|
+
secondary: '#527575',
|
|
24
|
+
};
|
|
25
|
+
// What getComputedStyle actually returns on a deployed build: the compiled
|
|
26
|
+
// stylesheet minifies hex custom properties to shorthand where possible.
|
|
27
|
+
const short = { background: '#fff', primary: '#b85c23', accent: '#eee', secondary: '#527575' };
|
|
28
|
+
|
|
29
|
+
it('produces the IDENTICAL config for shorthand #rgb and #rrggbb tokens', () => {
|
|
30
|
+
// Whole-config equality on purpose: it pins the wash (a failed shorthand
|
|
31
|
+
// parse degraded stops to the raw brand colour) AND the raw pass-throughs
|
|
32
|
+
// (colors[0]/[4] and backgroundColor once carried '#fff' verbatim, which
|
|
33
|
+
// Neat's parseInt-based parser reads as 0x000fff — electric blue).
|
|
34
|
+
expect(neatConfigFromSpec(SPEC, short, 'light')).toEqual(
|
|
35
|
+
neatConfigFromSpec(SPEC, long, 'light'),
|
|
36
|
+
);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('hands the shader only canonical six-digit hex, never shorthand', () => {
|
|
40
|
+
const c = neatConfigFromSpec(SPEC, short, 'light');
|
|
41
|
+
for (const stop of c.colors) expect(stop.color).toMatch(/^#[0-9a-f]{6}$/);
|
|
42
|
+
expect(c.backgroundColor).toBe('#ffffff');
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('actually washes the brand colour toward the ground', () => {
|
|
46
|
+
const washed = neatConfigFromSpec(SPEC, short, 'light').colors[1]?.color ?? '';
|
|
47
|
+
// Raw primary (or its 1.08 shade, #c66c39-ish) means the mix silently
|
|
48
|
+
// failed; a washed stop is much lighter than the brand colour.
|
|
49
|
+
expect(washed).not.toBe('#b85c23');
|
|
50
|
+
const mean =
|
|
51
|
+
[1, 3, 5].map((i) => parseInt(washed.slice(i, i + 2), 16)).reduce((s, n) => s + n, 0) / 3;
|
|
52
|
+
expect(mean).toBeGreaterThan(150);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('is deterministic — same seed, same config', () => {
|
|
56
|
+
expect(neatConfigFromSpec(SPEC, long, 'dark')).toEqual(neatConfigFromSpec(SPEC, long, 'dark'));
|
|
57
|
+
});
|
|
58
|
+
});
|
package/src/neat-background.ts
CHANGED
|
@@ -109,7 +109,20 @@ function mulberry32(seed: number): () => number {
|
|
|
109
109
|
}
|
|
110
110
|
|
|
111
111
|
function hexToRgb(hex: string): [number, number, number] | null {
|
|
112
|
-
const
|
|
112
|
+
const t = hex.trim();
|
|
113
|
+
// Shorthand #rgb MUST parse: the compiled stylesheets minify hex custom
|
|
114
|
+
// properties (#ffffff → #fff, #eeeeee → #eee), so this is what
|
|
115
|
+
// getComputedStyle actually returns on deployed builds. Rejecting it made
|
|
116
|
+
// mixHex silently return the RAW brand colour — no wash, a saturated
|
|
117
|
+
// poster instead of a tint — on exactly the themes whose tokens shorten,
|
|
118
|
+
// and only in production (dev CSS is unminified). Light modes were hit
|
|
119
|
+
// hardest because near-white grounds (#ffffff/#eeeeee) all shorten.
|
|
120
|
+
const short = /^#?([0-9a-f]{3})$/i.exec(t)?.[1];
|
|
121
|
+
if (short) {
|
|
122
|
+
const c = (i: number) => parseInt(short.charAt(i) + short.charAt(i), 16);
|
|
123
|
+
return [c(0), c(1), c(2)];
|
|
124
|
+
}
|
|
125
|
+
const digits = /^#?([0-9a-f]{6})$/i.exec(t)?.[1];
|
|
113
126
|
if (!digits) return null;
|
|
114
127
|
const n = parseInt(digits, 16);
|
|
115
128
|
return [(n >> 16) & 0xff, (n >> 8) & 0xff, n & 0xff];
|
|
@@ -148,6 +161,18 @@ function shadeHex(hex: string, factor: number): string {
|
|
|
148
161
|
]);
|
|
149
162
|
}
|
|
150
163
|
|
|
164
|
+
/** Canonical #rrggbb for any hex this module can parse; unparseable values
|
|
165
|
+
* pass through untouched. EVERY colour handed to the shader goes through
|
|
166
|
+
* this: Neat's own parser is `parseInt(hex, 16)` on whatever it gets, so a
|
|
167
|
+
* shorthand `#fff` reads as the 24-bit int 0x000fff — rgb(0, 15, 255), an
|
|
168
|
+
* electric blue poster where the page ground should be. That is precisely
|
|
169
|
+
* what deployed light themes produced, because minified stylesheets shorten
|
|
170
|
+
* near-white tokens to #fff/#eee. */
|
|
171
|
+
function normalizeHex(hex: string): string {
|
|
172
|
+
const rgb = hexToRgb(hex);
|
|
173
|
+
return rgb ? rgbToHex(rgb) : hex;
|
|
174
|
+
}
|
|
175
|
+
|
|
151
176
|
const round1 = (n: number) => Math.round(n * 10) / 10;
|
|
152
177
|
const round2 = (n: number) => Math.round(n * 100) / 100;
|
|
153
178
|
|
|
@@ -166,32 +191,40 @@ export function neatConfigFromSpec(
|
|
|
166
191
|
const range = (lo: number, hi: number) => lo + rnd() * (hi - lo);
|
|
167
192
|
const darker = spec.tone === 'darker' || (spec.tone === 'auto' && mode === 'dark');
|
|
168
193
|
|
|
194
|
+
// Tokens arrive as whatever getComputedStyle returns — on deployed builds
|
|
195
|
+
// that includes minifier shorthand. Normalized ONCE here, so both our wash
|
|
196
|
+
// math and the raw pass-throughs below hand the shader canonical hex.
|
|
197
|
+
const background = normalizeHex(tokens.background);
|
|
198
|
+
const primary = normalizeHex(tokens.primary);
|
|
199
|
+
const accent = normalizeHex(tokens.accent);
|
|
200
|
+
const secondary = normalizeHex(tokens.secondary);
|
|
201
|
+
|
|
169
202
|
// A wash: pull the brand colour part-way into the page background, then
|
|
170
203
|
// nudge the result off the surface in the chosen direction. Enough colour
|
|
171
204
|
// to read as a real gradient, close enough to the surface that content
|
|
172
205
|
// sitting on it never fights it.
|
|
173
206
|
const wash = (hex: string, towardBg: number) =>
|
|
174
|
-
shadeHex(mixHex(hex,
|
|
207
|
+
shadeHex(mixHex(hex, background, towardBg), darker ? 0.88 : 1.08);
|
|
175
208
|
|
|
176
209
|
return {
|
|
177
210
|
colors: [
|
|
178
|
-
{ color:
|
|
211
|
+
{ color: background, enabled: true, influence: round2(range(0.3, 0.6)) },
|
|
179
212
|
{
|
|
180
|
-
color: wash(
|
|
213
|
+
color: wash(primary, round2(range(0.25, 0.5))),
|
|
181
214
|
enabled: true,
|
|
182
215
|
influence: round2(range(0.5, 0.9)),
|
|
183
216
|
},
|
|
184
217
|
{
|
|
185
|
-
color: wash(
|
|
218
|
+
color: wash(accent, round2(range(0.25, 0.55))),
|
|
186
219
|
enabled: true,
|
|
187
220
|
influence: round2(range(0.45, 0.85)),
|
|
188
221
|
},
|
|
189
222
|
{
|
|
190
|
-
color: wash(
|
|
223
|
+
color: wash(secondary, round2(range(0.35, 0.6))),
|
|
191
224
|
enabled: true,
|
|
192
225
|
influence: round2(range(0.35, 0.75)),
|
|
193
226
|
},
|
|
194
|
-
{ color:
|
|
227
|
+
{ color: background, enabled: true, influence: round2(range(0.25, 0.55)) },
|
|
195
228
|
],
|
|
196
229
|
speed: spec.speed,
|
|
197
230
|
horizontalPressure: round1(range(2, 5)),
|
|
@@ -210,7 +243,7 @@ export function neatConfigFromSpec(
|
|
|
210
243
|
grainIntensity: round2(range(0.02, 0.1)),
|
|
211
244
|
grainSpeed: 0.3,
|
|
212
245
|
wireframe: false,
|
|
213
|
-
backgroundColor:
|
|
246
|
+
backgroundColor: background,
|
|
214
247
|
backgroundAlpha: 1,
|
|
215
248
|
};
|
|
216
249
|
}
|