Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@isimplifyme/ui",
"version": "1.4.0",
"version": "1.4.1",
"description": "React/Next.js UI library for iSimplifyMe properties \u2014 design system, article layouts, SEO helpers, bot middleware, and the iSM Concierge widget.",
"homepage": "https://isimplifyme.com",
"type": "module",
Expand Down
37 changes: 37 additions & 0 deletions src/concierge.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -114,3 +114,40 @@ describe('with a leading node', () => {
expect(slot.style.pointerEvents).toBe('none');
});
});

describe('auto theme — the colour parsing these refactors touched', () => {
// parseRgba and luminance were rewritten to satisfy
// noUncheckedIndexedAccess (gridiron is the first consumer strict enough
// to typecheck this package's source). Neither is exported, so the honest
// test is the observable one: does auto-theme still TELL THE TWO APART?
function barBackgroundOn(bodyColor: string) {
// jsdom implements no hit-testing; the auto path samples whatever sits
// under the bar, which on a real page is the section behind it.
(document as unknown as { elementFromPoint: () => Element })
.elementFromPoint = () => document.body;
document.body.style.backgroundColor = bodyColor;
const { bar } = render({ theme: 'auto' });
return bar.style.background;
}

it('resolves a dark page differently from a light one', () => {
const onDark = barBackgroundOn('rgb(14, 15, 19)');
act(() => root.unmount());
container.remove();
container = document.createElement('div');
document.body.appendChild(container);
root = createRoot(container);
const onLight = barBackgroundOn('rgb(247, 247, 245)');

expect(onDark).not.toBe('');
expect(onLight).not.toBe('');
// If parseRgba returned null for either, both would land on the same
// fallback and this would pass vacuously — so assert they DIFFER.
expect(onDark).not.toBe(onLight);
});

it('survives a background colour it cannot parse', () => {
const bg = barBackgroundOn('color(display-p3 0.1 0.1 0.1)');
expect(bg).not.toBe('');
});
});
22 changes: 14 additions & 8 deletions src/concierge.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -168,11 +168,13 @@ function readCookie(name: string): string {
const match = document.cookie.match(
new RegExp(`(?:^|;\\s*)${name}=([^;]*)`)
);
if (!match) return '';
const value = match?.[1];
if (value === undefined) return '';
try {
return decodeURIComponent(match[1]);
return decodeURIComponent(value);
} catch {
return match[1];
// Malformed percent-encoding — hand back the raw value rather than ''.
return value;
}
}

Expand Down Expand Up @@ -278,16 +280,20 @@ export default function ConciergeWidget({

function parseRgba(color: string): [number, number, number, number] | null {
const m = color.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*([\d.]+))?\)/);
if (!m) return null;
return [+m[1], +m[2], +m[3], m[4] !== undefined ? +m[4] : 1];
const [, r, g, b, a] = m ?? [];
if (r === undefined || g === undefined || b === undefined) return null;
return [+r, +g, +b, a !== undefined ? +a : 1];
}

function luminance(r: number, g: number, b: number): number {
const [rs, gs, bs] = [r, g, b].map((c) => {
// A mapped array is `number[]`, not a 3-tuple, so destructuring it
// yields `number | undefined` under noUncheckedIndexedAccess. Mapping
// each channel keeps the arity in the types.
const srgb = (c: number) => {
const s = c / 255;
return s <= 0.03928 ? s / 12.92 : Math.pow((s + 0.055) / 1.055, 2.4);
});
return 0.2126 * rs + 0.7152 * gs + 0.0722 * bs;
};
return 0.2126 * srgb(r) + 0.7152 * srgb(g) + 0.0722 * srgb(b);
}

function sampleBackground(el: Element): 'dark' | 'light' {
Expand Down
Loading