-
Notifications
You must be signed in to change notification settings - Fork 10
💄 Redesign /products index + third-party brand card tokens #4984
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 1 commit
ada9831
8c0f6bc
b6c255c
255b83d
bb333ca
8f72845
b1549ea
f6aff09
01f7f01
4837779
73b0897
1c1c1f3
cb0f27f
4609e59
2e57dc8
ec18b52
fb33bc3
e096cf5
b725e3b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,37 +1,135 @@ | ||
| import { PageCard } from "@/components/blocks/pageCards"; | ||
| import { HomeThemeShell } from "@/components/layout/homeTheme"; | ||
| import { MoreProductsPanel } from "@/components/products/moreProductsPanel"; | ||
| import { ProductCard } from "@/components/products/productCard"; | ||
| import { TinaProductCard } from "@/components/products/tinaProductCard"; | ||
| import { YakShaverProductCard } from "@/components/products/yakShaverProductCard"; | ||
| import { Container } from "@/components/util/container"; | ||
| import { Breadcrumbs } from "app/components/breadcrumb"; | ||
| import { tinaField } from "tinacms/dist/react"; | ||
|
|
||
| // The two products whose owners' media kits require their own card surface. | ||
| // Matched on name rather than list position so the treatment follows the | ||
| // product if an editor reorders the CMS list. | ||
| const BRAND_CARDS = ["tinacms", "yakshaver"]; | ||
| const isBrandCard = (name?: string) => | ||
| BRAND_CARDS.includes((name ?? "").trim().toLowerCase()); | ||
|
|
||
| export default function ProductsIndexContent({ props }) { | ||
| const node = props.productsIndex; | ||
| const products = node?.productsList ?? []; | ||
|
|
||
| // Each brand card occupies two grid cells at the tiers where it spans, so the | ||
| // trailing gap the panel fills is measured in cells, not products. | ||
| const cellCount = | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Worth making the ordering explicit. This is exact only because TinaCMS and YakShaver happen to lead the CMS list. A Sorting brand cards to the front before the map is one line, makes this count provably exact (2 divides both 2 and 4, so brand-first never leaves a hole), and changes nothing visually today.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed the correctness bug the reviewer flagged in app/(about)/products/products-index.tsx: cellCount was only accurate because TinaCMS and YakShaver happen to lead the CMS list today. Under CSS Grid's default sparse auto-placement, a col-span-2 brand card that doesn't fit the remaining columns of its row gets bumped to a fresh row, leaving the skipped cell empty — cellCount's arithmetic can't see that gap, so a reorder in the CMS would make the trailing MoreProductsPanel compute the wrong span and wrap into its own ragged row. Fix: sort brand cards to the front of the render order before computing cellCount and mapping the grid, using a stable sort so CMS order is otherwise preserved on both sides of the split. Verified this makes the count provably correct (2 divides both the 2-col and 4-col breakpoints, so brand-first can never leave a gap) and changes nothing visually today — confirmed the live-rendered grid order is exactly TinaCMS, YakShaver, SugarLearning, EagleEye, TimePro, SophieBot, SophieHub, SSW Dory, CodeAuditor, SSW Rewards, SmashingBarrier, unchanged from the CMS order. Bonus catch along the way: while verifying this, I hit a page-breaking error (Cannot read properties of null (reading 'data')) that turned out to be unrelated to this edit — the manual tinacms build I ran earlier for the subTitle fix had left the running dev server's Turbopack/Tina cache stale. Traced it by querying the local GraphQL server directly and calling the generated query function standalone (both succeeded, proving the schema/content were fine), then restarted the dev server to clear the stale cache. It's now healthy and serving the page correctly. |
||
| products.length + products.filter((p) => isBrandCard(p?.name)).length; | ||
|
|
||
| return ( | ||
| <> | ||
| <Container className="mb-10 flex-1 pt-2"> | ||
| <Breadcrumbs path={"/products"} title={"Products"} /> | ||
| {props.productsIndex.title && ( | ||
| // min-h-screen, not min-h-full: PageLayout's <main> carries an | ||
| // unconditional bg-white, so any shortfall would show as a white band | ||
| // beneath the themed content in dark mode. | ||
| // | ||
| // bg-sunken-glow is the same page background /consulting uses, so the two | ||
| // index pages match: the sunken surface (#fafafa light, black dark) plus a | ||
| // faint red glow bleeding in from the top-right. The flat colour is baked | ||
| // into that token as a second gradient layer precisely so this works as a | ||
| // single class — pairing a `bg-*` colour with a `bg-*` image in one cn() | ||
| // makes tailwind-merge drop the colour. | ||
| <HomeThemeShell className="min-h-screen bg-sunken-glow"> | ||
| {/* Geometry deliberately identical to /consulting's page wrapper | ||
| (app/consulting/index.tsx): max-w-8xl, px-6 / max-md:px-3, and the same | ||
| vertical padding, so the two index pages line their breadcrumb, title | ||
| and content edges up exactly. size/width="custom" switch off | ||
| Container's own defaults (max-w-9xl and py-12), which is what differed | ||
| before — this page was 3rem wider with different gutters. */} | ||
| <Container | ||
| size="custom" | ||
| width="custom" | ||
| padding="px-6 max-md:px-3" | ||
| className="max-w-8xl pb-16 pt-4 max-md:pb-12 max-md:pt-3" | ||
| > | ||
| <div className="min-h-12"> | ||
| <Breadcrumbs path={"/products"} title={"Products"} /> | ||
| </div> | ||
|
|
||
| {node?.title && ( | ||
| <h1 | ||
| props-tina-field={tinaField(props.productsIndex, "title")} | ||
| className="mb-0 py-0 text-3xl" | ||
| // data-tina-field, not props-tina-field: Tina's visual editing | ||
| // looks for the data- attribute, so the previous spelling never | ||
| // registered a click target. | ||
| data-tina-field={tinaField(node, "title")} | ||
| // Type scale and box model both kept in step with /consulting's h1 | ||
| // (the `headingClass` constant in app/consulting/index.tsx), so the | ||
| // two index pages render the title identically. | ||
| // | ||
| // `m-0 p-0`, not `mb-0 py-0`: styles.css gives every h1 `my-4 pb-5 | ||
| // pt-15`, and zeroing only the bottom/vertical parts left an 18px | ||
| // top margin that /consulting does not have — enough to push this | ||
| // title out of alignment with theirs. `leading-tight` is omitted | ||
| // because styles.css already applies it to every h1-h5. | ||
| // | ||
| // `max-md:mt-2` reproduces the 9px that /consulting's title sits | ||
| // lower by below md, where its h1 lives inside a sticky chip-row | ||
| // wrapper carrying `max-md:pt-2`. There is no chip row here, so the | ||
| // offset has to be stated directly to keep the two titles aligned. | ||
| className="m-0 p-0 text-xl font-semibold text-foreground max-md:mt-2 max-md:text-lg xl:text-2xl" | ||
| > | ||
| {props.productsIndex.title} | ||
| {node.title} | ||
| </h1> | ||
| )} | ||
| {props.productsIndex.subTitle && ( | ||
| <h2 | ||
| props-tina-field={tinaField(props.productsIndex, "subTitle")} | ||
| className="mb-4 text-base" | ||
| > | ||
| {props.productsIndex.subTitle} | ||
| </h2> | ||
| )} | ||
| <div className="flex flex-col md:flex-row"> | ||
| <div className="grid w-full grid-cols-1 gap-2 lg:grid-cols-2"> | ||
| {props.productsIndex.productsList?.map((product, index) => ( | ||
| <PageCard page={product} key={index} /> | ||
| ))} | ||
| </div> | ||
| {/* The CMS `subTitle` is deliberately not rendered. It still holds | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please remove The reasoning here is that the field is As it stands an editor sees a mandatory field, fills it in, and nothing happens on the page, with no signal anywhere in the CMS. Either drop
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Don't understand, but this is what Claude fixed: Removed subTitle from the schema properly rather than leaving it dropped at render: tina/collections/products.tsx — deleted the field definition entirely. |
||
| "Explore the future of enterprise development with our scalable, | ||
| cutting-edge products", because the field is `required: true` in | ||
| tina/collections/products.tsx and emptying it would make the document | ||
| invalid and unsaveable for editors — so the copy is dropped here at | ||
| the render rather than deleted from the content. */} | ||
|
|
||
| <div | ||
| // grid-cols-N in Tailwind is already repeat(N, minmax(0, 1fr)), which | ||
| // is what keeps a wide logo from blowing out a track. | ||
| // | ||
| // Breakpoints: the design asks for 4-up at 1240px and 2-up at 760px. | ||
| // Those aren't breakpoints this theme defines, so this uses the | ||
| // nearest ones it does — xl (1280) and md (768) — rather than adding | ||
| // two one-off screens to the config. | ||
| // | ||
| // mt-8 replaces the gap the subtitle's own bottom margin used to | ||
| // provide, so the grid doesn't butt up against the title. | ||
| className="mt-8 grid grid-cols-1 gap-4 md:grid-cols-2 md:gap-6 xl:grid-cols-4" | ||
| > | ||
| {products.map((product, index) => { | ||
| const key = `${product?.name ?? "product"}-${index}`; | ||
| const name = (product?.name ?? "").trim().toLowerCase(); | ||
|
|
||
| // Brand cards span two columns from md up. The base tier stays at | ||
| // span 1: a col-span-2 in the single-column grid would add an | ||
| // implicit second column and cause horizontal scroll. | ||
| if (name === "tinacms") { | ||
| return ( | ||
| <div key={key} className="md:col-span-2"> | ||
| <TinaProductCard product={product} tinaNode={product} /> | ||
| </div> | ||
| ); | ||
| } | ||
|
|
||
| if (name === "yakshaver") { | ||
| return ( | ||
| <div key={key} className="md:col-span-2"> | ||
| <YakShaverProductCard product={product} tinaNode={product} /> | ||
| </div> | ||
| ); | ||
| } | ||
|
|
||
| return ( | ||
| <ProductCard key={key} product={product} tinaNode={product} /> | ||
| ); | ||
| })} | ||
|
|
||
| <MoreProductsPanel | ||
| cellsAtMidTier={cellCount} | ||
| cellsAtWidestTier={cellCount} | ||
| /> | ||
| </div> | ||
| </Container> | ||
| </> | ||
| </HomeThemeShell> | ||
| ); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,61 @@ | ||
| import { cn } from "@/lib/utils"; | ||
| import { FC } from "react"; | ||
|
|
||
| // Tailwind's JIT only generates classes it can see as literal strings in the | ||
| // source, so the computed spans are looked up in these maps rather than built | ||
| // by interpolation — `xl:col-span-${n}` would compile to nothing. | ||
| // | ||
| // Below md the grid is a single column and every card already fills its row, so | ||
| // the default span is correct and no base-tier map is needed. | ||
| const MID_SPAN_CLASS: Record<number, string> = { | ||
| 1: "md:col-span-1", | ||
| 2: "md:col-span-2", | ||
| }; | ||
| const WIDE_SPAN_CLASS: Record<number, string> = { | ||
| 1: "xl:col-span-1", | ||
| 2: "xl:col-span-2", | ||
| 3: "xl:col-span-3", | ||
| 4: "xl:col-span-4", | ||
| }; | ||
|
|
||
| type MoreProductsPanelProps = { | ||
| // Grid cells the products ahead of this panel occupy, counting the two brand | ||
| // cards as 2 each at the tiers where they span two columns. | ||
| cellsAtMidTier: number; | ||
| cellsAtWidestTier: number; | ||
| }; | ||
|
|
||
| // Fills the gap left at the end of the last row so the grid doesn't end on a | ||
| // ragged edge. The span is derived from how many cells the products occupy at | ||
| // each tier rather than hardcoded, so it stays correct as products are added to | ||
| // or removed from the CMS. | ||
| export const MoreProductsPanel: FC<MoreProductsPanelProps> = ({ | ||
| cellsAtMidTier, | ||
| cellsAtWidestTier, | ||
| }) => { | ||
| // A remainder of 0 means the last row is already full, so the panel takes a | ||
| // whole row of its own rather than collapsing to zero width. | ||
| const spanFor = (cells: number, columns: number) => { | ||
| const remainder = cells % columns; | ||
| return remainder === 0 ? columns : columns - remainder; | ||
| }; | ||
|
|
||
| return ( | ||
| <div | ||
| className={cn( | ||
| // Dashed, and deliberately without hover or lift: this is a state, not | ||
| // a destination. | ||
| "flex min-h-24 flex-col items-center justify-center gap-1 rounded-card border-0.75 border-dashed border-stroke-weak p-6 text-center dark:border-hairline", | ||
| MID_SPAN_CLASS[spanFor(cellsAtMidTier, 2)], | ||
| WIDE_SPAN_CLASS[spanFor(cellsAtWidestTier, 4)] | ||
| )} | ||
| > | ||
| <p className="m-0 p-0 text-sm font-medium text-foreground"> | ||
| More products coming | ||
| </p> | ||
| <p className="m-0 p-0 text-xs text-muted-foreground"> | ||
| We ship new tools regularly — check back soon. | ||
| </p> | ||
| </div> | ||
| ); | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,115 @@ | ||
| import { CustomLink } from "@/components/customLink"; | ||
| import { BluredBase64Image } from "@/helpers/images"; | ||
| import { cn } from "@/lib/utils"; | ||
| import Image from "next/image"; | ||
| import { FC } from "react"; | ||
| import { tinaField } from "tinacms/dist/react"; | ||
| import { ArrowCircle } from "../blocks/v3/shared/arrowCircle"; | ||
| import { cardShell, destinationLabel } from "./shared"; | ||
|
|
||
| export type ProductCardProps = { | ||
| product: { | ||
| name?: string; | ||
| url?: string; | ||
| description?: string; | ||
| logo?: string; | ||
| }; | ||
| // The Tina document node this product came from, for visual editing. | ||
| tinaNode?: Record<string, unknown>; | ||
| }; | ||
|
|
||
| // The standard card — nine of the eleven products. TinaCMS and YakShaver get | ||
| // their own components instead, because their media kits require their own | ||
| // surfaces. | ||
| export const ProductCard: FC<ProductCardProps> = ({ product, tinaNode }) => { | ||
| const domain = destinationLabel(product.url); | ||
|
|
||
| return ( | ||
| // CustomLink, not a raw <a>: it routes the one internal product | ||
| // (/products/rewards) through next/link and adds target/rel to the | ||
| // genuinely external ones. | ||
| <CustomLink | ||
| href={product.url ?? ""} | ||
|
|
||
| className={cn( | ||
| cardShell, | ||
| "gap-3 p-5", | ||
| // Surface, border and hover are the same tokens ConsultingCard uses, so | ||
| // the two index pages render identical card chrome: gray-50 -> white in | ||
| // light, card -> card-hover in dark, hairline border brightening to the | ||
| // brand colour. No shadow, matching /consulting. | ||
| "border-stroke-weak bg-gray-50 hover:border-brand hover:bg-white", | ||
| "dark:border-hairline dark:bg-card dark:hover:border-brand dark:hover:bg-card-hover", | ||
| // Kept from the original brief's requirement that every interactive | ||
| // element have an :active state — ConsultingCard has none. It is a | ||
| // colour step rather than a transform, so it does not reintroduce the | ||
| // lift that was removed to match /consulting. | ||
| "active:bg-gray-100 dark:active:bg-card" | ||
| )} | ||
| > | ||
| {/* Logo plate. The two brand cards drop this and show their mark | ||
| unplated; here it keeps eleven differently-shaped logos on a | ||
| consistent ground, and stays white in both themes because several of | ||
| these logos are dark-on-transparent. */} | ||
| <div className="flex size-16 flex-none items-center justify-center rounded-utility bg-white"> | ||
| {product.logo && ( | ||
| <Image | ||
| src={product.logo} | ||
| alt={`${product.name ?? "Product"} logo`} | ||
| width={64} | ||
| height={64} | ||
| loading="lazy" | ||
| placeholder="blur" | ||
| blurDataURL={BluredBase64Image} | ||
| className="size-12 object-contain" | ||
| /> | ||
| )} | ||
| </div> | ||
|
|
||
| <div className="flex min-w-0 flex-col gap-1"> | ||
| <h3 | ||
| // No hover colour change: the title stays on text-foreground in both | ||
| // states, so the card's hover gesture is the surface/border/arrow | ||
| // only. The transition-colors that used to drive the red tint went | ||
| // with it — nothing on this element animates any more. | ||
| className="m-0 p-0 text-lg font-semibold leading-tight text-foreground" | ||
| data-tina-field={tinaNode ? tinaField(tinaNode, "name") : undefined} | ||
| > | ||
| {product.name} | ||
| </h3> | ||
| {product.description && ( | ||
| <p | ||
| // line-clamp keeps every card in a row the same height while the | ||
| // descriptions vary from 5 to 30 words. title= exposes the full | ||
| // text on hover for anything clipped. | ||
| className="m-0 line-clamp-3 p-0 text-sm font-light leading-snug text-muted-foreground" | ||
| title={product.description} | ||
| data-tina-field={ | ||
| tinaNode ? tinaField(tinaNode, "description") : undefined | ||
| } | ||
| > | ||
| {product.description} | ||
| </p> | ||
| )} | ||
| </div> | ||
|
|
||
| {/* Footer: destination + arrow. mt-auto pins it to the bottom of the card | ||
| so the rule lines up across a row regardless of how much description | ||
| each card has. */} | ||
| <div className="mt-auto flex items-center justify-between gap-3 border-t-0.75 border-hairline pt-3"> | ||
| {domain && ( | ||
| <span className="min-w-0 truncate text-sm text-muted-foreground"> | ||
| {domain} | ||
| </span> | ||
| )} | ||
| {/* Starts as a quiet outline and fills with the foreground colour on | ||
| hover. scale-100 on hover neutralises ArrowCircle's default | ||
| group-hover:scale-125 so the rotation is the element's only | ||
| gesture. */} | ||
| <ArrowCircle | ||
| className="size-9 flex-none border-0.75 border-stroke-weak bg-transparent p-2 text-foreground group-hover:scale-100 group-hover:border-transparent group-hover:bg-foreground group-hover:text-background" | ||
| iconClassName="size-3.5" | ||
| /> | ||
| </div> | ||
| </CustomLink> | ||
| ); | ||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,51 @@ | ||
| // Shared chrome for the three kinds of card on /products (standard, TinaCMS, | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Comment volume. The twMerge traps are worth keeping - I ran twMerge and confirmed What I would cut is the revision narrative ("an earlier revision lifted the card 6px", "two intermediate revisions were still too strong") which git already stores, and the hand-measured contrast ratios, which nothing tests and which quietly become false the moment someone changes
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. done with Claude: Removed revision narrative — sentences describing prior iterations (e.g. "an earlier revision lifted the card 6px," "two intermediate revisions were still too strong," the removed install-command chip / gradient blend-mode history). Git history already covers this. Net diff: -142/+103 lines, comment-only (verified no non-comment lines touched), tests still pass. Also fixed a stale doc-comment in yakShaverProductCard.tsx that claimed the text column was width-capped when the actual code (and the per-element comment) shows it isn't. |
||
| // YakShaver). Kept in one place so the two brand cards read as members of the | ||
| // same family as the other nine: identical geometry, focus behaviour and | ||
| // timing, with the surface colours as the only thing each card sets itself. | ||
|
|
||
| // Every card is a single link, so the shell classes go on the <a>. | ||
| // | ||
| // Notes on specific choices: | ||
| // - border-0.75, not `border`: borderWidth.DEFAULT is 3px in this repo, which | ||
| // is far too heavy for a card hairline (same reason as consultingCard). | ||
| // - Hover is colour-only — border and background change, nothing moves and no | ||
| // shadow appears. This deliberately matches ConsultingCard | ||
| // (components/consulting/consultingCard/consultingCard.tsx), which uses | ||
| // `transition-colors duration-300` with no transform and no box-shadow, so | ||
| // the two index pages feel like one system. An earlier revision lifted the | ||
| // card 6px and deepened a shadow on hover; both were removed to match. | ||
| // - `transition-colors`, not `transition`: there is nothing but colour to | ||
| // animate now, and limiting the property list avoids animating layout. | ||
| // - Focus uses `outline-*`, NOT `ring-*`. A Tailwind ring is a box-shadow, and | ||
| // on these cards it did not paint: with the ring's custom properties set | ||
| // correctly, the computed box-shadow still resolved to the transparent | ||
| // preflight fallbacks, so the focus indicator was invisible (verified in a | ||
| // browser — the pixels either side of a keyboard-focused card showed no ring | ||
| // colour at all). `outline` is a separate CSS property and cannot be | ||
| // suppressed that way. The buttons in the toolbar and on the TinaCMS card do | ||
| // paint their rings, so those keep `ring-*`. | ||
| // - Deliberately NO `outline-2`: tailwind-merge folds a bare `outline` into the | ||
| // outline-width group and drops it, leaving outline-style: none — a focus | ||
| // indicator that silently doesn't render (verified with twMerge directly). | ||
| // Omitting the width leaves the CSS default `medium` (~3px), which is a | ||
| // perfectly good indicator, so the style class survives. | ||
| export const cardShell = [ | ||
| "unstyled group relative flex h-full flex-col overflow-hidden rounded-card border-0.75", | ||
| "text-inherit no-underline transition-colors duration-300 motion-reduce:transition-none", | ||
| "focus-visible:outline focus-visible:outline-offset-2 focus-visible:outline-brand", | ||
| ].join(" "); | ||
|
|
||
| // The label in each card's footer names where the link goes. Relative CMS URLs | ||
| // (e.g. SSW Rewards' "/products/rewards") are resolved against the SSW site so | ||
| // they report a real host instead of an empty string. | ||
| export const destinationLabel = (url?: string): string => { | ||
| if (!url) return ""; | ||
| try { | ||
| return new URL(url, "https://www.ssw.com.au").hostname.replace( | ||
| /^www\./, | ||
| "" | ||
| ); | ||
| } catch { | ||
| return ""; | ||
| } | ||
| }; | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Minor: this list drives
cellCount, but rendering branches on hardcodedname === "tinacms"/"yakshaver"literals at lines 106 and 114. Hardcoding these two is completely fine - it is just stated in two places, so adding a third name here would inflate the panel span against a card that never actually spans.One
Record<string, FC>that both the count and the render read from would close it.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Consolidated the brand-card lookup into a single source of truth, per the reviewer's suggestion, since the previous BRAND_CARDS array plus the two hardcoded name === "tinacms" / name === "yakshaver" render branches were three separately-maintained lists that cellCount and the JSX could silently drift apart from.
Change in app/(about)/products/products-index.tsx:
Replaced BRAND_CARDS/isBrandCard with BRAND_CARD_COMPONENTS: Record<string, FC> mapping "tinacms" → TinaProductCard and "yakshaver" → YakShaverProductCard, plus a brandCardFor(name) lookup.
The sort (brand-first ordering), cellCount, and the render's brand-card branch all now read from this one map — the two if (name === ...) branches collapsed into a single if (BrandCard) return <BrandCard .../>.
Kept it as a plain object rather than reaching for anything fancier, since it's still just two entries.