SEED Design

Overview

index.tsx
import { useState } from "react";
import { type ActivityComponentType } from "@stackflow/react/future";
import { AppBar, AppBarMain } from "seed-design/ui/app-bar";
import { AppScreen, AppScreenContent } from "seed-design/ui/app-screen";
import { TabsRoot, TabsTrigger, TabsList } from "seed-design/ui/tabs";
import { ErrorState } from "seed-design/ui/error-state";
import { SnackbarProvider } from "seed-design/ui/snackbar";
import { Recommendations } from "@/examples/react/demo/tabs/recommendations";

declare module "@stackflow/config" {
  interface Register {
    "react/demo/index": unknown;
  }
}

const TABS = [
  { label: "추천", value: "recommendations" },
  { label: "구독", value: "subscriptions" },
] as const satisfies {
  label: string;
  value: string;
}[];

type Tab = (typeof TABS)[number]["value"];

const DemoActivity: ActivityComponentType<"react/demo/index"> = () => {
  const [tab, setTab] = useState<Tab>("recommendations");

  return (
    <SnackbarProvider>
      <style
        // biome-ignore lint/security/noDangerouslySetInnerHtml: this is for hiding scrollbar
        dangerouslySetInnerHTML={{
          __html: "::-webkit-scrollbar{display:none}",
        }}
      />
      <AppScreen>
        <AppBar tone="layer">
          <AppBarMain title="Demo" />
        </AppBar>
        <AppScreenContent>
          <TabsRoot
            value={tab}
            onValueChange={(value) => setTab(value as Tab)}
            triggerLayout="fill"
            size="medium"
            stickyList
            style={{ height: "100%", overflowY: "auto" }}
          >
            <TabsList>
              {TABS.map(({ label, value }) => (
                <TabsTrigger key={value} value={value}>
                  {label}
                </TabsTrigger>
              ))}
            </TabsList>
            {tab === "recommendations" && <Recommendations />}
            {tab === "subscriptions" && (
              <ErrorState
                title="구독한 글이 없습니다."
                description="추천 글을 확인해보세요."
                primaryActionProps={{
                  children: "추천 글 보기",
                  onClick: () => setTab("recommendations"),
                }}
              />
            )}
          </TabsRoot>
        </AppScreenContent>
      </AppScreen>
    </SnackbarProvider>
  );
};

export default DemoActivity;
article-detail.tsx
import { CATEGORIES, type Article } from "@/examples/react/demo/data";
import type { ActivityComponentType } from "@stackflow/react/future";
import { AppScreen, AppScreenContent } from "seed-design/ui/app-screen";
import {
  AppBar,
  AppBarBackButton,
  AppBarCloseButton,
  AppBarRight,
  AppBarLeft,
} from "seed-design/ui/app-bar";
import { VStack, HStack, Box } from "@seed-design/react";
import { Text } from "@seed-design/react";
import { Badge } from "@seed-design/react";
import { SegmentedControl, SegmentedControlItem } from "seed-design/ui/segmented-control";
import { Callout } from "seed-design/ui/callout";
import { TextField, TextFieldTextarea } from "seed-design/ui/text-field";
import { ErrorState } from "seed-design/ui/error-state";
import { ActionButton } from "seed-design/ui/action-button";
import { Skeleton } from "@seed-design/react";
import { IconILowercaseSerifCircleFill } from "@karrotmarket/react-monochrome-icon";
import { ArticleAuthor } from "./components/article-author";
import { formatDate } from "@/examples/react/demo/utils/date";
import { useState } from "react";

import img from "@/public/penguin.webp";

declare module "@stackflow/config" {
  interface Register {
    "react/demo/article-detail": {
      article: Article;
    };
  }
}

const SEGMENTS = [
  { value: "popular", label: "인기" },
  { value: "latest", label: "최근" },
] as const satisfies { value: string; label: string }[];

const DemoArticleDetail: ActivityComponentType<"react/demo/article-detail"> = ({
  params: { article },
}: {
  // XXX: this should be inferred
  params: { article: Article };
}) => {
  const categoryName = CATEGORIES.find((c) => c.id === article.categoryId)?.name;
  const [isImageLoading, setIsImageLoading] = useState(true);

  return (
    <AppScreen layerOffsetTop="none">
      <AppBar tone="transparent">
        <AppBarLeft>
          <AppBarBackButton />
        </AppBarLeft>
        <AppBarRight>
          <AppBarCloseButton />
        </AppBarRight>
      </AppBar>
      <AppScreenContent>
        <VStack gap="x4">
          <Box style={{ aspectRatio: "1 / 1", position: "relative" }}>
            <img
              src={img.src}
              alt="penguin"
              onLoad={() => setIsImageLoading(false)}
              style={{ position: "absolute", zIndex: 1 }}
            />
            {isImageLoading && <Skeleton width="full" height="full" radius="0" />}
          </Box>
          <VStack gap="x6" pb="x4">
            <VStack px="spacingX.globalGutter" gap="spacingY.componentDefault" align="flex-start">
              {article.isPopular && (
                <Badge variant="outline" tone="brand" size="large">
                  인기
                </Badge>
              )}
              <VStack gap="x1">
                <Text as="h1" textStyle="t7Bold" color="fg.neutral">
                  {article.title}
                </Text>
                <Text
                  as="p"
                  textStyle="articleBody"
                  color="fg.neutralMuted"
                  style={{ wordBreak: "keep-all" }}
                >
                  {article.content}
                </Text>
              </VStack>
              <HStack width="full" align="center">
                <ArticleAuthor author={article.author} />
                <Text textStyle="t2Regular" color="fg.neutralMuted">
                  {categoryName} ⸱ {formatDate(article.createdAt)}
                </Text>
              </HStack>
            </VStack>
            <VStack px="spacingX.globalGutter" gap="spacingY.componentDefault">
              <Callout
                tone="neutral"
                description="따뜻한 댓글을 남겨주세요."
                prefixIcon={<IconILowercaseSerifCircleFill />}
              />
              <SegmentedControl
                aria-label="댓글 정렬 방식"
                defaultValue={SEGMENTS[0].value}
                style={{ width: "100%" }}
              >
                {SEGMENTS.map((tab) => (
                  <SegmentedControlItem key={tab.value} value={tab.value}>
                    {tab.label}
                  </SegmentedControlItem>
                ))}
              </SegmentedControl>
              <Box py="x3">
                <ErrorState title="댓글 없음" description="댓글이 없습니다." />
              </Box>
              <TextField label="댓글" maxGraphemeCount={200}>
                <TextFieldTextarea placeholder="저는…" />
              </TextField>
              <ActionButton>게시</ActionButton>
            </VStack>
          </VStack>
        </VStack>
      </AppScreenContent>
    </AppScreen>
  );
};

export default DemoArticleDetail;

Last updated on