خانهٔ اپ سلامت
پیشنمایش تمامصفحهکارتهای رنگی با حلقهٔ پیشرفت، میلههای خواب و نمودار وزن، برنامهٔ روز و نوار ناوبری پایین.
۱. نصب کامپوننتها
این بلاک از ۸ آیتم دیگ استفاده میکند: app-baravatarbottom-navigationbuttoncontrol-sizeiconspersianstat-tile
پکیجمنیجر پیشفرض Node.jsنصب سریع با لینک سخت و دیسک مشترکYarn نسخهٔ ۲ به بالا (Berry)رانتایم و پکیجمنیجر Bun
npx digdesign@latest add https://docs.digdesign.ir/r/app-bar.json https://docs.digdesign.ir/r/avatar.json https://docs.digdesign.ir/r/bottom-navigation.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/stat-tile.jsonpnpm dlx digdesign@latest add https://docs.digdesign.ir/r/app-bar.json https://docs.digdesign.ir/r/avatar.json https://docs.digdesign.ir/r/bottom-navigation.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/stat-tile.jsonyarn dlx digdesign@latest add https://docs.digdesign.ir/r/app-bar.json https://docs.digdesign.ir/r/avatar.json https://docs.digdesign.ir/r/bottom-navigation.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/stat-tile.jsonbunx --bun digdesign@latest add https://docs.digdesign.ir/r/app-bar.json https://docs.digdesign.ir/r/avatar.json https://docs.digdesign.ir/r/bottom-navigation.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/stat-tile.json۲. کد صفحه
کد را در app/<route>/page.tsx بگذارید و متنها، داده و هندلرها را با محصول خودتان عوض کنید.
mobile-health.tsx
"use client"
import * as React from "react"
import {
ActivityIcon,
BarChartIcon,
BellIcon,
CalendarIcon,
CheckIcon,
DropletIcon,
FlameIcon,
HeartIcon,
MoonIcon,
} from "@/components/icons"
import { AppBar, AppBarActions, AppBarTitle } from "@/components/ui/app-bar"
import { ControlSizeProvider } from "@/components/ui/control-size"
import { Avatar } from "@/components/ui/avatar"
import { StatTileRing, StatTileSparkline } from "@/components/ui/stat-tile"
import { BottomNavigation, BottomNavigationItem } from "@/components/ui/bottom-navigation"
import { Button } from "@/components/ui/button"
import { formatNumber, toFaDigits } from "@/lib/persian"
/**
* شیار و میلههای غیرفعال با رنگِ میانی همان خانواده (ترکیبِ رنگِ اصلی با
* زمینهٔ کمرنگ)، نه سفید: سفید روی زمینهٔ رنگی روشن دیده نمیشود.
*/
const TRACK: Record<string, string> = {
orange: "[--track:color-mix(in_oklab,var(--color-orange)_28%,var(--color-orange-subtle))]",
cyan: "[--track:color-mix(in_oklab,var(--color-cyan)_28%,var(--color-cyan-subtle))]",
warning: "[--track:color-mix(in_oklab,var(--color-warning)_28%,var(--color-warning-subtle))]",
success: "[--track:color-mix(in_oklab,var(--color-success)_28%,var(--color-success-subtle))]",
}
/** حلقهٔ پیشرفت: همان StatTileRing با دو بخش (پیشرفت و باقیمانده). */
function Ring({ value, color, track }: { value: number; color: string; track: string }) {
return (
<StatTileRing
role="img"
aria-label={`${toFaDigits(value)} درصد از هدف`}
className="size-14 shrink-0"
segments={[
{ value, color },
{ value: 100 - value, color: track },
]}
/>
)
}
function Bars({ values, className }: { values: number[]; className: string }) {
const max = Math.max(...values)
return (
<div className={`flex h-14 items-end gap-1 ${className}`} aria-hidden>
{values.map((value, index) => (
<span
key={index}
// امروز (آخرین میله) پررنگ؛ بقیه رنگِ میانی.
className={index === values.length - 1 ? "w-1.5 rounded-full bg-current" : "w-1.5 rounded-full bg-(--track)"}
style={{ height: `${(value / max) * 100}%` }}
/>
))}
</div>
)
}
const TILES = [
{
title: "کالری",
value: formatNumber(1_839),
unit: "کیلوکالری",
icon: <FlameIcon />,
tone: `bg-orange-subtle text-orange ${TRACK.orange}`,
chart: <Ring value={72} color="var(--orange)" track="color-mix(in oklab, var(--orange) 26%, var(--orange-subtle))" />,
},
{
title: "آب",
value: formatNumber(1_200),
unit: "میلیلیتر",
icon: <DropletIcon />,
tone: `bg-cyan-subtle text-cyan ${TRACK.cyan}`,
chart: <Ring value={60} color="var(--cyan)" track="color-mix(in oklab, var(--cyan) 26%, var(--cyan-subtle))" />,
},
{
title: "خواب",
value: toFaDigits("7:30"),
unit: "ساعت",
icon: <MoonIcon />,
tone: `bg-warning-subtle text-warning ${TRACK.warning}`,
chart: <Bars values={[5, 6.5, 6, 8, 5.5, 7, 7.5]} className="text-warning" />,
},
{
title: "وزن",
value: formatNumber(68),
unit: "کیلوگرم",
icon: <ActivityIcon />,
tone: `bg-success-subtle text-success ${TRACK.success}`,
chart: <StatTileSparkline values={[70, 69.6, 69.8, 69.1, 68.7, 68.9, 68.2, 68]} className="h-14 w-24" />,
},
]
const PLAN = [
{ time: "۰۷:۳۰", title: "پیادهروی صبح", detail: "۳۰ دقیقه · ۲٫۵ کیلومتر", done: true },
{ time: "۱۳:۰۰", title: "ناهار سبک", detail: "۶۵۰ کیلوکالری", done: true },
{ time: "۱۸:۳۰", title: "یوگا", detail: "۴۵ دقیقه", done: false },
]
export default function MobileHealthBlock() {
const [tab, setTab] = React.useState("activity")
return (
// قابِ اپ: در موبایل تمامصفحه، در دسکتاپ ستونی به عرضِ گوشی.
<ControlSizeProvider size="xl" asChild>
<div className="mx-auto flex min-h-svh w-full max-w-md flex-col bg-background">
<AppBar sticky>
<Avatar alt="آرمیتا رستگار" size="sm" />
<AppBarTitle>سلام آرمیتا</AppBarTitle>
<AppBarActions>
<Button variant="ghost" size="icon" aria-label="اعلانها">
<BellIcon />
</Button>
</AppBarActions>
</AppBar>
<main className="flex flex-1 flex-col gap-5 p-4">
<section aria-labelledby="today" className="flex flex-col gap-3">
<div className="flex items-baseline justify-between">
<h1 id="today" className="text-lg font-bold">
امروز
</h1>
<span className="text-sm text-muted-foreground">۷۲٪ هدف روزانه</span>
</div>
<div className="grid grid-cols-2 gap-3">
{TILES.map((tile) => (
<article key={tile.title} className={`flex flex-col gap-3 rounded-xl p-4 ${tile.tone}`}>
<div className="flex items-center gap-2 text-foreground">
<span aria-hidden className={`[&_svg]:size-5 ${tile.tone.split(" ")[1]}`}>
{tile.icon}
</span>
<h2 className="font-semibold">{tile.title}</h2>
</div>
<div className="flex items-end justify-between gap-2">
<div className="flex flex-col text-foreground">
<span className="text-2xl font-bold">{tile.value}</span>
<span className="text-xs text-muted-foreground">{tile.unit}</span>
</div>
{tile.chart}
</div>
</article>
))}
</div>
</section>
<section aria-labelledby="plan" className="flex flex-col gap-3">
<h2 id="plan" className="font-bold">
برنامهٔ امروز
</h2>
<ol className="flex flex-col gap-2">
{PLAN.map((item) => (
<li key={item.title} className="flex items-center gap-3 rounded-xl border border-border p-3">
{/* تیک برای انجامشده؛ متن خط نمیخورد تا خوانا بماند. */}
<span
className={
item.done
? "flex size-8 shrink-0 items-center justify-center rounded-full bg-success-subtle text-success [&_svg]:size-4"
: "size-8 shrink-0 rounded-full border-2 border-dashed border-border"
}
role="img"
aria-label={item.done ? "انجام شد" : "انجام نشده"}
>
{item.done && <CheckIcon />}
</span>
<div className="flex min-w-0 flex-1 flex-col">
<span className="font-medium">{item.title}</span>
<span className="text-xs text-muted-foreground">{item.detail}</span>
</div>
<span className="shrink-0 text-sm text-muted-foreground">{item.time}</span>
</li>
))}
</ol>
</section>
</main>
<BottomNavigation value={tab} onValueChange={setTab} indicator="pill" fixed={false} className="sticky bottom-0">
<BottomNavigationItem value="activity" icon={<ActivityIcon />} label="فعالیت" />
<BottomNavigationItem value="fitness" icon={<HeartIcon />} label="تمرین" />
<BottomNavigationItem value="plans" icon={<CalendarIcon />} label="برنامه" />
<BottomNavigationItem value="progress" icon={<BarChartIcon />} label="پیشرفت" />
</BottomNavigation>
</div>
</ControlSizeProvider>
)
}