Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .claude/skills/design-system/references/components.md
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ Responsive grid for laying out a collection of items (cards, tiles, badges). Ren
- `size`: `small (7rem) | narrow (12rem) | base (18rem, default) | wide (22rem) | wider (26rem)`. The **min item width** (`--grid-item-min`) — the floor an item shrinks to before a column drops, and so the fold-aggressiveness lever. Pick by item shape: `small` for badges, `base` for standard content cards, `wide`/`wider` for horizontal items like callouts. Larger sizes wrap sooner; keep `columns` small enough that N items of the chosen width fit (min ≤ container/N).
- `fit`: `boolean` (auto-fill mode only). Default keeps empty tracks (`auto-fill`); `fit` collapses them (`auto-fit`) so a partially-filled row stretches to fill the width.
- `balanced`: `2 | 4`. A fixed, deterministic **breakpoint** reflow (overrides `columns` via `!important`; `size`/`fit` inert). `balanced={4}` → `4 → 2×2 → 1`, never an orphan 3-up row (which auto-fill produces); `balanced={2}` → `2 → 1` at `md`, a breakpoint-driven alternative to `columns={2}` (which folds by content width). Both fold `1 → 2` at `md`. Use with a **fixed set** of that many items (or a multiple).
- Applies `gap-4`. `className` is spread last, so override the gap (or `--grid-item-min` / `--grid-repeat`) per call site only when genuinely needed.
- Applies `gap-4`. `className` is spread last, so override the gap (or `--grid-item-min` / `--grid-repeat`) per call site only when genuinely needed. **If you override the gap, mirror it in `--grid-gap`** (e.g. `gap-x-8 [--grid-gap:--spacing(8)]`): the `grid-cols-auto-*` fold math assumes the gutter via `var(--grid-gap, 1rem)`, and with a wider real gap the even-share term is computed against the wrong gutter -- a second column may **never** form.

### `Section`

Expand Down
4 changes: 4 additions & 0 deletions .claude/skills/design-system/references/gotchas.md
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,10 @@ Use `useRtlFlip` only when you need to flip a non-chevron directional icon (a cu

Configured in `base.css` lines 97-107. Persian fallback. Triggered by `:lang(ur)`, not `dir`. Don't override.

### Bare `<ul>`/`<li>` inherit legacy global list styles

`base.css` still ships global `ul`/`ol` rules (start margin + `disc` markers, TODO-slated for removal) and a global `li` bottom margin. A non-prose `<ul>` (nav menu, card list) needs `m-0 list-none` on the `ul` and `m-0` on the `li`s or it renders indented with stray spacing. And once `list-none` removes the markers, Safari/VoiceOver drops list semantics -- add `role="list"` to keep the list announced.

## Heading Hierarchy

### One `<h1>` per page
Expand Down
4 changes: 4 additions & 0 deletions .claude/skills/design-system/references/i18n-rtl.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,10 @@ If a string is rendered to a user, it must come from a translation key. The exce

If you find yourself wanting to hard-code, add the key to `src/intl/en/[namespace].json` and use `t()`.

### Interpolate a translated value, never a hard-coded literal

A shared skeleton with an interpolated value -- `t("visit-brand", { brand: t("dune-brand") })` -- is the pattern to reach for: one `"Visit {brand}"` string instead of the `"Visit "` prefix repeated across N labels, and each brand is its own key the pipeline can transliterate or case-adjust per locale. The anti-pattern is passing a hard-coded English literal as the variable (`t("visit-brand", { brand: "Dune" })`): the `"Dune"` literal never enters an intl file, so it stays untranslated English no matter the locale. If a proper noun/brand is enumerable, give it a key and inject the translated value; reserve raw literals for genuinely dynamic runtime values (counts, dates, user input) that can't be keys. `ProductList` CTAs follow this -- see `AiAgentProductLists` and the stablecoins tools list.

## RTL: Logical CSS Properties

Always use logical equivalents instead of physical directions:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ const ToolsPageBody = async ({
noResults: t("page-developers-tools-no-results"),
}}
/>
<div className="flex flex-col items-center gap-4 rounded-base bg-radial-a p-12">
<div className="bg-radial-a flex flex-col items-center gap-4 rounded-base p-12">
<h2>{t("page-developers-tools-suggest-resource-title")}</h2>
<p>{t("page-developers-tools-suggest-resource-description")}</p>
<ButtonLink
Expand Down
5 changes: 4 additions & 1 deletion app/[locale]/developers/tools/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,10 @@ const Page = async (props: { params: Promise<PageParams> }) => {
getDeveloperToolsData(),
getAppPageContributorInfo("developers/tools", locale as Lang),
getTranslations({ locale, namespace: "page-developers-tools" }),
getTranslations({ locale, namespace: "page-developers-tools-descriptions" }),
getTranslations({
locale,
namespace: "page-developers-tools-descriptions",
}),
])

const normalized = normalizeDeveloperToolsData(data)
Expand Down
55 changes: 33 additions & 22 deletions app/[locale]/stablecoins/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import { CheckCircle } from "@/components/icons/CheckCircle"
import { XCircle } from "@/components/icons/XCircle"
import { Image } from "@/components/Image"
import MainArticle from "@/components/MainArticle"
import ProductList from "@/components/ProductList"
import ProductList, { type ProductListContent } from "@/components/ProductList"
import { StandaloneQuizWidget } from "@/components/Quiz/QuizWidget"
import Tooltip from "@/components/Tooltip"
import Translation from "@/components/Translation"
Expand Down Expand Up @@ -371,51 +371,63 @@ async function Page(props: { params: Promise<PageParams> }) {
},
]

const toolsData = [
const toolsData: ProductListContent[] = [
{
title: "Stablecoins.wtf",
title: t("page-stablecoins-tools-stablecoinswtf-title"),
description: t("page-stablecoins-tools-stablecoinswtf-description"),
link: "https://stablecoins.wtf",
href: "https://stablecoins.wtf",
image: stablecoinsWtfImg,
alt: "",
ctaLabel: t("page-stablecoins-tools-visit-brand", {
brand: t("page-stablecoins-tools-stablecoinswtf-title"),
}),
},
{
title: "Stablepulse",
title: t("page-stablecoins-tools-stablepulse-title"),
description: t("page-stablecoins-tools-stablepulse-description"),
link: "https://www.stablepulse.org/",
href: "https://www.stablepulse.org/",
image: stablePulseImg,
alt: "",
className: "[&_img]:p-1",
ctaLabel: t("page-stablecoins-tools-visit-brand", {
brand: t("page-stablecoins-tools-stablepulse-title"),
}),
},
{
title: "Stables.info",
title: t("page-stablecoins-tools-stablesinfo-title"),
description: t("page-stablecoins-tools-stablesinfo-description"),
link: "https://stables.info/",
href: "https://stables.info/",
image: stablesInfoImg,
alt: "",
ctaLabel: t("page-stablecoins-tools-visit-brand", {
brand: t("page-stablecoins-tools-stablesinfo-title"),
}),
},
{
title: "Dune Stablecoin Metrics",
title: t("page-stablecoins-tools-dune-title"),
description: t("page-stablecoins-tools-dune-description"),
link: "https://dune.com/overview/stablecoin",
href: "https://dune.com/overview/stablecoin",
image: duneImg,
alt: "",
className: "dark:[&_img]:invert [&_img]:p-2",
ctaLabel: t("page-stablecoins-tools-visit-brand", {
brand: t("page-stablecoins-tools-dune-brand"),
}),
},
{
title: "Visa Onchain Analytics",
title: t("page-stablecoins-tools-visa-title"),
description: t("page-stablecoins-tools-visa-description"),
link: "https://visaonchainanalytics.com/",
href: "https://visaonchainanalytics.com/",
image: visaImg,
alt: "",
ctaLabel: t("page-stablecoins-tools-visit-brand", {
brand: t("page-stablecoins-tools-visa-brand"),
}),
},
{
title: "Stablewars",
title: t("page-stablecoins-tools-stablewars-title"),
description: t("page-stablecoins-tools-stablewars-description"),
link: "https://stablewars.xyz/",
href: "https://stablewars.xyz/",
image: stablesWarsImg,
alt: "",
className: "[&_img]:p-1",
ctaLabel: t("page-stablecoins-tools-visit-brand", {
brand: t("page-stablecoins-tools-stablewars-title"),
}),
},
]

Expand Down Expand Up @@ -855,12 +867,11 @@ async function Page(props: { params: Promise<PageParams> }) {
<h2>{t("page-stablecoins-tools-title")}</h2>
<div className="[&_img]:shadow-none">
<ProductList
columns={2}
category={t(
"page-stablecoins-category-dashboard-and-education"
)}
actionLabel={t("page-apps-ready-button")}
content={toolsData}
columns={2}
/>
</div>
</Section>
Expand Down
109 changes: 34 additions & 75 deletions src/components/Content/ai-agents/AiAgentProductLists.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { useTranslations } from "next-intl"
import { getTranslations } from "next-intl/server"

import { Strong } from "@/components/IntlStringElements"
import ProductListComponent from "@/components/ProductList"
import { ButtonLink } from "@/components/ui/buttons/Button"
import InlineLink from "@/components/ui/Link"
import ProductListComponent, {
type ProductListContent,
} from "@/components/ProductList"

import aiagenttoolkit from "@/public/images/ai-agents/aiagenttoolkit.png"
import aixbt from "@/public/images/ai-agents/aixbt.png"
Expand All @@ -14,121 +14,80 @@ import cookiefun from "@/public/images/ai-agents/cookiefun.png"
import heyanon from "@/public/images/ai-agents/heyanon.png"
import luna from "@/public/images/ai-agents/luna.png"

const AiAgentProductLists = ({ list }: { list: string }) => {
const t = useTranslations("component-ai-agent-products")
const AiAgentProductLists = async ({ list }: { list: string }) => {
const t = await getTranslations("component-ai-agent-products")

const productListSets = {
"ai-agents": [
{
title: t("luna-title"),
description: "",
image: luna,
contentItems: [
<p key="luna-description-1">
{t.rich("luna-description-1", {
strong: Strong,
a: (chunks) => (
<InlineLink href="https://www.tiktok.com/@aidolofficial">
{chunks}
</InlineLink>
),
})}
</p>,
<p key="luna-description-2">{t("luna-description-2")}</p>,
<div key="luna-button">
<ButtonLink
href="https://app.virtuals.io/virtuals/68"
variant="outline"
>
{t("chat-with-brand", { brand: t("luna-brand") })}
</ButtonLink>
</div>,
description: [
t.rich("luna-description-1", {
strong: Strong,
}),
t("luna-description-2"),
],
image: luna,
href: "https://app.virtuals.io/virtuals/68",
ctaLabel: t("chat-with-brand", { brand: t("luna-brand") }),
},
{
title: t("aixbt-title"),
description: "",
description: [t("aixbt-description-1"), t("aixbt-description-2")],
image: aixbt,
contentItems: [
<p key="aixbt-description-1">{t("aixbt-description-1")}</p>,
<p key="aixbt-description-2">{t("aixbt-description-2")}</p>,
<div key="aixbt-button">
<ButtonLink href="https://x.com/aixbt_agent" variant="outline">
{t("visit-brand", { brand: t("aixbt-brand") })}
</ButtonLink>
</div>,
],
href: "https://x.com/aixbt_agent",
ctaLabel: t("visit-brand", { brand: t("aixbt-brand") }),
},
{
title: t("botto-title"),
description: "",
description: t("botto-description"),
image: botto,
contentItems: [
<p key="botto-description">{t("botto-description")}</p>,
<div key="botto-button">
<ButtonLink href="https://botto.com/" variant="outline">
{t("visit-brand", { brand: t("botto-brand") })}
</ButtonLink>
</div>,
],
href: "https://botto.com/",
ctaLabel: t("visit-brand", { brand: t("botto-brand") }),
},
],
chat: [
{
title: t("bankr-title"),
description: "",
description: t("bankr-description"),
image: bankr,
contentItems: [
<p key="bankr-description">{t("bankr-description")}</p>,
<div key="bankr-button">
<ButtonLink href="https://bankr.bot/" variant="outline">
{t("bankr-button")}
</ButtonLink>
</div>,
],
href: "https://bankr.bot/",
ctaLabel: t("bankr-button"),
},
{
title: t("heyanon-title"),
description: "",
description: t("heyanon-description"),
image: heyanon,
contentItems: [
<p key="heyanon-description">{t("heyanon-description")}</p>,
<div key="heyanon-button">
<ButtonLink href="https://heyanon.ai/" variant="outline">
{t("visit-brand", { brand: t("heyanon-brand") })}
</ButtonLink>
</div>,
],
href: "https://heyanon.ai/",
ctaLabel: t("visit-brand", { brand: t("heyanon-brand") }),
},
],
"dive-deeper": [
{
title: t("aiagenttoolkit-title"),
description: t("aiagenttoolkit-description"),
image: aiagenttoolkit,
link: "https://www.aiagenttoolkit.xyz/t/frameworks",
href: "https://www.aiagenttoolkit.xyz/t/frameworks",
ctaLabel: t("visit-brand", { brand: t("aiagenttoolkit-brand") }),
},
{
title: t("cookiefun-title"),
description: t("cookiefun-description"),
image: cookiefun,
link: "https://cookie.fun/",
href: "https://cookie.fun/",
ctaLabel: t("visit-brand", { brand: t("cookiefun-brand") }),
},
{
title: t("clanker-title"),
description: t("clanker-description"),
image: clanker,
link: "https://farcaster.xyz/clanker",
href: "https://farcaster.xyz/clanker",
ctaLabel: t("visit-brand", { brand: t("clanker-brand") }),
},
],
}
} satisfies Record<string, ProductListContent[]>

return (
<ProductListComponent
content={productListSets[list]}
actionLabel={t("action-label")}
/>
)
return <ProductListComponent content={productListSets[list]} />
}

export default AiAgentProductLists
Loading
Loading