خواندنِ خبر موبایل
پیشنمایش تمامصفحهنوارِ بالا با ذخیره و اشتراک و نوارِ پیشرفتِ خواندن، تیترِ درشت، نویسنده با دنبال کن، عکس با منبع، نقلقولِ برجسته و نوارِ شناورِ پسند، نظر و اشتراک.
۱. نصب کامپوننتها
این بلاک از ۶ آیتم دیگ استفاده میکند: avatarbuttoncontrol-sizeiconspersiantoast
پکیجمنیجر پیشفرض Node.jsنصب سریع با لینک سخت و دیسک مشترکYarn نسخهٔ ۲ به بالا (Berry)رانتایم و پکیجمنیجر Bun
npx digdesign@latest add https://docs.digdesign.ir/r/avatar.json https://docs.digdesign.ir/r/button.json https://docs.digdesign.ir/r/control-size.json https://docs.digdesign.ir/r/icons.json https://docs.digdesign.ir/r/persian.json https://docs.digdesign.ir/r/toast.jsonpnpm dlx digdesign@latest add https://docs.digdesign.ir/r/avatar.json https://docs.digdesign.ir/r/button.json https://docs.digdesign.ir/r/control-size.json https://docs.digdesign.ir/r/icons.json https://docs.digdesign.ir/r/persian.json https://docs.digdesign.ir/r/toast.jsonyarn dlx digdesign@latest add https://docs.digdesign.ir/r/avatar.json https://docs.digdesign.ir/r/button.json https://docs.digdesign.ir/r/control-size.json https://docs.digdesign.ir/r/icons.json https://docs.digdesign.ir/r/persian.json https://docs.digdesign.ir/r/toast.jsonbunx --bun digdesign@latest add https://docs.digdesign.ir/r/avatar.json https://docs.digdesign.ir/r/button.json https://docs.digdesign.ir/r/control-size.json https://docs.digdesign.ir/r/icons.json https://docs.digdesign.ir/r/persian.json https://docs.digdesign.ir/r/toast.json۲. کد صفحه
کد را در app/<route>/page.tsx بگذارید و متنها، داده و هندلرها را با محصول خودتان عوض کنید.
mobile-news-article.tsx
"use client"
import * as React from "react"
import { ChatIcon, CheckIcon, ChevronRightIcon, HeartIcon, ShareIcon } from "@/components/icons"
import { Avatar } from "@/components/ui/avatar"
import { Button } from "@/components/ui/button"
import { ControlSizeProvider } from "@/components/ui/control-size"
import { toast } from "@/components/ui/toast"
import { toFaDigits } from "@/lib/persian"
import { stockImages } from "@/lib/stock-images"
/**
* خواندنِ خبر در موبایل: نوارِ بالا با بازگشت، ذخیره و اشتراک و نوارِ پیشرفتِ خواندن زیرش؛
* دسته و زیردسته، تیترِ درشت، نویسنده با «دنبال کن»، عکسِ اصلی با برچسبِ منبع، لیدِ پررنگ،
* نقلقولِ برجسته با خطِ رنگی، و نوارِ شناورِ واکنش (پسند، نظر، اشتراک) که پایینِ صفحه میماند.
*/
export default function MobileNewsArticleBlock() {
const [liked, setLiked] = React.useState(false)
const [saved, setSaved] = React.useState(false)
const [following, setFollowing] = React.useState(false)
const [progress, setProgress] = React.useState(12)
return (
<ControlSizeProvider size="xl" asChild>
<div
className="mx-auto flex h-svh w-full max-w-md flex-col bg-background"
onScroll={(event) => {
const element = event.target as HTMLElement
if (element.scrollHeight > element.clientHeight) setProgress(Math.round((element.scrollTop / (element.scrollHeight - element.clientHeight)) * 100))
}}
>
<header className="relative flex items-center gap-1 px-2 py-2">
<Button variant="ghost" size="icon" aria-label="بازگشت">
<ChevronRightIcon />
</Button>
<span className="flex-1" />
<Button variant="ghost" size="icon" aria-pressed={saved} aria-label={saved ? "برداشتن از ذخیرهها" : "ذخیره"} onClick={() => setSaved((value) => !value)} className={saved ? "text-[var(--dig-pink-600)]" : ""}>
<HeartIcon filled={saved} />
</Button>
<Button variant="ghost" size="icon" aria-label="اشتراکگذاری" onClick={() => toast.success("پیوند کپی شد.")}>
<ShareIcon />
</Button>
<span role="progressbar" aria-label="پیشرفتِ خواندن" aria-valuenow={progress} aria-valuemin={0} aria-valuemax={100} className="absolute inset-x-0 bottom-0 h-0.5 bg-muted">
<span className="block h-full bg-[var(--dig-orange-500)] transition-[width]" style={{ width: `${Math.max(progress, 4)}%` }} />
</span>
</header>
<main className="flex-1 overflow-y-auto pb-28">
<article className="flex flex-col gap-6 px-4 pt-5">
<div className="flex flex-col gap-3">
<span className="text-sm text-muted-foreground">
<span className="font-semibold text-brand-secondary">اقتصاد</span> · بازارِ طلا
</span>
<h1 className="text-2xl leading-snug font-black text-balance">بازارِ طلا در هفتهای که دلار آرام گرفت؛ حالا چه کنیم؟</h1>
<div className="flex items-center gap-3">
<Avatar alt="نگار احمدی" src={stockImages.personWoman3.src} className="size-10" />
<span className="flex min-w-0 flex-1 flex-col">
<span className="text-sm font-bold">نگار احمدی</span>
<span className="text-xs text-muted-foreground">۲ ساعت پیش · ۴ دقیقه مطالعه</span>
</span>
<ControlSizeProvider size="lg" asChild>
<Button variant={following ? "secondary" : "default"} aria-pressed={following} onClick={() => setFollowing((value) => !value)} className="rounded-full px-4 text-sm">
{following && <CheckIcon />}
{following ? "دنبال شد" : "دنبال کن"}
</Button>
</ControlSizeProvider>
</div>
</div>
<figure className="relative -mx-4">
{/* eslint-disable-next-line @next/next/no-img-element -- عکس از کتابخانهٔ تصویرِ دیگ */}
<img src={stockImages.coverFinanceDesk.src} alt={stockImages.coverFinanceDesk.alt} className="aspect-[4/3] w-full object-cover" />
<figcaption className="absolute start-4 bottom-4 rounded-full bg-[var(--dig-white)] px-3 py-1 text-xs font-bold text-[var(--dig-slate-900)]">عکس: آرشیوِ آوند</figcaption>
</figure>
<div className="flex flex-col gap-5 text-[1.0625rem] leading-8">
<p>
<strong>تهران</strong>، بازارِ طلا هفتهای آرام را پشتِ سر گذاشت؛ دلار در کانالِ ثابتی ماند و قیمتِ هر گرم طلای ۱۸ عیار نوسانِ کمی داشت. برای خریدارانِ خرد، این یعنی فرصتی برای تصمیمِ بیعجله.
</p>
<blockquote className="border-s-4 border-[var(--dig-orange-500)] ps-4">
<p className="leading-8 font-semibold">«خریدِ پلهای و منظم، از حدسزدنِ کف و سقفِ بازار مطمئنتر است.»</p>
<footer className="mt-2 text-sm text-muted-foreground">علی کاظمی، مشاورِ سرمایهگذاری</footer>
</blockquote>
<p>تحلیلگرها میگویند تا روشنشدنِ وضعِ نرخِ ارز، بهتر است بخشِ کوچکی از پسانداز ماهانه به طلا برود، نه همهٔ آن. ذخیره در حسابِ طلای بانکی هم هزینهٔ نگهداری را حذف میکند.</p>
<p>اگر تازه شروع کردهای، سقفِ ماهانه بگذار و آن را خودکار کن؛ اینطور خبرِ روزانه تصمیمت را عوض نمیکند.</p>
</div>
</article>
</main>
{/* نوارِ واکنش: شناور و گرد، بالای لبهٔ امنِ پایینِ صفحه. */}
<div className="pointer-events-none fixed inset-x-0 bottom-0 flex justify-center p-4 pb-[max(1rem,env(safe-area-inset-bottom))]">
<div className="pointer-events-auto flex items-center gap-1 rounded-full bg-[var(--dig-slate-900)] p-1 text-[var(--dig-white)] shadow-card dark:bg-[var(--dig-slate-700)]">
<Button
variant="ghost"
aria-pressed={liked}
aria-label={liked ? "برداشتنِ پسند" : "پسندیدن"}
onClick={() => setLiked((value) => !value)}
className="gap-1.5 rounded-full px-4 text-[var(--dig-white)] hover:bg-[var(--dig-slate-700)] hover:text-[var(--dig-white)] dark:hover:bg-[var(--dig-slate-600)]"
>
<HeartIcon filled={liked} className={liked ? "text-[var(--dig-pink-400)]" : ""} />
<span className="tabular-nums">{toFaDigits(liked ? 154 : 153)}</span>
</Button>
<Button variant="ghost" aria-label="نظرها" className="gap-1.5 rounded-full px-4 text-[var(--dig-white)] hover:bg-[var(--dig-slate-700)] hover:text-[var(--dig-white)] dark:hover:bg-[var(--dig-slate-600)]">
<ChatIcon />
<span className="tabular-nums">{toFaDigits(47)}</span>
</Button>
<Button size="icon" aria-label="اشتراکگذاری" onClick={() => toast.success("پیوند کپی شد.")} className="rounded-full bg-[var(--dig-orange-500)] text-[var(--dig-white)] hover:bg-[var(--dig-orange-600)]">
<ShareIcon />
</Button>
</div>
</div>
</div>
</ControlSizeProvider>
)
}