Takazudo Modular Styleguide

Article/Typography

All Elements
Blockquotes
Heading With Anchor
Headings
Horizontal Rule
Lists
Text Formatting

Component Source

h2.tsx

import { isValidElement, type FunctionComponent, type ComponentChildren } from 'preact';
import { Bookmark } from '@/components/icons/bookmark';

interface H2Props {
  children?: ComponentChildren;
  id?: string;
}

/** Extract text from React nodes, including Astro MDX's props.value wrapper pattern. */
function extractNodeText(node: ComponentChildren): string {
  if (typeof node === 'string') return node;
  if (typeof node === 'number') return String(node);
  if (Array.isArray(node)) return node.map(extractNodeText).join('');
  if (isValidElement(node)) {
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    const props = node.props as any;
    if (typeof props?.value === 'string') return props.value;
    if (props?.children != null) return extractNodeText(props.children);
  }
  return '';
}

const H2: FunctionComponent<H2Props> = ({ children, id }) => {
  // Astro MDX wraps heading text in a component with props.value instead of children.
  // Use the rehype-slug-generated id as reliable TOC detection; extractNodeText
  // covers any other environment where children may differ.
  const childText = extractNodeText(children).trim();
  if (childText === 'TOC' || id === 'toc') {
    return (
      <div className="zd-toc-pointer relative">
        <Bookmark
          className={
            'absolute top-[10px] md:top-[15px] lg:top-0 scale-50 md:scale-75 lg:scale-100 origin-top-right right-[20px] lg:right-auto lg:left-[-60px]'
          }
        />
      </div>
    );
  }
  return (
    <h2
      id={id}
      className="group text-base md:text-lg sm:text-xl pb-vgap-sm font-bold font-futura ml-[-1.5em] pl-[1.5em] clear-both"
    >
      <span className="block border-t-1 border-zd-white will-change-[transform]">
        <span
          className={'inline-block border-t-8 border-zd-white pt-vgap-sm mt-[-1px] min-w-[30%]'}
        >
          <span className="block relative">
            {children}
            {id && (
              <span className={'inline-block w-0 h-0 relative align-bottom'}>
                <a
                  href={`#${id}`}
                  aria-hidden="true"
                  className={
                    'font-bold hidden no-underline text-zd-white text-sm sm:text-base md:text-xl hover:text-white group-hover:block absolute left-0 bottom-0 px-[.4em]'
                  }
                >
                  #
                </a>
              </span>
            )}
          </span>
        </span>
      </span>
    </h2>
  );
};

export { H2 };

h3.tsx

import type { FunctionComponent, ComponentChildren } from 'preact';
interface H3Props {
  children?: ComponentChildren;
  id?: string;
}

/**
 * MDX h3 heading component
 * Matches Gatsby styling with gradient underline and hover anchor links
 */
const H3: FunctionComponent<H3Props> = ({ children, id }) => {
  return (
    <h3
      id={id}
      className={
        'text-sm sm:text-lg pt-vgap-sm pb-vgap-sm font-futura font-bold ml-[-1.5em] pl-[1.5em]'
      }
    >
      <span className="flow-root">
        <span className="block relative group">
          {/* Gradient underline */}
          <span
            className={
              'block mb-vgap-sm h-1px bg-linear-to-r from-zd-white to-zd-black [[data-component-column]_&]:hidden will-change-[transform]'
            }
          ></span>
          {children}
          {/* Hover anchor link */}
          {id && (
            <span className={'inline-block w-0 h-0 relative align-bottom'}>
              <a
                href={`#${id}`}
                aria-hidden="true"
                className={
                  'font-bold hidden no-underline text-zd-link absolute left-0 bottom-0 px-[.4em] group-hover:block'
                }
              >
                #
              </a>
            </span>
          )}
        </span>
      </span>
    </h3>
  );
};

export { H3 };

p.tsx

import { isValidElement, type FunctionComponent, type ComponentChildren } from 'preact';
import { Youtube } from '../mdx/youtube';
import { A } from './a';

interface PProps {
  children?: ComponentChildren;
}

/**
 * MDX paragraph component
 * - Auto-detects YouTube URLs and renders Youtube component
 * - Applies responsive typography and spacing matching Gatsby styling
 * - Note: mt-[-0.3em] compensates for default line-height to maintain visual rhythm
 */
const P: FunctionComponent<PProps> = ({ children }) => {
  // If the paragraph contains just a YouTube link, render it as a Youtube component
  if (isValidElement(children) && children.type === A) {
    const text = (children.props as { children?: ComponentChildren }).children;
    if (typeof text === 'string' && text.startsWith('https://youtu.be/')) {
      return <Youtube url={text} />;
    }
  }

  return <p className="text-sm md:text-base">{children}</p>;
};

export { P };

ul.tsx

import type { FunctionComponent, ComponentChildren } from 'preact';
interface UlProps {
  children?: ComponentChildren;
}

/**
 * MDX unordered list component
 * Matches Gatsby styling with proper spacing for nested lists
 */
const Ul: FunctionComponent<UlProps> = ({ children }) => {
  return (
    <ul
      className={
        'text-sm md:text-base list-disc flow-root pl-hgap-md [&>*+*]:mt-vgap-xs [&_ul]:ml-0 [&_ul]:mt-vgap-sm [&_ul]:pb-vgap-xs'
      }
    >
      {children}
    </ul>
  );
};

export { Ul };

ol.tsx

import type { FunctionComponent, ComponentChildren } from 'preact';
interface OlProps {
  children?: ComponentChildren;
  start?: number;
}

/**
 * MDX ordered list component
 * Matches Gatsby styling with proper spacing for nested lists
 */
const Ol: FunctionComponent<OlProps> = ({ children, start }) => {
  return (
    <ol
      start={start}
      className={
        'text-sm md:text-base list-decimal flow-root ml-hgap-md [&>*+*]:mt-vgap-xs [&_ol]:ml-hgap-sm [&_ol]:mt-vgap-sm [&_ol]:pb-vgap-xs'
      }
    >
      {children}
    </ol>
  );
};

export { Ol };

blockquote.tsx

import type { FunctionComponent, ComponentChildren } from 'preact';

interface BlockquoteProps {
  children?: ComponentChildren;
}

/**
 * MDX blockquote component
 * Port of the doc-site blockquote style (ContentBlockquote: 3px left border +
 * muted text), adapted for the main site:
 * - `font-style: italic` dropped — Noto Sans JP has no true italics, and
 *   synthetic oblique renders poorly for Japanese text.
 * - Text muted via `text-zd-white/80` instead of `text-zd-gray` — the gray
 *   token (p6) is ~3.6:1 on the article background, below WCAG AA for the
 *   long quoted passages some articles carry (e.g. col003-poemer).
 */
const Blockquote: FunctionComponent<BlockquoteProps> = ({ children }) => {
  return (
    <blockquote className="border-l-[3px] border-zd-gray pl-hgap-md text-zd-white/80">
      {children}
    </blockquote>
  );
};

export { Blockquote };

Story Source

typography.stories.tsx

import { H2 } from './h2';
import { H3 } from './h3';
import { P } from './p';
import { Ul } from './ul';
import { Ol } from './ol';
import { Blockquote } from './blockquote';

/**
 * Article Typography Components
 *
 * These components are used in MDX article content to provide
 * consistent styling for headings, paragraphs, lists, and text formatting.
 *
 * H4-H6, strong, em, code, hr are styled via rehype-article-elements plugin
 * at build time instead of React component wrappers.
 *
 * Features:
 * - Responsive typography (mobile/tablet/desktop)
 * - Consistent spacing and visual rhythm
 * - Anchor links for headings with IDs
 * - Proper list nesting styles
 */
export const meta = {
  title: 'Article/Typography',
};

/**
 * All typography elements
 */
export const AllElements = () => (
  <div className="max-w-[800px]">
    <H2>見出し2 (H2) - Main Section</H2>
    <P>
      これは段落テキストです。モジュラーシンセサイザーは、独立したモジュールを組み合わせて音を作り出す電子楽器です。
      各モジュールはオシレーター、フィルター、アンプなどの機能を持ち、パッチケーブルで接続します。
    </P>

    <H3>見出し3 (H3) - Sub Section</H3>
    <P>
      Eurorack(ユーロラック)は最も普及しているモジュラーシンセサイザーの規格です。
      3Uの高さと電源規格が標準化されており、異なるメーカーのモジュールを組み合わせることができます。
    </P>

    <h4 className="font-futura font-bold pb-vgap-sm text-sm md:text-lg">
      見出し4 (H4) - Detail Section
    </h4>
    <P>
      <strong className="text-zd-strong font-bold">ボールドテキスト</strong>と
      <em className="italic">イタリックテキスト</em>
      を使って強調することができます。これらはMDXで**と*で囲むことで表現できます。
    </P>

    <h5 className="font-futura font-bold pb-vgap-sm text-sm md:text-base">見出し5 (H5)</h5>
    <P>見出し5は小さめのセクション見出しに使用します。</P>

    <h6 className="font-futura font-bold pb-vgap-sm text-sm">見出し6 (H6)</h6>
    <P>見出し6は最も小さい見出しレベルです。</P>

    <hr className="border-0 h-[4px] bg-zd-white my-vgap-md lg:my-vgap-lg" />

    <H3>リスト表示</H3>

    <h4 className="font-futura font-bold pb-vgap-sm text-sm md:text-lg">順序なしリスト (Ul)</h4>
    <Ul>
      <li>VCO (Voltage Controlled Oscillator) - 電圧制御発振器</li>
      <li>VCF (Voltage Controlled Filter) - 電圧制御フィルター</li>
      <li>VCA (Voltage Controlled Amplifier) - 電圧制御アンプ</li>
      <li>
        モジュレーション
        <Ul>
          <li>LFO (Low Frequency Oscillator)</li>
          <li>Envelope Generator</li>
        </Ul>
      </li>
    </Ul>

    <h4 className="font-futura font-bold pb-vgap-sm text-sm md:text-lg">順序付きリスト (Ol)</h4>
    <Ol>
      <li>電源を接続する</li>
      <li>VCOの出力をVCFに接続する</li>
      <li>VCFの出力をVCAに接続する</li>
      <li>VCAの出力をミキサーまたはオーディオインターフェースに接続する</li>
    </Ol>

    <H3>引用 (Blockquote)</H3>
    <Blockquote>
      <P>
        モジュラーシンセサイザーの魅力は、決まった音色や構成にとらわれず、自分だけのシステムを組み上げられることにあります。
      </P>
    </Blockquote>
  </div>
);

/**
 * Headings only
 */
export const Headings = () => (
  <div className="max-w-[800px]">
    <H2>H2 見出し - セクションタイトル</H2>
    <H3>H3 見出し - サブセクション</H3>
    <h4 className="font-futura font-bold pb-vgap-sm text-sm md:text-lg">
      H4 見出し - 詳細セクション
    </h4>
    <h5 className="font-futura font-bold pb-vgap-sm text-sm md:text-base">H5 見出し - 小見出し</h5>
    <h6 className="font-futura font-bold pb-vgap-sm text-sm">H6 見出し - 最小見出し</h6>
  </div>
);

/**
 * Heading with anchor link
 */
export const HeadingWithAnchor = () => (
  <div className="max-w-[800px]">
    <P>見出しにIDを設定すると、ホバー時にアンカーリンクが表示されます。</P>
    <H2 id="section-with-anchor">アンカー付き見出し</H2>
    <P>この見出しにマウスを乗せると # リンクが表示されます。</P>
  </div>
);

/**
 * Paragraphs and text formatting
 */
export const TextFormatting = () => (
  <div className="max-w-[800px]">
    <P>これは通常の段落テキストです。複数行にわたる長いテキストも適切な行間で表示されます。</P>
    <P>
      <strong className="text-zd-strong font-bold">Strong要素</strong>
      はテキストを太字にします。MDXでは**で囲みます。
    </P>
    <P>
      <em className="italic">Em要素</em>はテキストをイタリックにします。MDXでは*で囲みます。
    </P>
    <P>
      <strong className="text-zd-strong font-bold">
        <em className="italic">両方を組み合わせる</em>
      </strong>
      こともできます。
    </P>
  </div>
);

/**
 * Lists
 */
export const Lists = () => (
  <div className="max-w-[800px]">
    <H3>順序なしリスト</H3>
    <Ul>
      <li>アイテム1</li>
      <li>アイテム2</li>
      <li>
        ネストしたリスト
        <Ul>
          <li>サブアイテム1</li>
          <li>サブアイテム2</li>
        </Ul>
      </li>
      <li>アイテム4</li>
    </Ul>

    <H3>順序付きリスト</H3>
    <Ol>
      <li>ステップ1</li>
      <li>ステップ2</li>
      <li>ステップ3</li>
      <li>ステップ4</li>
    </Ol>
  </div>
);

/**
 * Horizontal rule
 */
export const HorizontalRule = () => (
  <div className="max-w-[800px]">
    <P>上のセクション</P>
    <hr className="border-0 h-[4px] bg-zd-white my-vgap-md lg:my-vgap-lg" />
    <P>下のセクション</P>
  </div>
);

/**
 * Blockquotes
 * The zd-prose-flow wrapper mirrors the article-body context, providing
 * inter-paragraph spacing inside multi-paragraph quotes.
 */
export const Blockquotes = () => (
  <div className="max-w-[800px] zd-prose-flow">
    <P>引用の前の段落です。MDXでは行頭に &gt; を置くことで引用になります。</P>
    <Blockquote>
      <P>
        Harmonizer、Scale Remap、Arpeggiator、Turing Machine、Polyphonic Envelope、Euclidean
        Sequencer、Looper、Delay、Randomizer、Filters、Mergers、Routersなどを使って、サウンドを形作ることができます。
      </P>
    </Blockquote>
    <P>複数の段落を含む引用の例:</P>
    <Blockquote>
      <P>そして一度あなたが自らの心を余すこと無く散策してしまったら</P>
      <P>あなたが支配するものは積み荷リストと同じぐらいに明白だ</P>
    </Blockquote>
    <P>リストを含む引用の例:</P>
    <Blockquote>
      <P>ファームウェア更新の手順:</P>
      <Ul>
        <li>Bootボタンを押し続ける</li>
        <li>Resetボタンを押し続ける</li>
        <li>Resetボタンを離す</li>
        <li>Bootボタンを離す</li>
      </Ul>
    </Blockquote>
    <P>引用の後の段落です。</P>
  </div>
);