Takazudo Modular Styleguide

Shared/ArticleListCard

As H3
Default
Guide Alignment
With Updated Date
Without Badge

Component Source

article-list-card.tsx

import type { ComponentChildren } from 'preact';
import CategoryBadge from '@/src/components/shared/category-badge';
import type { CuratedTag } from '@/src/lib/home/curated-tags';
import type { Locale } from '@/lib/i18n/types';

type HeadingLevel = 'h2' | 'h3';
type Align = 'stretch' | 'start' | 'center';
type Size = 'default' | 'lead';

export interface ArticleListCardProps {
  href: string;
  ariaLabel?: string;
  /** Pre-rendered square thumbnail. Caller-specific (ItemVisual / ResponsiveImage / <picture>). */
  thumb: ComponentChildren;
  /** Optional curated-tag → a colored CategoryBadge above the title. */
  curatedTag?: CuratedTag;
  locale?: Locale;
  title: ComponentChildren;
  /** Already-formatted created date (e.g. via `formatDate`); shown with a 作成:/Created: prefix. */
  createdDate?: string;
  /** Already-formatted updated date; appended as `| 更新:/Updated:` when present. */
  updatedDate?: string;
  /** Pre-rendered excerpt / description text. */
  excerpt?: ComponentChildren;
  /** Pre-rendered tag list. Caller-specific (TagList / ArrowTag list). */
  tags?: ComponentChildren;
  /** Heading level for the title. Defaults to `h2` (use `h3` under a higher heading, e.g. the home news lead). */
  as?: HeadingLevel;
  /** Cross-axis alignment of the thumb cell. Defaults to `start` (pinned to the top). */
  thumbAlign?: Align;
  /** Cross-axis alignment of the meta cell. Defaults to `center` (vertically centered against the thumb). */
  contentAlign?: Align;
  /**
   * Visual scale. `default` = the compact `/notes/`-style row. `lead` = the home
   * news magazine's featured item: a wider thumb track, larger title/excerpt, and
   * a bigger category badge — but the SAME two-row structure (thumb+meta row, then
   * a full-width excerpt row) so it reads cleanly at narrow widths instead of
   * cramming the excerpt into a narrow column beside a tall thumb.
   */
  size?: Size;
}

const ALIGN_CLASS: Record<Align, string> = {
  stretch: '',
  start: 'self-start',
  center: 'self-center',
};

const SIZE_CLASS: Record<
  Size,
  { row: string; pad: string; badge: 'sm' | 'lg'; date: string; title: string; excerpt: string }
> = {
  default: {
    row: 'grid-cols-[80px_1fr] md:grid-cols-[120px_1fr] gap-x-hgap-sm',
    pad: 'pt-vgap-sm',
    badge: 'sm',
    date: 'font-futura font-thin text-xs lg:text-sm',
    title: 'font-futura text-sm md:text-base underline decoration-2 line-clamp-2',
    excerpt: 'font-thin text-xs md:text-sm xl:text-base line-clamp-2',
  },
  lead: {
    row: 'grid-cols-[100px_1fr] sm:grid-cols-[140px_1fr] md:grid-cols-[200px_1fr] gap-x-hgap-sm md:gap-x-hgap-md',
    pad: 'pt-vgap-md',
    badge: 'lg',
    date: 'font-futura font-thin text-sm md:text-base',
    title: 'font-futura text-lg md:text-xl underline decoration-2 line-clamp-2',
    excerpt: 'font-thin text-sm md:text-base line-clamp-3',
  },
};

/**
 * Shared article/news list card. One row = `[square thumb | badge · title · date]`,
 * with the description + tags on a full-width row below.
 *
 * SSR-safe by construction: a bare `<a>` (no hook-based `Link`), CategoryBadge, and
 * plain elements only. The thumbnail and tag list are passed in as slots so each
 * surface renders its own — the home news magazine needs `ItemVisual`'s restock-gallery
 * / news-no logic, while the `/notes/` and guide-series listings just need a single
 * image. This is the single source of truth for the badge → title → 作成-date → excerpt
 * → tags layout shared by all three.
 */
export function ArticleListCard({
  href,
  ariaLabel,
  thumb,
  curatedTag,
  locale = 'ja',
  title,
  createdDate,
  updatedDate,
  excerpt,
  tags,
  as: Heading = 'h2',
  thumbAlign = 'start',
  contentAlign = 'center',
  size = 'default',
}: ArticleListCardProps) {
  const isEn = locale === 'en';
  const sz = SIZE_CLASS[size];
  return (
    <a
      href={href}
      aria-label={ariaLabel}
      class="group block no-underline zd-invert-color-link border-t border-zd-white"
    >
      {/* 1st row: square thumb + (badge / title / date) */}
      <div class={`grid ${sz.row} ${sz.pad}`}>
        <div class={ALIGN_CLASS[thumbAlign]}>{thumb}</div>
        <div class={ALIGN_CLASS[contentAlign]}>
          {/* First line: category badge with the date to its right (no explicit
              color on the date → inherits currentColor so it flips on hover/focus). */}
          {(curatedTag || createdDate) && (
            <div class="flex flex-wrap items-center gap-hgap-xs pb-vgap-2xs">
              {curatedTag && <CategoryBadge tag={curatedTag} locale={locale} size={sz.badge} />}
              {createdDate && (
                <span class={sz.date}>
                  {isEn ? `Created: ${createdDate}` : `作成: ${createdDate}`}
                  {updatedDate && (
                    <span class="text-zd-subtext">
                      {` | ${isEn ? `Updated: ${updatedDate}` : `更新: ${updatedDate}`}`}
                    </span>
                  )}
                </span>
              )}
            </div>
          )}
          <Heading class={sz.title}>{title}</Heading>
        </div>
      </div>
      {/* 2nd row: description + tags, full width */}
      {(excerpt || tags) && (
        <div class="pt-vgap-xs pb-vgap-xs">
          {excerpt && <p class={sz.excerpt}>{excerpt}</p>}
          {tags && <div class="pt-vgap-xs">{tags}</div>}
        </div>
      )}
    </a>
  );
}

export default ArticleListCard;

Story Source

article-list-card.stories.tsx

import ArticleListCard from './article-list-card';
import type { CuratedTag } from '@/src/lib/home/curated-tags';

/**
 * ArticleListCard
 *
 * The shared article/news list card: `[square thumb | badge · title · 作成-date]`
 * on the first row, with the excerpt + tags on a full-width row below. SSR-safe
 * (bare `<a>`); the thumbnail and tag list are passed in as slots. Used by the home
 * news magazine ("rest" rows), the `/notes/` listing, and the guide-series listing.
 */
export const meta = {
  title: 'Shared/ArticleListCard',
};

const sampleThumb = <div class="aspect-square overflow-hidden border border-zd-white bg-zd-gray" />;

const sampleTags = (
  <ul class="flex flex-wrap gap-vgap-2xs text-xs">
    {['#発表/告知', '#ADDAC System', '#Modular Synthesizer'].map((t) => (
      <li key={t} class="inline-flex whitespace-nowrap">
        {t}
      </li>
    ))}
  </ul>
);

const newsTag: CuratedTag = { key: 'news', label: 'お知らせ', labelEn: 'News' };
const guideTag: CuratedTag = { key: 'guide', label: 'ガイド', labelEn: 'Guide' };

/**
 * Default — badge + title + 作成 date, excerpt + tags full width
 */
export const Default = () => (
  <ArticleListCard
    href="#"
    thumb={sampleThumb}
    curatedTag={newsTag}
    title="Notes009: ADDAC System: ADDAC120s Four Strings シリーズ 発表!"
    createdDate="2026/06/21"
    excerpt="ADDAC Systemより発表された、フィジカルな弦をEurorackに持ち込むモジュール群「ADDAC120s Four Strings」シリーズの紹介です。"
    tags={sampleTags}
  />
);

/**
 * Without a curated-tag badge (title leads)
 */
export const WithoutBadge = () => (
  <ArticleListCard
    href="#"
    thumb={sampleThumb}
    title="Notes008: SHIK S32 発表!"
    createdDate="2026/06/05"
    excerpt="SHIKより発表されたスタンドアロンMIDIプロセッサー/マクロコントローラーS32の紹介テキストの日本語訳です。"
    tags={sampleTags}
  />
);

/**
 * With an updated date (appends `| 更新:`)
 */
export const WithUpdatedDate = () => (
  <ArticleListCard
    href="#"
    thumb={sampleThumb}
    curatedTag={guideTag}
    title="MetaModuleガイド EP.1: Richard Devineが語るモジュラーとの歩みとMetaModule"
    createdDate="2026/06/25"
    updatedDate="2026/06/28"
    excerpt="4ms公式ポッドキャスト第1回のゲストは、アトランタ拠点のエレクトロニックアーティスト、リチャード・ディヴァイン。"
    tags={sampleTags}
  />
);

/**
 * Guide-series alignment — thumb pinned top, meta block centered (thumbAlign/contentAlign)
 */
export const GuideAlignment = () => (
  <ArticleListCard
    href="#"
    thumb={sampleThumb}
    curatedTag={guideTag}
    title="OXI ONE MKII 解説 EP.1: 全体像を把握する"
    createdDate="2026/03/05"
    excerpt="OXI ONE MKIIの解説シリーズです。EP.1では、OXI ONE MKIIの全体像と基本的な操作フローを解説します。"
    tags={sampleTags}
    thumbAlign="start"
    contentAlign="center"
  />
);

/**
 * As `h3` — used by the home news magazine (the lead item is the column's h2)
 */
export const AsH3 = () => (
  <ArticleListCard
    href="#"
    thumb={sampleThumb}
    curatedTag={newsTag}
    title="Notes007: OAM Time Machine ファームウェアアップグレード手順"
    createdDate="2026/05/10"
    excerpt="OAM Time MachineのファームウェアをV1.1.1にアップデートする手順をまとめました。"
    tags={sampleTags}
    as="h3"
  />
);