From 31953380f6736e903e49136c726f3875614bf6f1 Mon Sep 17 00:00:00 2001 From: Harlan Wilton Date: Tue, 6 Oct 2026 14:05:25 +1100 Subject: [PATCH 1/2] feat(og): redraw OG cards as the homepage hero The cards still set titles in mono with the old tagline, so a shared link looked like the site before its rebrand. Every card now centres the lockup, a Plus Jakarta Sans title and its detail, with braille names in the margins, matching the new hero. The frame lives in OgLayout, so Splash and Make a skill became plain Page cards and the per-card file minimap went with them. Two-line text breaks evenly in OgLines, because takumi's text-wrap: balance shifts balanced lines off centre. --- COPY.md | 6 +- DESIGN.md | 15 ++- app/components/OgImage/Collection.takumi.vue | 111 +++++----------- app/components/OgImage/Curator.takumi.vue | 101 +++++--------- app/components/OgImage/MakeSkill.takumi.vue | 49 ------- app/components/OgImage/Page.takumi.vue | 24 +--- app/components/OgImage/Skill.takumi.vue | 112 ++++++---------- app/components/OgImage/Splash.takumi.vue | 45 ------- app/components/OgLayout.vue | 50 ++++++- app/components/OgLines.vue | 24 ++++ app/pages/index.vue | 7 +- app/utils/og-style.ts | 84 ++++++++++++ app/utils/og-texture.ts | 130 +------------------ layers/marketing/app/pages/make-skill.vue | 2 +- test/unit/og-style.test.ts | 33 +++++ test/unit/og-texture.test.ts | 22 +--- 16 files changed, 321 insertions(+), 494 deletions(-) delete mode 100644 app/components/OgImage/MakeSkill.takumi.vue delete mode 100644 app/components/OgImage/Splash.takumi.vue create mode 100644 app/components/OgLines.vue create mode 100644 app/utils/og-style.ts create mode 100644 test/unit/og-style.test.ts diff --git a/COPY.md b/COPY.md index 14c8c1fa..e9f61969 100644 --- a/COPY.md +++ b/COPY.md @@ -19,13 +19,13 @@ These exact strings. Do not paraphrase them per page. | Asset | String | Where it goes | | --- | --- | --- | | Name | `skilld` lowercase by default, `Skilld` only at a sentence start, `skilld.dev` for the domain. Never SKILLD, Skill'd, Skill-d, SkillD. | guidance only (applies everywhere, not one placement) | -| Tagline | Curated agent skills by humans. | index OG image alt | +| Tagline | Curated agent skills by humans. | guidance only: the five-word product summary. The homepage and its OG card use the Home H1 instead | | Site description | Curated agent skills by humans, written by real maintainers in their own GitHub repos | `nuxt.config.ts` site description (feeds every page's meta description) | | Elevator pitch | Skilld is a curated registry of agent skills written by real people in the GitHub repos you already depend on. One install command, every agent; watch your stack and get a digest when it changes. | guidance only: no page ships it yet (use it where two sentences fit) | -| Home H1 | Agent skills for you and your agent | guidance only: the homepage H1. The page draws the rose brand dot as its full stop, so never type a period | +| Home H1 | Agent skills for you and your agent | guidance only: the homepage H1, and the title of the homepage OG card. The page draws the rose brand dot as its full stop, so never type a period. The card types no full stop either, because its lockup dot is its one rose element. It breaks the line after "for you", as the page does. Its alt text is "skilld, agent skills for you and your agent" | | Home verb line | search · run · install · keep current | guidance only: the homepage line under the H1. Each verb links to its `/cli` section: `#search`, `#run`, `#install`, `#update` | | Home CLI link | The skilld CLI → | guidance only: the homepage link to `/cli` at the end of the verb line. The arrow is an icon | -| Home caption | Try any skill before you install it. Your agent can search for its own. | guidance only: the homepage caption under the verb line. It assumes the reader knows what a Skill is, so it names only what sets skilld apart. "Search for its own" is the skilld Skill behind the promo label | +| Home caption | Try any skill before you install it. Your agent can search for its own. | guidance only: the homepage caption under the verb line. It assumes the reader knows what a Skill is, so it names only what sets skilld apart. "Search for its own" is the skilld Skill behind the promo label. The homepage OG card uses it as its description | | Home claims line | Open-source CLI, no telemetry · Analytics without cookies or IPs · A skills.sh alternative | guidance only: the homepage claims line under the search. The third item links to `/vs/skills-sh`, and the promo label closes the line | | Promo label | Teach your agent skilld | guidance only: the last item of the homepage claims line. It opens a popover with `npx skilld install skilld --global` | | Step 01: find | Find · Stay hyped. A curated registry, plus what devs talk about on X and Bluesky. | guidance only: the first step of the homepage lifecycle band. The step name comes first, then the pitch line, then the description. Links `/skills/trending`. The braille spark motif pairs with the pitch line | diff --git a/DESIGN.md b/DESIGN.md index 1b3b194d..90c817eb 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -321,7 +321,7 @@ Three textures replace the noise field. Each has one job, and none appears outsi | --- | --- | --- | | **Braille names** | Hero, OG cards, CLI | Rows of real Skill names in braille. Far from the rose dot they thin to sparse dots. Near it they resolve into letters. Each hop of the pick sends three rings outward, half a second apart. Every cell is a real character, so the same field prints in a terminal | | **Converge** | Dividers and loading | Scattered dots fall into one line and end on a rose dot. Many Skills in, one out. It reads as progress | -| **File minimap** | Skill pages and per-Skill OG cards | Each Skill as its own `SKILL.md`: frontmatter, headings, lists and code drawn as rows of dots. The rose dot is the cursor on the last changed line | +| **File minimap** | Skill pages | Each Skill as its own `SKILL.md`: frontmatter, headings, lists and code drawn as rows of dots. The rose dot is the cursor on the last changed line | Rules for every texture: @@ -332,14 +332,14 @@ Rules for every texture: 5. **One per region.** No other motion runs in the same viewport region as a moving texture. 6. **Converge is the loader.** At small sizes the file minimap looks like a skeleton loader. Do not use it as one. -**On OG cards.** An OG card is one still frame, drawn server side as SVG dots. The OG renderer has only Plus Jakarta Sans and IBM Plex Mono, and neither has braille glyphs, so a card never relies on a glyph or a canvas. The lockup's dot is the card's one rose element. Textures on a card draw stone only, or use the lockup's dot as their focal point. `app/utils/og-texture.ts` draws them. +**On OG cards.** An OG card is the homepage hero as one still frame, drawn server side. The OG renderer has only Plus Jakarta Sans and IBM Plex Mono, and neither has braille glyphs, so the texture is SVG dots, never a glyph or a canvas. `OgLayout` draws the frame for every card: the lockup centred at the top, the content centred under it, and Braille names in the margins. The field resolves toward the lockup's dot, which is the card's one rose element. The title takes the hero H1's face, Plus Jakarta Sans 600 at −0.045em. A Skill name has no space to wrap at, so it shrinks to fit one line. Text that needs two lines breaks into two of even length, since the renderer's `text-wrap: balance` pushes the lines off centre. `app/utils/og-texture.ts` draws the dots, `app/utils/og-style.ts` holds the type, and `OgLines` draws the even lines. -| Card | Texture | +| Card | Content | | --- | --- | -| Splash (`/`) | Braille names. The focal point is the lockup's dot, so the noise resolves into the wordmark | -| Skill | File minimap down the right edge, faded in from the left. Each Skill seeds its own files from its owner, repository and name | -| Make a skill | One `SKILL.md` drawn large from its frontmatter down | -| Page, Collection, Curator | None | +| Page | Title and description. `/` uses it with the hero H1 and caption | +| Skill | Name, then owner, then the curator note or the curator count | +| Collection | Name, description, curator, then the curator note or the first Skills | +| Curator | Avatar, name, handle with counts, description, then the first Skills | ### Motifs @@ -455,6 +455,7 @@ These are editorial sets, not necessarily database collections yet. If a set has - **One geometry feeds every placement**: `shared/brand-mark.ts` holds the coordinates and the outlined wordmark. `AppLogo`, the OG lockup and every file in `public/` come from it, and `pnpm brand:assets` writes the files. The old chevron lived in hand-written copies, and their rose shades and wordmark weights had drifted apart. Confirmed 2026-10-06. - **Three textures replace the noise field**: Braille names for the hero, OG cards and the CLI; Converge for dividers and loading; File minimap for Skill pages and per-Skill OG cards. One job each, so no texture becomes wallpaper. Confirmed 2026-10-06. - **The lockup dot is an OG card's one rose element**: the 4px rose top bar, the rose quote rails and the rose avatar ring went stone. On the Splash card, the braille field radiates from the lockup's dot, so the texture spends no second rose. Confirmed 2026-10-06. +- **OG cards repeat the homepage hero**: every card centres the lockup, a Plus Jakarta Sans title and its detail, with Braille names in the margins. Harlan chose this over a left-aligned editorial card and a card that leads with the run command. It supersedes the per-card textures above: the Skill card's file minimap, the Make a skill card's large `SKILL.md`, and the separate Splash and Make a skill templates are gone. Confirmed 2026-10-06. - **The braille spark replaces the trending flame**: seven braille bars for seven days of mentions, today in rose. The emoji exception for Trending is retired. The spark keeps the sourced mention count beside it, which the flame never carried. Confirmed 2026-10-06. - **Run and install read as different materials**: run is a dashed border with a hollow dot, because nothing stays; install is a solid border with a filled dot, because files land. Run is preselected. Confirmed 2026-10-06. - **Trending shares the /skills shell**: `/skills/trending` uses the full-width layout from `/skills`. Its heading replaces `CompactPageHeader` and lines up with the results column. A sticky 14rem sidebar holds the range links, then the weekly CTA and the skilld install. Nothing sits right of the board, so the board takes every column the sidebar leaves. Rows switch on their own width, and the post carousel shows two cards where it has room. On narrow screens the ranges are a compact row above the board, and the CTA stays after the fifth row. Harlan asked for this on 2026-10-06. It supersedes the 2026-10-01 call that dropped the 9/3 sidebar for taking a quarter of the board's width: that sidebar sat right of a 64rem board, and this one sits left of a board that runs the full page width. diff --git a/app/components/OgImage/Collection.takumi.vue b/app/components/OgImage/Collection.takumi.vue index 47c9cd7c..ebf9b9ab 100644 --- a/app/components/OgImage/Collection.takumi.vue +++ b/app/components/OgImage/Collection.takumi.vue @@ -1,5 +1,6 @@ - - diff --git a/app/components/OgImage/Page.takumi.vue b/app/components/OgImage/Page.takumi.vue index 1b8c0d7a..da07d521 100644 --- a/app/components/OgImage/Page.takumi.vue +++ b/app/components/OgImage/Page.takumi.vue @@ -1,5 +1,6 @@ diff --git a/app/components/OgImage/Skill.takumi.vue b/app/components/OgImage/Skill.takumi.vue index 7b2a866e..13a8329a 100644 --- a/app/components/OgImage/Skill.takumi.vue +++ b/app/components/OgImage/Skill.takumi.vue @@ -1,7 +1,6 @@ diff --git a/app/components/OgImage/Splash.takumi.vue b/app/components/OgImage/Splash.takumi.vue deleted file mode 100644 index f0c8d1ff..00000000 --- a/app/components/OgImage/Splash.takumi.vue +++ /dev/null @@ -1,45 +0,0 @@ - - - diff --git a/app/components/OgLayout.vue b/app/components/OgLayout.vue index b1def39a..8fa88dd9 100644 --- a/app/components/OgLayout.vue +++ b/app/components/OgLayout.vue @@ -1,3 +1,34 @@ + + diff --git a/app/components/OgLines.vue b/app/components/OgLines.vue new file mode 100644 index 00000000..07ba4db8 --- /dev/null +++ b/app/components/OgLines.vue @@ -0,0 +1,24 @@ + + + diff --git a/app/pages/index.vue b/app/pages/index.vue index 169518ce..a9037ec8 100644 --- a/app/pages/index.vue +++ b/app/pages/index.vue @@ -22,7 +22,12 @@ useHead({ templateParams: { separator: '·' }, }) -defineOgImage('Splash.takumi', {}, { alt: 'skilld, curated agent skills by humans' }) +// The card repeats the hero: the H1 and the caption from COPY.md, with the +// H1 broken where the page breaks it. +defineOgImage('Page.takumi', { + title: 'Agent skills for you\nand your agent', + description: 'Try any skill before you install it. Your agent can search for its own.', +}, { alt: 'skilld, agent skills for you and your agent' }) const serverTimingHeader = useResponseHeader('Server-Timing') const homeDataStartedAt = performance.now() diff --git a/app/utils/og-style.ts b/app/utils/og-style.ts new file mode 100644 index 00000000..3361e900 --- /dev/null +++ b/app/utils/og-style.ts @@ -0,0 +1,84 @@ +// Type and colour for the content column of every OG card. A card is the +// homepage hero as one still frame, so its title takes the hero H1's face: +// Plus Jakarta Sans 600, tracked tight. DESIGN.md "On OG cards" has the frame. + +/** Width of the centred content column. `OgLayout` keeps the texture out of it. */ +export const OG_COLUMN_WIDTH = 840 + +export const OG_INK = 'oklch(0.97 0.005 60)' +export const OG_MUTED = 'oklch(0.66 0.01 60)' +export const OG_DIMMED = 'oklch(0.55 0.01 60)' +export const OG_RULE = 'oklch(0.36 0.012 60)' +export const OG_SURFACE = 'oklch(0.22 0.012 60)' + +export type OgFace = 'title' | 'body' + +// Average advance of one character, in em, measured from renders. A name such +// as `frontend-design` has no narrow spaces, so it runs wider than a sentence. +const NAME_EM = 0.5 +const FACE_EM: Record = { title: 0.45, body: 0.43 } + +type OgStyle = Record + +/** + * The largest title size, in px, at which `text` fits `lines` lines of the + * column. A Skill name with no space to wrap at shrinks to fit instead of + * running off the card. + */ +export function ogTitleSize(text: string, options: { max: number, min: number, lines?: number }): number { + const { max, min, lines = 1 } = options + const perLine = Math.max(1, Math.ceil(text.length / lines)) + return Math.max(min, Math.min(max, Math.floor(OG_COLUMN_WIDTH / (perLine * NAME_EM)))) +} + +/** + * Splits text that needs two lines at the space that makes them closest in + * length, so centred text never leaves one word alone. The renderer's own + * `text-wrap: balance` shifts balanced lines off centre, so the card breaks + * them itself. A line break in the text wins, for a title that should break + * where its page breaks it. Text that fits one line, or needs more than + * `lines`, comes back whole to wrap and clamp on its own. + */ +export function ogEvenLines(text: string, options: { fontSize: number, face: OgFace, lines: number }): string[] { + const { fontSize, face, lines } = options + if (text.includes('\n')) + return text.split('\n').slice(0, lines) + const words = text.split(' ') + const needed = Math.ceil((text.length * FACE_EM[face] * fontSize) / OG_COLUMN_WIDTH) + if (lines < 2 || needed !== 2 || words.length < 2) + return [text] + let split = 1 + let longest = Number.POSITIVE_INFINITY + for (let k = 1; k < words.length; k++) { + const length = Math.max(words.slice(0, k).join(' ').length, words.slice(k).join(' ').length) + if (length < longest) { + split = k + longest = length + } + } + return [words.slice(0, split).join(' '), words.slice(split).join(' ')] +} + +export function ogTitleStyle(fontSize: number, lines: number): OgStyle { + return { + maxWidth: `${OG_COLUMN_WIDTH}px`, + fontSize: `${fontSize}px`, + fontWeight: 600, + letterSpacing: '-0.045em', + lineHeight: 1.04, + color: OG_INK, + lineClamp: lines, + textOverflow: 'ellipsis', + } +} + +export function ogLineStyle(fontSize: number, lines: number, color: string = OG_MUTED): OgStyle { + return { + maxWidth: `${OG_COLUMN_WIDTH}px`, + fontSize: `${fontSize}px`, + lineHeight: 1.35, + color, + lineClamp: lines, + textOverflow: 'ellipsis', + } +} diff --git a/app/utils/og-texture.ts b/app/utils/og-texture.ts index cf94ce5a..0c602983 100644 --- a/app/utils/og-texture.ts +++ b/app/utils/og-texture.ts @@ -1,7 +1,7 @@ -// Static brand textures for OG cards. The site draws the live versions on a +// The static Braille names texture for OG cards. The site draws the live one on a // canvas; an OG card is one still frame, drawn server side as SVG dots, // because the OG renderer has only Plus Jakarta Sans and IBM Plex Mono and -// neither has braille glyphs. Every texture is stone: on a card, the lockup's +// neither has braille glyphs. The texture is stone: on a card, the lockup's // trailing dot is the one rose element. DESIGN.md "Texture system" has the jobs. export interface TextureDot { @@ -25,14 +25,6 @@ function hash(x: number, y: number, s: number): number { return ((n ^ (n >>> 16)) >>> 0) / 4294967295 } -/** A stable seed for a string, such as a Skill's owner, repository and name. */ -export function textureSeed(text: string): number { - let h = 2166136261 - for (let i = 0; i < text.length; i++) - h = Math.imul(h ^ text.charCodeAt(i), 16777619) - return (h >>> 0) % 1_000_000_007 -} - /** Whether a box touches any of the rects. */ function overlaps(rects: readonly TextureRect[], box: TextureRect): boolean { return rects.some(r => box.x <= r.x + r.width && box.x + box.width >= r.x && box.y <= r.y + r.height && box.y + box.height >= r.y) @@ -141,124 +133,6 @@ export function brailleNamesField(input: { return dots } -/** - * File minimap: columns of SKILL.md files drawn as rows of dots. Each file has - * frontmatter, a title, an intro, then sections of prose, lists or code. One - * dot is one character cell. The same seed always draws the same files. - */ -export function fileMinimap(input: { - region: TextureRect - seed: number - /** Distance in px over which the left edge fades in. */ - fade?: number - /** Cell pitch in px. Rows sit at twice the pitch. */ - pitch?: number - /** Start each column at the top of a file, instead of partway down the stream. */ - fromStart?: boolean -}): TextureDot[] { - const { region, seed, fade = 0, pitch = 5.6, fromStart = false } = input - const rowPitch = pitch * 2 - const lineCells = 20 - const columnCells = 25 - const levels = [0.12, 0.2, 0.3, 0.42, 0.56] - const rowCount = Math.ceil(region.height / rowPitch) - - const dots: TextureDot[] = [] - for (let c = 0; c * columnCells * pitch < region.width; c++) { - const lines: Array = [] - const offset = fromStart ? 0 : Math.floor(hash(seed, c, 41) * 30) - for (let f = 0; lines.length < offset + rowCount; f++) - skillFileLines(Math.floor(hash(seed + c, f, 43) * 1e9), lineCells, lines) - const x0 = region.x + c * columnCells * pitch - for (let r = 0; r < rowCount; r++) { - const runs = lines[offset + r] - if (!runs) - continue - const y = region.y + (r + 0.5) * rowPitch - for (let i = 0; i < runs.length; i += 3) { - for (let k = 0; k < runs[i + 1]!; k++) { - const x = x0 + (runs[i]! + k + 0.5) * pitch - if (x > region.x + region.width) - continue - const edge = fade > 0 ? Math.min(1, (x - region.x) / fade) : 1 - if (edge > 0) - dots.push({ x, y, alpha: levels[runs[i + 2]!]! * edge * edge }) - } - } - } - } - return dots -} - -/** Appends one SKILL.md's lines. A line is a flat list of runs: start cell, length, level 0 to 4. A blank line is null. */ -function skillFileLines(seed: number, width: number, out: Array): void { - let n = 0 - const rnd = (): number => hash(seed, n++, 23) - const ri = (k: number): number => Math.floor(rnd() * k) - const words = (from: number, to: number, level: number, runs: number[] = [], minLen = 2, spread = 6): number[] => { - const wordSeed = ri(1e9) - let x = from - for (let i = 0; i < 40; i++) { - const len = minLen + Math.floor(hash(wordSeed, i, 11) * spread) - if (x + len > to) - break - runs.push(x, len, level) - x += len + 1 - } - return runs - } - const line = (runs: number[]): void => { - out.push(runs) - } - const gap = (): void => { - out.push(null) - } - const para = (count: number): void => { - for (let j = 0; j < count; j++) - line(words(0, j === count - 1 ? 6 + ri(11) : width - ri(3), 1)) - } - // Frontmatter fences, name, description. - line([0, 3, 0]) - line(words(6, 9 + ri(7), 1, [0, 4, 2])) - line(words(9, width, 1, [0, 7, 2])) - if (rnd() < 0.6) - line(words(2, width - ri(5), 1)) - line([0, 3, 0]) - gap() - // Title, intro. - line(words(2, 8 + ri(10), 4, [0, 1, 4])) - gap() - para(2 + ri(2)) - const sections = 2 + ri(3) - for (let i = 0; i < sections; i++) { - gap() - line(words(3, 8 + ri(9), 3, [0, 2, 3])) - gap() - const kind = rnd() - if (kind < 0.36) { - para(2 + ri(3)) - } - else if (kind < 0.72) { - const items = 2 + ri(3) - for (let j = 0; j < items; j++) { - line(words(2, width - ri(7), 1, [0, 1, 3])) - if (rnd() < 0.3) - line(words(2, 7 + ri(10), 1)) - } - } - else { - line([0, 3, 0]) - const count = 2 + ri(4) - for (let j = 0; j < count; j++) - line(words(ri(3) * 2, width - ri(6), 0, [], 1, 5)) - line([0, 3, 0]) - } - } - gap() - gap() - gap() -} - /** * Dots as one SVG image. Each alpha step is one path of zero-length round-capped * segments, which keeps a field of thousands of dots to a few kilobytes. diff --git a/layers/marketing/app/pages/make-skill.vue b/layers/marketing/app/pages/make-skill.vue index 9632d4c2..9632a6c3 100644 --- a/layers/marketing/app/pages/make-skill.vue +++ b/layers/marketing/app/pages/make-skill.vue @@ -64,7 +64,7 @@ useSeoMeta({ description: 'Choose what your Skill is for. Get a guide to writing it and shipping it from your repository.', robots: 'noindex,follow', }) -defineOgImage('MakeSkill.takumi', { +defineOgImage('Page.takumi', { title: 'Make a skill', description: 'Write a Skill for your package or project, and ship it from your repository.', }, { alt: 'Make a skill on skilld' }) diff --git a/test/unit/og-style.test.ts b/test/unit/og-style.test.ts new file mode 100644 index 00000000..a2023131 --- /dev/null +++ b/test/unit/og-style.test.ts @@ -0,0 +1,33 @@ +// @vitest-environment node +import { describe, expect, it } from 'vitest' +import { ogEvenLines } from '../../app/utils/og-style' + +describe('ogEvenLines', () => { + it('breaks a two line title where the lines come out closest in length', () => { + expect(ogEvenLines('Design Engineering Essentials', { fontSize: 80, face: 'title', lines: 2 })) + .toEqual(['Design Engineering', 'Essentials']) + expect(ogEvenLines('Try any skill before you install it. Your agent can search for its own.', { fontSize: 30, face: 'body', lines: 2 })) + .toEqual(['Try any skill before you install it.', 'Your agent can search for its own.']) + }) + + it('breaks where the text has a line break', () => { + expect(ogEvenLines('Agent skills for you\nand your agent', { fontSize: 84, face: 'title', lines: 2 })) + .toEqual(['Agent skills for you', 'and your agent']) + }) + + it('leaves text whole when it fits one line', () => { + expect(ogEvenLines('The skilld CLI', { fontSize: 84, face: 'title', lines: 2 })).toEqual(['The skilld CLI']) + }) + + it('leaves text whole when it needs more lines than the card allows', () => { + const description = 'The skilld CLI searches, runs, and installs agent skills for Claude Code, Codex, Cursor, and 16 more Agents. One native binary, pinned commits, no telemetry.' + + expect(ogEvenLines(description, { fontSize: 30, face: 'body', lines: 2 })).toEqual([description]) + }) + + it('leaves a single word whole, since it has no space to break at', () => { + const name = 'nuxt-4-minors-migration-guide-for-modules-and-layers' + + expect(ogEvenLines(name, { fontSize: 84, face: 'title', lines: 2 })).toEqual([name]) + }) +}) diff --git a/test/unit/og-texture.test.ts b/test/unit/og-texture.test.ts index 357409e9..88275394 100644 --- a/test/unit/og-texture.test.ts +++ b/test/unit/og-texture.test.ts @@ -1,6 +1,6 @@ // @vitest-environment node import { describe, expect, it } from 'vitest' -import { brailleNamesField, fileMinimap, textureSeed } from '../../app/utils/og-texture' +import { brailleNamesField } from '../../app/utils/og-texture' function inRect(rect: { x: number, y: number, width: number, height: number }) { return (dot: { x: number, y: number }) => dot.x >= rect.x && dot.x <= rect.x + rect.width && dot.y >= rect.y && dot.y <= rect.y + rect.height @@ -25,23 +25,3 @@ describe('brailleNamesField', () => { expect(near).toBeGreaterThan(far * 2) }) }) - -describe('fileMinimap', () => { - const region = { x: 780, y: 0, width: 420, height: 600 } - - it('draws the same files for the same Skill and different files for another', () => { - const tdd = fileMinimap({ region, seed: textureSeed('mattpocock/skills/tdd') }) - - expect(fileMinimap({ region, seed: textureSeed('mattpocock/skills/tdd') })).toEqual(tdd) - expect(fileMinimap({ region, seed: textureSeed('mattpocock/skills/grill-me') })).not.toEqual(tdd) - }) - - it('stays inside its region and fades in from the left edge', () => { - const dots = fileMinimap({ region, seed: textureSeed('antfu/skills/vite'), fade: 300 }) - const edge = dots.filter(dot => dot.x < region.x + 30) - const inner = dots.filter(dot => dot.x > region.x + 300) - - expect(dots.every(inRect(region))).toBe(true) - expect(Math.max(...edge.map(dot => dot.alpha))).toBeLessThan(Math.min(...inner.map(dot => dot.alpha))) - }) -}) From 4d9dce0cc348290ff6da029c35768bffeff36ad1 Mon Sep 17 00:00:00 2001 From: Harlan Wilton Date: Tue, 6 Oct 2026 14:17:38 +1100 Subject: [PATCH 2/2] test(a11y): skip OgLines, a server-only OG component The component coverage check failed because OgLines renders only inside OG images, like OgBrand and OgLayout, so no axe test mounts it. --- test/a11y.spec.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/test/a11y.spec.ts b/test/a11y.spec.ts index 8ba32428..1f3a899d 100644 --- a/test/a11y.spec.ts +++ b/test/a11y.spec.ts @@ -636,6 +636,7 @@ describe('accessibility: component coverage', () => { 'TextureFileMinimap.client', // Decorative client-only canvas, hidden from screen readers 'OgBrand', // OG image component, rendered server-side only 'OgLayout', // OG image layout component, rendered server-side only + 'OgLines', // OG image text component, rendered server-side only 'SkillCard', // Tested at page level 'SkillReceiptsBadge', // Tested at page level 'StatsBars', // Decorative chart, tested at page level