From 1ee2ccfeb0234d8ee3453cc248a828a410532945 Mon Sep 17 00:00:00 2001 From: Gab Date: Thu, 24 Sep 2026 22:04:35 -0300 Subject: [PATCH 1/4] feat(webkit): add the logo-wall marketing component --- .specs/logo-wall.md | 163 ++++++++++++++++ .../marketing/logo-wall/LogoWall.stories.js | 183 ++++++++++++++++++ packages/webkit/.size-limit.json | 5 + packages/webkit/catalog.json | 64 ++++++ packages/webkit/package.json | 1 + .../marketing/logo-wall/logo-wall.test.ts | 169 ++++++++++++++++ .../marketing/logo-wall/logo-wall.vue | 113 +++++++++++ 7 files changed, 698 insertions(+) create mode 100644 .specs/logo-wall.md create mode 100644 apps/storybook/src/stories/components/marketing/logo-wall/LogoWall.stories.js create mode 100644 packages/webkit/src/components/marketing/logo-wall/logo-wall.test.ts create mode 100644 packages/webkit/src/components/marketing/logo-wall/logo-wall.vue diff --git a/.specs/logo-wall.md b/.specs/logo-wall.md new file mode 100644 index 000000000..720730ce4 --- /dev/null +++ b/.specs/logo-wall.md @@ -0,0 +1,163 @@ +--- +name: logo-wall +category: marketing +structure: monolithic +status: implemented +spec_version: 1 +checksum: 0000a7ede0732472f868b87a74e702a8ea87dbaee838fdc6a6971eb6c7b29f34 +created: 2026-09-22 +last_updated: 2026-09-24 +--- + +# Logo Wall — Component Spec + +## Purpose + +The customer-proof band of a marketing page: a responsive grid of company marks, optionally linked, under one accessible group name. It is deliberately a static grid rather than an auto-scrolling strip — moving content needs a pause control to meet WCAG 2.2.2, and a wall a reader can scan beats one they have to wait for. + +The marks rest at 60% opacity, so the wall reads as the evidence behind the page's own words rather than as twelve things competing with them; a linked mark comes up to full strength on hover and focus. Fill the `aside` slot and the band splits from `lg` up — the wall on the start edge, one of those customers speaking on the end edge — and the wall narrows from six columns to four to sit in half the width. + +## When to use + +- To show who already uses the product, as the proof band under a hero or section. +- Wherever a set of partner, customer or certification marks is the message. +- When the marks are recognisable enough that names are unnecessary. +- To set one customer's statement beside the crowd that backs it, by filling `aside` with a `quote`. + +## When NOT to use + +- For a measured claim rather than a set of names → use `big-numbers`. +- For one customer's statement → use `quote`. +- For a single brand mark, such as the product's own → use `brand`. +- For a grid of described features rather than marks → use `feature-card`. + +## Related + +- `quote` — one named customer's statement; the wall shows many without words. +- `big-numbers` — the numeric form of the same proof band. +- `brand` — the product's own mark, not a customer's. + +## Best practices + +- Supply marks that read on the page's own surface. The component applies no colour filter — only a uniform opacity — so a mark that only works on one theme will not work on both; ship a mark that does, rather than inverting it. When the marks come from a registry that already places them per theme, render them through the `mark` slot and keep `items` as the list they are drawn from. +- Give every mark a real `alt` — the company name. The wall is a list of who uses the product, and that list should be readable without seeing it. +- Set `ariaLabel` so the group announces its purpose ("Customers using Azion") instead of reading as an unnamed list. +- Keep the marks to one visual weight. A wall mixing wordmarks and full lockups reads as inconsistent no matter how the grid is set. +- Link a mark with `href` only when there is somewhere worth going, such as a case study. +- Put one statement in `aside`, not a second grid. The band's argument is *many customers, one of them talking*; two things of equal weight leave a reader with neither. +- Give the wall eight or twelve marks when `aside` is filled — it is four columns wide there — so the last row is full rather than ragged. + +## Usage + +```vue + + + +``` + +## Props + +| Prop | Type | Default | Required | JSDoc | +|---|---|---|---|---| +| `items` | `LogoItem[]` | `[]` | false | The marks rendered in the grid, in order; each item is `{ src, alt, href? }` where `src` is the mark's URL, `alt` names the company, and `href` links the mark when there is somewhere to go. | +| `ariaLabel` | `string` | `''` | false | Accessible name for the group of marks, announced instead of an unnamed list. | + +## Events + +| _none_ | — | — | + +## Slots + +| Slot | Scope | Notes | +|---|---|---| +| `aside` | — | Content set beside the wall from `lg` up, such as one customer's `quote`; when it is empty the wall spans the full width. | +| `mark` | `{ item: LogoItem; index: number }` | One cell's mark, replacing the image built from the item — for a mark that owns its own theming (a per-theme asset swap, a silhouette filter). The slotted content carries its own alternative text. | + +## States + +- Visual states: every mark rests at 60% opacity; `hover` / `focus-visible` on a mark that carries an `href` is the only thing that lifts it to full +- `data-aside` is present when the `aside` slot is filled, and is what splits the band into two columns from `lg` up and narrows the wall from six columns to four +- A cell's mark is the `mark` slot's content when it is filled, and the image built from the item otherwise; either way it rests at the same 60% opacity and, with an `href`, lifts on `hover` / `focus-visible` +- Empty: when `items` is empty the wall renders no grid, so a page with no customers to name shows nothing rather than an empty frame — an `aside` given without items still renders, since a statement stands on its own + +## Motion & Animations + +| Trigger | Animation / Transition | Token (see `.claude/docs/DESIGN.md` § Animations) | Reduced-motion fallback | +|---|---|---|---| +| hover on a linked mark | `transition-opacity duration-150 ease-out` | inline (matches catalog) | `motion-reduce:transition-none` | + +## Tokens + +| Region | Token (DESIGN.md) | +|---|---| +| spacing (grid gap) | `var(--spacing-xl)` | +| spacing (wall ↔ aside gap) | `var(--spacing-xxl)` | +| ring | `var(--ring-color)` | +| focus ring offset | `var(--bg-canvas)` | + +## Theme gaps + +| Figma variable | Temporary primitive | Follow-up | +|---|---|---| +| _none_ | — | — | + +## Accessibility (WCAG 2.1 AA) + +- Visible focus: a linked mark carries `focus-visible:ring-2 focus-visible:ring-(--ring-color) focus-visible:ring-offset-2 focus-visible:ring-offset-(--bg-canvas)`; an unlinked mark is not focusable. +- Keyboard map: `Tab` reaches each linked mark in DOM order; unlinked marks are skipped. No arrow-key model — this is a list, not a composite widget. +- ARIA: the grid is a `