index.tsx4.9 KB raw2.0 KB gz
// cnfast — a product-grid TILE as a long Tailwind class list. The elements whose class
// list is fixed use a plain className string (as a user would write it); cn() is only
// called where there's something to resolve — the two always-on buttons and the
// conditional discount badge merge composed token bundles + conditional fragments on
// EVERY render (next-yak did all of this at build time). Default-exports a
// single-instance render(i) (§6); the harness loops it. Class strings + the
// per-item data formula are verbatim from the original cnfast product-grid lane.
import React, { type FunctionComponent } from "react";
import { cn } from "cnfast";

// Shared min-target-size + focus-ring token bundles (WCAG 2.5.5 + keyboard nav).
const TAP_TARGET =
  "before:content-[''] before:absolute before:inset-1/2 before:[translate:-50%_-50%] before:w-full before:h-full " +
  "[@media(hover:none)_and_(pointer:coarse)]:before:min-w-[44px] [@media(hover:none)_and_(pointer:coarse)]:before:min-h-[44px] " +
  "[@media(hover:hover)_and_(pointer:fine)]:before:min-w-[24px] [@media(hover:hover)_and_(pointer:fine)]:before:min-h-[24px]";
const FOCUS_RING =
  "focus-visible:outline focus-visible:outline-2 focus-visible:outline-blue-600 focus-visible:outline-offset-2";

interface Product {
  i: number;
  title: string;
  price: number;
  discount: number;
  rating: number;
  inStock: boolean;
  wishlisted: boolean;
}

const Tile: FunctionComponent<{ p: Product }> = ({ p }) => {
  const pct = (p.rating / 5) * 100 + "%";
  return (
    <li className="flex flex-col [container-type:inline-size] [container-name:tile] border border-solid border-gray-200 rounded-lg p-3 bg-white transition-shadow duration-150 [@media(hover:hover)]:hover:shadow-md motion-reduce:transition-none">
      <div className="relative aspect-square bg-gray-100 rounded-md overflow-hidden">
        <div aria-hidden="true" className="absolute inset-0 bg-gradient-to-br from-gray-100 to-gray-200" />
        {p.discount > 0 && (
          <span className={cn("absolute top-1.5 left-1.5 rounded px-1.5 py-0.5 text-xs leading-[normal] font-bold text-white", p.discount >= 30 ? "bg-red-600" : "bg-amber-500")}>
            <span className="sr-only">Reduced by </span>-{p.discount}%
          </span>
        )}
        <button
          type="button"
          aria-pressed={p.wishlisted}
          aria-label={p.wishlisted ? "Remove from wishlist" : "Add to wishlist"}
          className={cn(
            "absolute top-1.5 right-1.5 border-none rounded-full bg-white/80 cursor-pointer text-lg leading-none p-1 transition-colors duration-150 motion-reduce:transition-none",
            "[@media(hover:hover)]:hover:text-red-500",
            FOCUS_RING,
            TAP_TARGET,
            p.wishlisted ? "text-red-500" : "text-gray-400",
          )}
        >
          <span aria-hidden="true">♥</span>
        </button>
      </div>
      <h3 className="mt-2 mb-1 text-sm leading-[normal] font-medium whitespace-nowrap overflow-hidden text-ellipsis lg:text-[15px] [@container_tile_(min-width:240px)]:text-base [@container_tile_(min-width:240px)]:leading-[normal]">{p.title}</h3>
      <div role="img" aria-label={`Rated ${p.rating} out of 5`} className="h-2 rounded bg-gray-200 overflow-hidden">
        <div aria-hidden="true" className="h-full bg-amber-400 w-[var(--pct)]" style={{ "--pct": pct } as React.CSSProperties} />
      </div>
      <div className="flex items-baseline gap-1.5 mt-1.5 mb-2.5 [@container_tile_(min-width:240px)]:gap-2.5">
        {p.discount > 0 && (
          <span className="text-xs leading-[normal] text-gray-400 line-through">
            <span className="sr-only">Was </span>${(p.price * (1 + p.discount / 100)).toFixed(2)}
          </span>
        )}
        <span className="text-base leading-[normal] font-bold text-gray-900">
          {p.discount > 0 && <span className="sr-only">Now </span>}${p.price.toFixed(2)}
        </span>
      </div>
      <button
        disabled={!p.inStock}
        aria-label={p.inStock ? `Add ${p.title} to cart` : `${p.title} is sold out`}
        className={cn(
          "mt-auto relative border-none rounded-md px-3 py-2 text-sm leading-[normal] font-semibold text-white bg-blue-600 cursor-pointer transition-colors duration-150 motion-reduce:transition-none lg:py-[9px]",
          "[@media(hover:hover)]:hover:bg-blue-700",
          FOCUS_RING,
          TAP_TARGET,
          !p.inStock && "bg-gray-300 text-gray-500 cursor-not-allowed",
        )}
      >
        {p.inStock ? "Add to cart" : "Sold out"}
      </button>
    </li>
  );
};

export default (i: number) => {
  // ONE product built from i with the same per-item formula as the original ITEMS array.
  const p: Product = {
    i,
    title: "Product " + i,
    price: (i % 50) + 9.99,
    discount: i % 4 === 0 ? (i % 3 === 0 ? 40 : 20) : 0,
    rating: (i % 5) + 1,
    inStock: i % 7 !== 0,
    wishlisted: i % 6 === 0,
  };
  return <Tile p={p} />;
};