diff --git a/.agents/plans/pr-reconciliation-site-check.plan.md b/.agents/plans/pr-reconciliation-site-check.plan.md new file mode 100644 index 00000000..6e344474 --- /dev/null +++ b/.agents/plans/pr-reconciliation-site-check.plan.md @@ -0,0 +1,67 @@ +--- +name: pr-reconciliation-site-check +overview: "Reconcile the approved Stripe and typography PRs with main, verify their combined behavior, and investigate the reported site failure." +todos: + - id: verify-live-failure + content: Reproduce the reported site failure and inspect production deployment and browser evidence + status: in_progress + - id: reconcile-stripe + content: Preserve null-body and malformed-ID validation tests while merging current main into PR 161 + status: completed + - id: reconcile-typography + content: Resolve PR 171 font configuration conflicts and preserve the merged mobile theme control + status: completed + - id: verify-and-ship + content: Run local gates, review current PR checks and comments, squash-merge approved changes, and verify production + status: in_progress +isProject: false +--- + +# PR reconciliation and site verification + +## Goal + +Finish the approved PR merges and restore any reproduced site failure, with production verification. + +## Scope + +- Reconcile PRs 161 and 171; review the newly opened PR 172 under the existing open-PR request. +- Inspect public pages, browser errors, deployment state, and relevant runtime logs. +- Preserve authentication, payment, chain, and data semantics. + +## Files + +- `web/__tests__/api/stripe-routes.test.ts`: retain the null-body and malformed-UUID regressions. +- `web/app/globals.css` and `web/tailwind.config.ts`: retain Crimson Pro titles, Inter body/UI, and Inconsolata code. +- `web/components/AppNavbar.tsx`: verify the merged mobile theme control remains available. +- `web/app/skills/MarketplaceClient.tsx`: retain the loading button until the final requested batch finishes. +- `web/app/globals.css` and `web/__tests__/app/typography-utilities.test.ts`: register semantic fonts in the active Tailwind v4 theme and compile-test font variants. +- Any site-failure fix must follow a reproduced cause; record exact files here before editing. + +## Verification + +Run Node 24, focused Stripe tests, `npm run format:check`, web lint, typecheck, full web Vitest, `next build --webpack`, and `git diff --check`. Verify current PR head checks before squash merging and inspect the resulting production deployment and public browser behavior. + +## Rollout and rollback + +Update PR branches with signed merge commits, preserving history. Squash-merge after verification. If a reproduced release regression requires rollback, use a verified prior deployment or a focused revert; do not change live data or feature flags. + +## Evidence (2026-09-05) + +- Production alias points to deployment `dpl_E9bynP9hLSKYRTMoSQan4pHWNWd4`, commit `ccb432b`, state READY. +- Marketplace loaded 94 skills, pagination reached page 2, and browser error logs were empty. +- Ports 3000, 3001, and 3002 have no local listener; the user has been asked which page/action fails while independent work continues. +- PR 161 conflicts only in its Stripe test insertion; PR 171 conflicts in global CSS and Tailwind font mapping. +- PR 161 resolution passed format, lint, typecheck, all 948 tests, and the webpack production build. The build required network access for Google Fonts and used expected missing-database fallbacks in the isolated checkout. Signed commit `ea7a47c` was published. +- Local marketplace at port 3002 loaded 9 then 18 cards. Searching for `subagent` after appending correctly replaced the list with one result; the suspected search regression was ruled out. +- UI review found the end-of-list label can replace the button while the final page is still in flight, because the requested page advances before its response. Keep the button mounted and disabled until completion; no pagination/API semantics change. +- The final-batch browser check showed a disabled `Loading more` button at 9/12, followed by 12/12 and the end label, with no console errors. +- Skill detail computed styles showed the page title in Crimson Pro, prose in Inter, and code in Inconsolata, but Markdown headings incorrectly inherited Inter. `prose-headings:font-display` has no generated utility because the legacy TypeScript Tailwind config is not loaded by the v4 CSS entrypoint. Register the font tokens with `@theme inline` and verify generated CSS plus rendered styles. +- All four compiled-utility regression tests failed before the theme mapping and passed afterward. The browser confirmed the corrected skill Markdown heading in Crimson Pro, body in Inter, and code in Inconsolata in dark theme. Light-theme marketplace and narrow-layout carousel navigation (01/03 to 02/03) also passed without horizontal overflow. +- PRs 172 and 161 were squash-merged after green CI and Vercel builds; both are integrated into this branch from `origin/main` at `00972b7`. +- Final combined local gate: format, lint, typecheck, 129 test files / 953 tests, and webpack production build all passed. Build retains the existing non-blocking `ox` dynamic-dependency warning. Real wallet signing and paid checkout were not exercised. +- Production homepage, marketplace, dashboard, and sign-in page loaded successfully. Production runtime error checks returned no errors; no specific failing public URL/action has been supplied yet. + +## Blockers + +- The reported site failure is not yet reproduced; continue public-route checks and await the user's failing URL/action. diff --git a/package-lock.json b/package-lock.json index 761d9747..df244ca8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18140,6 +18140,34 @@ "integrity": "sha512-/4t+AS1l4S3ZC0Ja7PHFIWeBIxGA3QGqV8/yKsP36v7NcyUCl+bIcmw6s5zVuMIECWwBrAK/6QLzTmbJChBboQ==", "license": "ISC" }, + "node_modules/embla-carousel": { + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/embla-carousel/-/embla-carousel-8.6.0.tgz", + "integrity": "sha512-SjWyZBHJPbqxHOzckOfo8lHisEaJWmwd23XppYFYVh10bU66/Pn5tkVkbkCMZVdbUE5eTCI2nD8OyIP4Z+uwkA==", + "license": "MIT" + }, + "node_modules/embla-carousel-react": { + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/embla-carousel-react/-/embla-carousel-react-8.6.0.tgz", + "integrity": "sha512-0/PjqU7geVmo6F734pmPqpyHqiM99olvyecY7zdweCw+6tKEXnrE90pBiBbMMU8s5tICemzpQ3hi5EpxzGW+JA==", + "license": "MIT", + "dependencies": { + "embla-carousel": "8.6.0", + "embla-carousel-reactive-utils": "8.6.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.1 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + } + }, + "node_modules/embla-carousel-reactive-utils": { + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/embla-carousel-reactive-utils/-/embla-carousel-reactive-utils-8.6.0.tgz", + "integrity": "sha512-fMVUDUEx0/uIEDM0Mz3dHznDhfX+znCCDCeIophYb1QGVM7YThSWX+wz11zlYwWFOr74b4QLGg0hrGPJeG2s4A==", + "license": "MIT", + "peerDependencies": { + "embla-carousel": "8.6.0" + } + }, "node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", @@ -29470,6 +29498,7 @@ "8004-solana": "^0.8.3", "ai": "^6.0.191", "codama": "^1.5.1", + "embla-carousel-react": "^8.6.0", "highlight.js": "^11.11.1", "next": "16.1.6", "next-themes": "^0.4.6", diff --git a/web/__tests__/app/typography-utilities.test.ts b/web/__tests__/app/typography-utilities.test.ts new file mode 100644 index 00000000..37bc0fa5 --- /dev/null +++ b/web/__tests__/app/typography-utilities.test.ts @@ -0,0 +1,32 @@ +import fs from "node:fs/promises"; +import path from "node:path"; +import { compile } from "@tailwindcss/node"; +import { beforeAll, describe, expect, it } from "vitest"; + +describe("semantic typography utilities", () => { + const cases = [ + ["hover:font-title", "--font-crimson-pro"], + ["prose-headings:font-display", "--font-crimson-pro"], + ["hover:font-sans", "--font-inter"], + ["hover:font-mono", "--font-inconsolata"], + ] as const; + let css: string; + + beforeAll(async () => { + const stylesheet = path.join(process.cwd(), "app/globals.css"); + const compiler = await compile(await fs.readFile(stylesheet, "utf8"), { + base: path.dirname(stylesheet), + onDependency: () => {}, + }); + css = compiler.build(cases.map(([utility]) => utility)); + }); + + it.each(cases)("generates %s using %s", (utility, fontVariable) => { + // Compile real variants: plain CSS classes alone cannot provide these. + const selector = `.${utility.replace(":", "\\:")}`; + const start = css.indexOf(`${selector} {`); + expect(start).toBeGreaterThanOrEqual(0); + const declaration = css.slice(start).match(/font-family:\s*([^;]+);/); + expect(declaration?.[1]).toContain(`var(${fontVariable})`); + }); +}); diff --git a/web/app/agent-reputation-system/page.tsx b/web/app/agent-reputation-system/page.tsx index 5e9fd376..21779235 100644 --- a/web/app/agent-reputation-system/page.tsx +++ b/web/app/agent-reputation-system/page.tsx @@ -105,7 +105,7 @@ export default function AgentReputationSystemPage() { AgentVouch is an on-chain agent reputation system for AI agents.

-

+

Why AI agents need a reputation system

@@ -121,7 +121,7 @@ export default function AgentReputationSystemPage() { for the concrete failure mode.

-

+

How an agent reputation system works

@@ -147,7 +147,7 @@ export default function AgentReputationSystemPage() { -

+

How AgentVouch implements it

@@ -169,7 +169,7 @@ export default function AgentReputationSystemPage() { .

-

+

Agent reputation system vs. reputation oracle

@@ -187,12 +187,12 @@ export default function AgentReputationSystemPage() {

-

+

Frequently asked questions

-

+

What is an agent reputation system?

@@ -203,7 +203,7 @@ export default function AgentReputationSystemPage() {

-

+

How does AgentVouch score agent reputation?

diff --git a/web/app/author/[pubkey]/page.tsx b/web/app/author/[pubkey]/page.tsx index 56e1d9cf..170ba370 100644 --- a/web/app/author/[pubkey]/page.tsx +++ b/web/app/author/[pubkey]/page.tsx @@ -1163,7 +1163,7 @@ export default function AuthorProfilePage() {

Report this author

@@ -1386,7 +1386,7 @@ export default function AuthorProfilePage() { {authorDisputes.length > 0 && (
-

+

{" "} Author Disputes

@@ -1448,7 +1448,7 @@ export default function AuthorProfilePage() { {isOwnProfile && profile && (
-

+

Link Solana Agent Registry

@@ -1656,7 +1656,7 @@ export default function AuthorProfilePage() {

{stat.icon}
-
+
{stat.value}
@@ -1670,7 +1670,7 @@ export default function AuthorProfilePage() {
-

+

Manage Base Self Stake

@@ -1758,7 +1758,7 @@ export default function AuthorProfilePage() {
-

+

Your Backing

@@ -1802,7 +1802,7 @@ export default function AuthorProfilePage() { {/* Vouch for this author */} {!isOwnProfile && (

-

+

Vouch for this Author

@@ -1916,7 +1916,7 @@ export default function AuthorProfilePage() { {!isOwnProfile && connected && viewerVouch && (
-

+

Voucher Revenue

@@ -2024,7 +2024,7 @@ export default function AuthorProfilePage() { {/* Skills */}
-

+

Published Skills

@@ -2045,7 +2045,7 @@ export default function AuthorProfilePage() { className="rounded-sm border border-gray-100 dark:border-gray-800 bg-gray-50 dark:bg-gray-800 p-4 hover:border-gray-300 dark:hover:border-gray-600 transition block" >
-

+

{skill.name}

{skill.price_usdc_micros ? ( @@ -2097,7 +2097,7 @@ export default function AuthorProfilePage() { className="rounded-sm border border-gray-100 dark:border-gray-800 bg-gray-50 dark:bg-gray-800 p-4 hover:border-gray-300 dark:hover:border-gray-600 transition block" >
-

+

{skill.name || "Untitled"}

{priceUsdcMicros ? ( @@ -2134,7 +2134,7 @@ export default function AuthorProfilePage() { {/* Vouchers (who vouches for this author) */} {vouchesReceived.length > 0 && (
-

+

Vouchers

@@ -2209,7 +2209,7 @@ export default function AuthorProfilePage() { {/* Vouching for (who this author vouches for) */} {vouchesGiven.length > 0 && (

-

+

Vouching For

@@ -2268,7 +2268,7 @@ export default function AuthorProfilePage() {
-

+

Report this author

diff --git a/web/app/blog/[slug]/page.tsx b/web/app/blog/[slug]/page.tsx index fc2bf5e6..6e4f0b01 100644 --- a/web/app/blog/[slug]/page.tsx +++ b/web/app/blog/[slug]/page.tsx @@ -73,7 +73,7 @@ export default async function BlogPostPage({ type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(blogPostingJsonLd) }} /> -
+
No posts yet.

) : ( -
    +
      {posts.map((post) => (
    • diff --git a/web/app/dashboard/page.tsx b/web/app/dashboard/page.tsx index 8f24599e..bcb24430 100644 --- a/web/app/dashboard/page.tsx +++ b/web/app/dashboard/page.tsx @@ -787,7 +787,7 @@ export default function DashboardPage() { const renderRegisteredAgentsCard = () => (
      -

      +

      Registered Agents

      - {/* Pagination */} + {/* Append-only pagination */} {totalPages > 1 && ( -
      - + ) : ( + + You've reached the end + + )} + - Previous - - - Page {page} of {totalPages} + Showing {skills.length} of {total} skills -
      )} diff --git a/web/app/skills/[id]/SkillDetailClient.tsx b/web/app/skills/[id]/SkillDetailClient.tsx index b8b1ee38..9e6f6c67 100644 --- a/web/app/skills/[id]/SkillDetailClient.tsx +++ b/web/app/skills/[id]/SkillDetailClient.tsx @@ -2200,7 +2200,7 @@ export default function SkillDetailPage({ ); return ( -
      +
      {buyerCardAccessEnabled && ( )} @@ -2228,7 +2228,7 @@ export default function SkillDetailPage({ {skill.name} {skill.description && ( -

      +

      {skill.description}

      )} @@ -2355,7 +2355,7 @@ export default function SkillDetailPage({ {displaySummaryLine && displaySummaryLine !== skill.description && ( -

      +

      {displaySummaryLine}

      )} @@ -2407,7 +2407,7 @@ export default function SkillDetailPage({

      Paid content

      -

      +

      {buyerHasPurchased ? "Purchase verified. Use Sign & Download in the purchase panel to retrieve SKILL.md." : `Complete the ${usdcPriceLabel} purchase, then sign with the buyer wallet to retrieve SKILL.md.`} diff --git a/web/components/AppFooter.tsx b/web/components/AppFooter.tsx index 8f8e6849..39d0f830 100644 --- a/web/components/AppFooter.tsx +++ b/web/components/AppFooter.tsx @@ -11,7 +11,7 @@ export function AppFooter() {

      AgentVouch diff --git a/web/components/AppNavbar.tsx b/web/components/AppNavbar.tsx index 7863c401..d95f598f 100644 --- a/web/components/AppNavbar.tsx +++ b/web/components/AppNavbar.tsx @@ -47,7 +47,7 @@ export const navItems: NavItem[] = [ function navLinkClass(isActive: boolean) { return [ - "font-display text-[15px] px-2 py-1 transition whitespace-nowrap", + "font-sans text-[15px] px-2 py-1 transition whitespace-nowrap", isActive ? "text-gray-900 dark:text-white" : "text-gray-500 hover:text-gray-900 dark:text-gray-400 dark:hover:text-white", @@ -56,7 +56,7 @@ function navLinkClass(isActive: boolean) { function mobileNavLinkClass(isActive: boolean) { return [ - "flex items-center justify-between rounded-sm border px-3 py-2 font-display text-[15px] transition", + "flex items-center justify-between rounded-sm border px-3 py-2 font-sans text-[15px] transition", isActive ? "border-[var(--sea-accent-border)] bg-[var(--sea-accent-soft)] text-[var(--sea-accent-strong)]" : "border-transparent text-gray-600 hover:border-gray-200 hover:bg-gray-50 hover:text-gray-900 dark:text-gray-300 dark:hover:border-gray-800 dark:hover:bg-gray-900 dark:hover:text-white", diff --git a/web/components/FeaturedSkillsCarousel.tsx b/web/components/FeaturedSkillsCarousel.tsx new file mode 100644 index 00000000..de17aa3c --- /dev/null +++ b/web/components/FeaturedSkillsCarousel.tsx @@ -0,0 +1,106 @@ +"use client"; + +import { Children, useEffect, useState, type ReactNode } from "react"; +import { + Carousel, + CarouselContent, + CarouselItem, + CarouselNext, + CarouselPrevious, + type CarouselApi, +} from "@/components/ui/carousel"; + +interface FeaturedSkillsCarouselProps { + children: ReactNode; +} + +export function FeaturedSkillsCarousel({ + children, +}: FeaturedSkillsCarouselProps) { + const items = Children.toArray(children); + const [api, setApi] = useState(); + const [current, setCurrent] = useState(0); + const [snapCount, setSnapCount] = useState(0); + + useEffect(() => { + if (!api) return; + + const updateCarousel = () => { + setCurrent(api.selectedScrollSnap()); + setSnapCount(api.scrollSnapList().length); + }; + updateCarousel(); + api.on("reInit", updateCarousel); + api.on("select", updateCarousel); + + return () => { + api.off("reInit", updateCarousel); + api.off("select", updateCarousel); + }; + }, [api]); + + if (items.length === 0) return null; + + return ( +
      + 1 }} + aria-label="Featured skills" + > + + {items.map((item, index) => ( + +
      {item}
      +
      + ))} +
      + {snapCount > 1 && ( + <> + + + + )} +
      + + {snapCount > 1 && ( +
      +
      + {Array.from({ length: snapCount }, (_, index) => { + const isCurrent = current === index; + + return ( +
      + + {String(current + 1).padStart(2, "0")} /{" "} + {String(snapCount).padStart(2, "0")} + +
      + )} +
      + ); +} diff --git a/web/components/MarkdownRenderer.tsx b/web/components/MarkdownRenderer.tsx index 625e15d7..030f4f7e 100644 --- a/web/components/MarkdownRenderer.tsx +++ b/web/components/MarkdownRenderer.tsx @@ -43,7 +43,7 @@ function FrontmatterBlock({ value }: { value: string }) { if (!value) return null; return ( -
      +
      skill metadata
      @@ -84,8 +84,8 @@ export default function MarkdownRenderer({ const variantClass = variant === "skill" ? [ - "font-heading text-[13px] leading-6 text-gray-700 dark:text-gray-300", - "prose-headings:font-heading prose-headings:font-normal prose-headings:tracking-normal", + "font-sans text-[13px] leading-6 text-gray-700 dark:text-gray-300", + "prose-headings:font-display prose-headings:font-normal prose-headings:tracking-normal", "prose-h1:mt-6 prose-h1:mb-3 prose-h1:text-[22px] prose-h1:leading-tight", "prose-h2:mt-6 prose-h2:mb-2 prose-h2:text-[16px] prose-h2:leading-snug", "prose-h3:mt-5 prose-h3:mb-2 prose-h3:text-[14px] prose-h3:leading-snug", @@ -96,8 +96,8 @@ export default function MarkdownRenderer({ : ""; const inlineCodeClass = variant === "skill" - ? "rounded-sm border border-[var(--lobster-accent-border)] bg-[var(--lobster-accent-soft)] px-1.5 py-0.5 font-heading text-[0.92em] text-[var(--lobster-accent)]" - : "bg-gray-100 dark:bg-gray-800 rounded px-1.5 py-0.5 text-sm text-pink-600 dark:text-pink-400"; + ? "rounded-sm border border-[var(--lobster-accent-border)] bg-[var(--lobster-accent-soft)] px-1.5 py-0.5 font-mono text-[0.92em] text-[var(--lobster-accent)]" + : "bg-gray-100 dark:bg-gray-800 rounded px-1.5 py-0.5 font-mono text-sm text-pink-600 dark:text-pink-400"; return (
      -

      +

      Full content unlocks after purchase

      diff --git a/web/components/SkillPreviewCard.tsx b/web/components/SkillPreviewCard.tsx index 6d3896ea..a80f14c0 100644 --- a/web/components/SkillPreviewCard.tsx +++ b/web/components/SkillPreviewCard.tsx @@ -493,7 +493,7 @@ export default function SkillPreviewCard({ {/* Description — readable body face */} {displayDescription && (

      {displayDescription} diff --git a/web/components/ui/carousel.tsx b/web/components/ui/carousel.tsx new file mode 100644 index 00000000..a0ad0fb9 --- /dev/null +++ b/web/components/ui/carousel.tsx @@ -0,0 +1,259 @@ +"use client"; + +import * as React from "react"; +import useEmblaCarousel from "embla-carousel-react"; +import type { UseEmblaCarouselType } from "embla-carousel-react"; +import { FiChevronLeft, FiChevronRight } from "react-icons/fi"; + +type CarouselApi = UseEmblaCarouselType[1]; +type UseCarouselParameters = Parameters; +type CarouselOptions = UseCarouselParameters[0]; +type CarouselPlugin = UseCarouselParameters[1]; + +type CarouselProps = { + opts?: CarouselOptions; + plugins?: CarouselPlugin; + orientation?: "horizontal" | "vertical"; + setApi?: (api: CarouselApi) => void; +}; + +type CarouselContextProps = { + carouselRef: ReturnType[0]; + api: CarouselApi; + opts?: CarouselOptions; + orientation: "horizontal" | "vertical"; + scrollPrev: () => void; + scrollNext: () => void; + canScrollPrev: boolean; + canScrollNext: boolean; +}; + +const CarouselContext = React.createContext(null); + +function joinClasses( + ...classes: Array +): string { + return classes.filter(Boolean).join(" "); +} + +function useCarousel() { + const context = React.useContext(CarouselContext); + + if (!context) { + throw new Error("useCarousel must be used within a "); + } + + return context; +} + +const Carousel = React.forwardRef< + HTMLDivElement, + CarouselProps & React.HTMLAttributes +>( + ( + { + orientation = "horizontal", + opts, + setApi, + plugins, + className, + children, + onKeyDown, + ...props + }, + ref + ) => { + const [carouselRef, api] = useEmblaCarousel( + { + ...opts, + axis: orientation === "horizontal" ? "x" : "y", + }, + plugins + ); + const [canScrollPrev, setCanScrollPrev] = React.useState(false); + const [canScrollNext, setCanScrollNext] = React.useState(false); + + const onSelect = React.useCallback((carouselApi: CarouselApi) => { + if (!carouselApi) return; + + setCanScrollPrev(carouselApi.canScrollPrev()); + setCanScrollNext(carouselApi.canScrollNext()); + }, []); + + const scrollPrev = React.useCallback(() => { + api?.scrollPrev(); + }, [api]); + + const scrollNext = React.useCallback(() => { + api?.scrollNext(); + }, [api]); + + const handleKeyDown = React.useCallback( + (event: React.KeyboardEvent) => { + if (event.key === "ArrowLeft") { + event.preventDefault(); + scrollPrev(); + } else if (event.key === "ArrowRight") { + event.preventDefault(); + scrollNext(); + } + + onKeyDown?.(event); + }, + [onKeyDown, scrollNext, scrollPrev] + ); + + React.useEffect(() => { + if (!api) return; + + onSelect(api); + api.on("reInit", onSelect); + api.on("select", onSelect); + + return () => { + api.off("reInit", onSelect); + api.off("select", onSelect); + }; + }, [api, onSelect]); + + React.useEffect(() => { + if (api && setApi) setApi(api); + }, [api, setApi]); + + return ( + +

      + {children} +
      + + ); + } +); +Carousel.displayName = "Carousel"; + +const CarouselContent = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => { + const { carouselRef, orientation } = useCarousel(); + + return ( +
      +
      +
      + ); +}); +CarouselContent.displayName = "CarouselContent"; + +const CarouselItem = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => { + const { orientation } = useCarousel(); + + return ( +
      + ); +}); +CarouselItem.displayName = "CarouselItem"; + +const CarouselPrevious = React.forwardRef< + HTMLButtonElement, + React.ButtonHTMLAttributes +>(({ className, children, ...props }, ref) => { + const { orientation, scrollPrev, canScrollPrev } = useCarousel(); + + return ( + + ); +}); +CarouselPrevious.displayName = "CarouselPrevious"; + +const CarouselNext = React.forwardRef< + HTMLButtonElement, + React.ButtonHTMLAttributes +>(({ className, children, ...props }, ref) => { + const { orientation, scrollNext, canScrollNext } = useCarousel(); + + return ( + + ); +}); +CarouselNext.displayName = "CarouselNext"; + +export { + Carousel, + CarouselContent, + CarouselItem, + CarouselPrevious, + CarouselNext, +}; +export type { CarouselApi }; diff --git a/web/package.json b/web/package.json index 08ef41d4..03fdb091 100644 --- a/web/package.json +++ b/web/package.json @@ -41,6 +41,7 @@ "8004-solana": "^0.8.3", "ai": "^6.0.191", "codama": "^1.5.1", + "embla-carousel-react": "^8.6.0", "highlight.js": "^11.11.1", "next": "16.1.6", "next-themes": "^0.4.6", diff --git a/web/tailwind.config.ts b/web/tailwind.config.ts index 808202ee..a4f4b1ee 100644 --- a/web/tailwind.config.ts +++ b/web/tailwind.config.ts @@ -10,7 +10,7 @@ const config: Config = { extend: { fontFamily: { sans: ["var(--font-inter)", '"Inter"', "sans-serif"], - heading: ["var(--font-inconsolata)", '"Inconsolata"', "monospace"], + title: ["var(--font-crimson-pro)", '"Crimson Pro"', "serif"], display: ["var(--font-crimson-pro)", '"Crimson Pro"', "serif"], mono: ["var(--font-inconsolata)", '"Inconsolata"', "monospace"], },