بلاک‌ها · موبایل

خانهٔ اپ سلامت

پیش‌نمایش تمام‌صفحه

کارت‌های رنگی با حلقهٔ پیشرفت، میله‌های خواب و نمودار وزن، برنامهٔ روز و نوار ناوبری پایین.

۱. نصب کامپوننت‌ها

این بلاک از ۸ آیتم دیگ استفاده می‌کند: 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.json
pnpm 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.json
yarn 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.json
bunx --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>
  )
}