fix: update responsive styles and improve layout across various components

This commit is contained in:
jinhojang6
2026-06-19 00:31:34 +09:00
parent 933b2130c4
commit 4a70bc3f5a
9 changed files with 161 additions and 63 deletions
+4
View File
@@ -60,3 +60,7 @@ apps/*/public/fonts/**/*.otf
mcp.json
.env*.local
# Playwright / browser-MCP artifacts (never commit)
.playwright-mcp/
**/.playwright-mcp/
@@ -6,22 +6,22 @@ import type { LambdaPrizePageCopy } from './types'
export function Support({ copy }: { copy: LambdaPrizePageCopy['support'] }) {
return (
<section className="h-[406px] bg-brand-off-white px-0 py-10 text-brand-dark-green min-[1025px]:h-[421px] min-[1025px]:px-3 min-[1025px]:py-12">
<section className="h-[406px] bg-brand-off-white px-0 py-10 text-brand-dark-green lg:h-[421px] lg:px-3 lg:py-12">
<ContentWidth>
<div className="grid grid-cols-[1fr_auto] gap-y-6 min-[1025px]:grid-cols-3">
<div className="grid grid-cols-[1fr_auto] gap-y-6 lg:grid-cols-3">
<h2 className="text-h3-serif">{copy.heading}</h2>
<p className="text-mono-s col-start-1 row-start-2 w-[178px] min-[1025px]:col-start-auto min-[1025px]:row-start-auto min-[1025px]:w-[226px]">
<p className="text-mono-s col-start-1 row-start-2 w-[178px] lg:col-start-auto lg:row-start-auto lg:w-[226px]">
{copy.body}
</p>
<div className="col-start-2 row-start-1 min-[1025px]:col-start-auto min-[1025px]:row-start-auto">
<div className="col-start-2 row-start-1 lg:col-start-auto lg:row-start-auto">
<TertiaryCta href={ROUTES.faq}>{copy.cta}</TertiaryCta>
</div>
</div>
<div className="mt-6 min-[1025px]:mt-8">
<div className="mt-6 lg:mt-8">
{copy.rows.map((row, index) => (
<div
key={row.label}
className={`grid h-[58px] grid-cols-[1fr_83px] items-start px-3 py-3 min-[1025px]:h-[50px] min-[1025px]:grid-cols-[714fr_464fr_238fr] ${
className={`grid h-[58px] grid-cols-[1fr_83px] items-start px-3 py-3 lg:h-[50px] lg:grid-cols-[714fr_464fr_238fr] ${
index % 2 === 0 ? 'bg-gray-01' : 'bg-brand-dark-green/5'
}`}
>
@@ -31,7 +31,7 @@ export function Support({ copy }: { copy: LambdaPrizePageCopy['support'] }) {
</p>
<h3 className="text-body-serif">{row.label}</h3>
</div>
<p className="text-mono-s hidden max-w-[312px] min-[1025px]:block">
<p className="text-mono-s hidden max-w-[312px] lg:block">
{row.body}
</p>
<div>
@@ -29,7 +29,7 @@ export function RfpListRow({ index, rfp }: Props) {
>
{/* Mobile / tablet (below 1025px): 2-column flow grid so a long title
pushes the description down instead of overlapping it. */}
<div className="grid min-h-[116px] grid-cols-[1fr_auto] items-start gap-x-3 gap-y-3 p-3 min-[1025px]:hidden">
<div className="grid min-h-[116px] grid-cols-[1fr_auto] items-start gap-x-3 gap-y-3 p-3 lg:hidden">
<p className="font-sans text-[14px] leading-[1.2] text-brand-dark-green">
<span className="font-medium">{indexLabel}</span>
<span className="ml-3 font-display">{rfp.title}</span>
@@ -56,7 +56,7 @@ export function RfpListRow({ index, rfp }: Props) {
{/* Desktop (1025px+): single flex row. The reward + CTA block is
shrink-0 so it always stays inside the table; title and description
truncate as the window narrows. */}
<div className="hidden h-[50px] items-start gap-3 px-3 pt-3 min-[1025px]:flex">
<div className="hidden h-[50px] items-start gap-3 px-3 pt-3 lg:flex">
<div className="flex w-1/2 min-w-0 items-baseline gap-3">
<span className="w-[18px] shrink-0 font-sans text-[14px] font-normal leading-[1.2] text-brand-dark-green">
{indexLabel}
@@ -17,7 +17,7 @@ export function ContributeSection({
jobs: ReactNode
}) {
return (
<section className="border-t border-brand-dark-green/10 bg-brand-off-white px-3 py-10 text-brand-dark-green md:min-h-[262px]">
<section className="border-t border-brand-dark-green/10 bg-brand-off-white py-10 text-brand-dark-green md:min-h-[262px]">
<ContentWidth className="grid gap-10 md:grid-cols-12 md:gap-3">
<h2 className="text-[24px] leading-[1.1] tracking-[-0.24px] md:col-span-3">
{title}
@@ -15,17 +15,17 @@ export function OverviewSection({
cta: LinkItem
}) {
return (
<section className="border-t border-brand-dark-green/10 bg-brand-off-white px-3 py-10 text-brand-dark-green min-[1025px]:min-h-[670px]">
<ContentWidth className="grid gap-10 min-[1025px]:grid-cols-12 min-[1025px]:gap-3">
<h2 className="text-[24px] leading-[1.1] tracking-[-0.24px] min-[1025px]:col-span-3">
<section className="border-t border-brand-dark-green/10 bg-brand-off-white py-10 text-brand-dark-green lg:min-h-[670px]">
<ContentWidth className="grid gap-10 lg:grid-cols-12 lg:gap-3">
<h2 className="text-[24px] leading-[1.1] tracking-[-0.24px] lg:col-span-3">
{title}
</h2>
<div className="text-mono-s space-y-[13px] min-[1025px]:col-span-3 min-[1025px]:col-start-7 min-[1025px]:max-w-[345px]">
<div className="text-mono-s space-y-[13px] lg:col-span-3 lg:col-start-7 lg:max-w-[345px]">
{paragraphs.map((paragraph, index) => (
<p key={index}>{paragraph}</p>
))}
</div>
<div className="min-[1025px]:col-span-2 min-[1025px]:col-start-11 min-[1025px]:justify-self-start">
<div className="lg:col-span-2 lg:col-start-11 lg:justify-self-start">
<LinkButton {...cta} />
</div>
</ContentWidth>
@@ -52,7 +52,7 @@ export function ArticleCard({
return (
<ExternalLink
href={href}
className="group flex w-84.75 shrink-0 cursor-pointer flex-col gap-1.5 min-[1025px]:w-auto"
className="group flex w-84.75 shrink-0 cursor-pointer flex-col gap-1.5 lg:w-auto"
>
<div className="relative aspect-339/431 w-full overflow-hidden">
<Image
@@ -76,8 +76,8 @@ export function ArticleCard({
<p className={titleClassName}>
{mobileTitle ? (
<>
<span className="min-[1025px]:hidden">{mobileTitle}</span>
<span className="hidden min-[1025px]:inline">{title}</span>
<span className="lg:hidden">{mobileTitle}</span>
<span className="hidden lg:inline">{title}</span>
</>
) : (
title
@@ -11,7 +11,7 @@ import { getTechStackRelatedArticleCards } from '@/lib/tech-stack-related-articl
import { ArticleCard } from './related-articles-card'
const TITLE_CLASSNAME =
'text-caption-sans w-[169.5px] shrink-0 font-medium text-brand-dark-green min-[1025px]:text-[14px] min-[1025px]:leading-[1.2]'
'text-caption-sans w-[169.5px] shrink-0 font-medium text-brand-dark-green lg:text-[14px] lg:leading-[1.2]'
type Props = {
data: RelatedArticlesSection
@@ -36,51 +36,51 @@ export default function TechStackRelatedArticles({
className={twMerge('h-220 px-3 pb-3 pt-0', contentClassName)}
>
<Reveal
amount={0.15}
className="relative h-full overflow-hidden rounded-xl bg-accent-tan px-3 pt-6 pb-10 min-[1025px]:px-0 min-[1025px]:pt-0 min-[1025px]:pb-0"
>
<div className="flex items-start justify-between min-[1025px]:block">
{data.label ? (
<p className="text-mono-s w-56.5 max-w-[50%] text-brand-dark-green min-[1025px]:absolute min-[1025px]:top-6 min-[1025px]:left-3 min-[1025px]:max-w-none">
{data.mobileLabel ?? data.label}
</p>
) : null}
{data.eyebrow ? (
// xl, not min-[1025px]: the fixed left-178.5 offset collides with the
// right-anchored CTA on viewports narrower than ~1190px.
<p className="text-mono-s absolute top-6 left-178.5 hidden w-56.5 text-brand-dark-green xl:block">
{data.eyebrow}
</p>
) : null}
{data.cta ? (
<div className="flex w-56.5 max-w-[50%] justify-end min-[1025px]:absolute min-[1025px]:top-5.5 min-[1025px]:right-[87px] min-[1025px]:block min-[1025px]:w-auto min-[1025px]:max-w-none">
<Button
href={data.cta.href}
variant="tertiary"
className="cursor-pointer"
>
{data.cta.label}
</Button>
</div>
) : null}
</div>
<h2 className="mt-16.25 text-center font-display text-[40px] leading-none tracking-[-0.03em] whitespace-nowrap text-brand-dark-green min-[1025px]:absolute min-[1025px]:top-25.5 min-[1025px]:left-1/2 min-[1025px]:mt-0 min-[1025px]:w-116 min-[1025px]:-translate-x-1/2 min-[1025px]:text-h3-serif">
{data.title}
</h2>
<Reveal
stagger
amount={0.2}
className="mt-25 flex gap-3 overflow-x-auto min-[1025px]:absolute min-[1025px]:top-60.25 min-[1025px]:right-3 min-[1025px]:left-3 min-[1025px]:mt-0 min-[1025px]:grid min-[1025px]:grid-cols-4 min-[1025px]:overflow-visible"
amount={0.15}
className="relative h-full overflow-hidden rounded-xl bg-accent-tan px-3 pt-6 pb-10 lg:px-0 lg:pt-0 lg:pb-0"
>
{cards.map((card) => (
<RevealItem key={`${card.href}:${card.title}`}>
<ArticleCard {...card} titleClassName={TITLE_CLASSNAME} />
</RevealItem>
))}
<div className="flex items-start justify-between lg:block">
{data.label ? (
<p className="text-mono-s w-56.5 max-w-[50%] text-brand-dark-green lg:absolute lg:top-6 lg:left-3 lg:max-w-none">
{data.mobileLabel ?? data.label}
</p>
) : null}
{data.eyebrow ? (
// xl, not lg: the fixed left-178.5 offset collides with the
// right-anchored CTA on viewports narrower than ~1190px.
<p className="text-mono-s absolute top-6 left-178.5 hidden w-56.5 text-brand-dark-green xl:block">
{data.eyebrow}
</p>
) : null}
{data.cta ? (
<div className="flex w-56.5 max-w-[50%] justify-end lg:absolute lg:top-5.5 lg:right-[87px] lg:block lg:w-auto lg:max-w-none">
<Button
href={data.cta.href}
variant="tertiary"
className="cursor-pointer"
>
{data.cta.label}
</Button>
</div>
) : null}
</div>
<h2 className="mt-16.25 text-center font-display text-[40px] leading-none tracking-[-0.03em] whitespace-nowrap text-brand-dark-green lg:absolute lg:top-25.5 lg:left-1/2 lg:mt-0 lg:w-116 lg:-translate-x-1/2 lg:text-h3-serif">
{data.title}
</h2>
<Reveal
stagger
amount={0.2}
className="mt-25 flex gap-3 overflow-x-auto lg:absolute lg:top-60.25 lg:right-3 lg:left-3 lg:mt-0 lg:grid lg:grid-cols-4 lg:overflow-visible"
>
{cards.map((card) => (
<RevealItem key={`${card.href}:${card.title}`}>
<ArticleCard {...card} titleClassName={TITLE_CLASSNAME} />
</RevealItem>
))}
</Reveal>
</Reveal>
</Reveal>
</ContentWidth>
</div>
</section>
@@ -0,0 +1,49 @@
import { describe, expect, it } from 'vitest'
import { rewriteRfpMarkdownLinks } from '@/lib/rfps-github'
const FILE_URL =
'https://github.com/logos-co/rfp/blob/master/RFPs/RFP-016-lbp-launchpad.md'
describe('rewriteRfpMarkdownLinks', () => {
it('rewrites sibling RFP cross-references to internal detail routes', () => {
const md = 'See [LBP](./RFP-008-lending-borrowing-protocol.md) for context.'
expect(rewriteRfpMarkdownLinks(md, FILE_URL)).toBe(
'See [LBP](/builders-hub/rfps/lending-borrowing-protocol) for context.'
)
})
it('rewrites RFP references without a leading ./', () => {
const md = '[admin](RFP-001-admin-authority-lib.md)'
expect(rewriteRfpMarkdownLinks(md, FILE_URL)).toBe(
'[admin](/builders-hub/rfps/admin-authority-lib)'
)
})
it('resolves other repo-relative .md links to absolute GitHub URLs, keeping anchors', () => {
const md =
'[fees](../appendix/token-launchpad-ecosystem.md#fee-structures)'
expect(rewriteRfpMarkdownLinks(md, FILE_URL)).toBe(
'[fees](https://github.com/logos-co/rfp/blob/master/appendix/token-launchpad-ecosystem.md#fee-structures)'
)
})
it('leaves external, absolute, and anchor links unchanged', () => {
const md = [
'[ext](https://github.com/logos-co/lambda-prize/blob/master/prizes/LP-0004.md)',
'[abs](/builders-hub/rfps)',
'[frag](#overview)',
].join('\n')
expect(rewriteRfpMarkdownLinks(md, FILE_URL)).toBe(md)
})
it('leaves relative non-markdown links untouched', () => {
const md = '![diagram](./images/flow.png)'
expect(rewriteRfpMarkdownLinks(md, FILE_URL)).toBe(md)
})
})
+46 -1
View File
@@ -1,6 +1,7 @@
import matter from 'gray-matter'
import { cache } from 'react'
import { ROUTES } from '@/constants/routes'
import type { RfpListItem } from '@/lib/rfp-types'
/**
@@ -73,6 +74,50 @@ const toSlug = (filename: string, fallback: string): string => {
return slug || fallback.toLowerCase()
}
/** Matches the URL target of a markdown inline link/image: `](target)`. */
const MARKDOWN_LINK_TARGET = /\]\(\s*(<[^>]+>|[^)\s]+)\s*\)/g
const isAbsoluteOrAnchor = (href: string): boolean =>
/^(https?:\/\/|\/|#|mailto:)/i.test(href)
/**
* Rewrites repo-relative markdown links so RFP detail pages don't 404. Upstream
* RFP files cross-reference each other with paths relative to the repo's
* `RFPs/` directory (e.g. `./RFP-008-lending-borrowing-protocol.md`), which the
* browser would otherwise resolve against the page URL and 404.
*
* - `RFP-NNN-*.md` references become the internal detail route
* (`/builders-hub/rfps/<slug>`), using the same slug the pages are built with.
* - Other repo-relative `.md` links (e.g. `../appendix/…`) have no site page, so
* they resolve to their absolute GitHub URL (anchors preserved).
* - Absolute, external, and anchor links are left untouched.
*
* `fileHtmlUrl` is the GitHub `blob` URL of the file being parsed; it anchors
* the resolution of the remaining relative links.
*/
export const rewriteRfpMarkdownLinks = (
markdown: string,
fileHtmlUrl: string
): string =>
markdown.replace(MARKDOWN_LINK_TARGET, (match, rawTarget: string) => {
const href = rawTarget.replace(/^<|>$/g, '')
if (isAbsoluteOrAnchor(href)) return match
const [path] = href.split('#')
if (!/\.md$/i.test(path)) return match
const filename = path.split('/').pop() ?? ''
if (/^RFP-\d+/i.test(filename)) {
return `](${ROUTES.rfps}/${toSlug(filename, filename)})`
}
try {
return `](${new URL(href, fileHtmlUrl).href})`
} catch {
return match
}
})
const firstMatch = (raw: string, patterns: RegExp[]): string | undefined => {
for (const pattern of patterns) {
const value = raw.match(pattern)?.[1]?.replace(/[`*]/g, '').trim()
@@ -169,7 +214,7 @@ const parseRfpMarkdown = (
status,
tier: tier ?? '',
githubUrl: htmlUrl,
rawMarkdown: content.trim(),
rawMarkdown: rewriteRfpMarkdownLinks(content.trim(), htmlUrl),
}
}