diff --git a/src/components/ui/Icon.tsx b/src/components/ui/Icon.tsx
index 27d077f..26e0c8c 100644
--- a/src/components/ui/Icon.tsx
+++ b/src/components/ui/Icon.tsx
@@ -248,7 +248,7 @@ export function Icon({
aria-label={label}
role={label ? 'img' : undefined}
>
-
+
);
diff --git a/tests/icon-geometry.spec.ts b/tests/icon-geometry.spec.ts
new file mode 100644
index 0000000..0bfec42
--- /dev/null
+++ b/tests/icon-geometry.spec.ts
@@ -0,0 +1,167 @@
+/**
+ * Icon geometry regression tests.
+ *
+ * The Contributions icon shipped with a hand-edited `package-16` path whose final
+ * subpath ("M2.5 13.677v-2.3L6.5 13.8v2.3Z") was a leftover sliver extending to
+ * y=16.1 — past the bottom of the 16x16 viewBox, so it rendered as a stray line
+ * poking out of the cube.
+ *
+ * Artwork that escapes its own viewBox is silently clipped by the renderer, which
+ * makes it easy to ship and hard to spot in review. These tests measure the real
+ * geometry of every icon the card renders and fail when any of it falls outside
+ * the box it declares.
+ */
+import { expect, test, type Page } from '@playwright/test';
+
+const avatarDataUrl =
+ 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+/p9sAAAAASUVORK5CYII=';
+
+async function mockGitHubProfile(page: Page) {
+ await page.route('https://api.github.com/users/octocat', async (route) => {
+ await route.fulfill({
+ contentType: 'application/json',
+ body: JSON.stringify({
+ login: 'octocat',
+ name: 'The Octocat',
+ avatar_url: avatarDataUrl,
+ html_url: 'https://github.com/octocat',
+ followers: 42,
+ public_repos: 8,
+ bio: 'GitHub mascot',
+ created_at: '2011-01-25T18:44:36Z',
+ }),
+ });
+ });
+
+ await page.route(
+ 'https://api.github.com/users/octocat/repos?per_page=100&sort=updated',
+ async (route) => {
+ await route.fulfill({
+ contentType: 'application/json',
+ body: JSON.stringify([
+ { stargazers_count: 10, forks_count: 2, language: 'TypeScript' },
+ ]),
+ });
+ }
+ );
+
+ await page.route('https://github.com/octocat.contribs', async (route) => {
+ await route.fulfill({
+ contentType: 'application/json',
+ body: JSON.stringify({
+ total_contributions: 123,
+ weeks: [{ contribution_days: [{ count: 1 }] }],
+ }),
+ });
+ });
+}
+
+async function openDevemonCard(page: Page) {
+ await page.goto('/', { waitUntil: 'networkidle' });
+ const usernameInput = page.getByLabel('GitHub Username');
+ await usernameInput.fill('octocat');
+ await page.getByRole('button', { name: /generate/i }).click();
+ await expect(page.getByText('@octocat').first()).toBeVisible();
+ await page.getByRole('tab', { name: /devémon/i }).click();
+ await expect(page.locator('[data-devemon-card="true"]')).toBeVisible();
+}
+
+/**
+ * Measure each rendered icon's true path geometry against the viewBox it declares.
+ */
+async function measureIcons(page: Page) {
+ return page.evaluate(() => {
+ const card = document.querySelector('[data-devemon-card="true"]')!;
+
+ return [...card.querySelectorAll('svg')].map((svg) => {
+ const [vx, vy, vw, vh] = (svg.getAttribute('viewBox') ?? '0 0 16 16')
+ .split(/\s+/)
+ .map(Number);
+
+ let box: { x: number; y: number; mx: number; my: number } | null = null;
+ for (const path of svg.querySelectorAll('path')) {
+ const b = path.getBBox();
+ box = box
+ ? {
+ x: Math.min(box.x, b.x),
+ y: Math.min(box.y, b.y),
+ mx: Math.max(box.mx, b.x + b.width),
+ my: Math.max(box.my, b.y + b.height),
+ }
+ : { x: b.x, y: b.y, mx: b.x + b.width, my: b.y + b.height };
+ }
+
+ return {
+ label: svg.getAttribute('aria-label') ?? '(unlabelled)',
+ viewBox: { x: vx, y: vy, mx: vx + vw, my: vy + vh },
+ box,
+ };
+ });
+ });
+}
+
+test('card icons stay inside their viewBox', async ({ page }) => {
+ await mockGitHubProfile(page);
+ await openDevemonCard(page);
+
+ const icons = await measureIcons(page);
+ expect(icons.length).toBeGreaterThan(0);
+
+ // Sub-pixel slack: stroke joins can round a hair past the edge legitimately.
+ const tolerance = 0.02;
+
+ const escaping = icons.filter(
+ (icon) =>
+ icon.box !== null &&
+ (icon.box.x < icon.viewBox.x - tolerance ||
+ icon.box.y < icon.viewBox.y - tolerance ||
+ icon.box.mx > icon.viewBox.mx + tolerance ||
+ icon.box.my > icon.viewBox.my + tolerance)
+ );
+
+ expect(
+ escaping.map(
+ (i) =>
+ `${i.label}: [${i.box!.x.toFixed(2)}, ${i.box!.y.toFixed(2)} -> ` +
+ `${i.box!.mx.toFixed(2)}, ${i.box!.my.toFixed(2)}] outside ` +
+ `[${i.viewBox.x}, ${i.viewBox.y} -> ${i.viewBox.mx}, ${i.viewBox.my}]`
+ )
+ ).toEqual([]);
+});
+
+test('contributions icon is a closed, centered cube', async ({ page }) => {
+ await mockGitHubProfile(page);
+ await openDevemonCard(page);
+
+ const contributions = await page.evaluate(() => {
+ const svg = document
+ .querySelector('[data-devemon-card="true"]')!
+ .querySelector('svg[aria-label="Contributions"]');
+ if (!svg) return null;
+
+ const paths = [...svg.querySelectorAll('path')];
+ const b = paths[0].getBBox();
+
+ return {
+ pathCount: paths.length,
+ x: b.x,
+ y: b.y,
+ mx: b.x + b.width,
+ my: b.y + b.height,
+ };
+ });
+
+ expect(contributions).not.toBeNull();
+
+ // A single path draws the whole cube; a stray leftover subpath is what broke it.
+ expect(contributions!.pathCount).toBe(1);
+
+ // The cube is horizontally symmetric within the 16x16 box. The broken version
+ // started at x=0.378 on the left but stopped at x=15 on the right.
+ const leftGap = contributions!.x;
+ const rightGap = 16 - contributions!.mx;
+ expect(Math.abs(leftGap - rightGap)).toBeLessThan(0.05);
+
+ // ...and it must not hang below the box, which is what produced the stray line.
+ expect(contributions!.my).toBeLessThanOrEqual(16);
+});