Article/Typography
All Elements
Blockquotes
Heading With Anchor
Headings
Horizontal Rule
Lists
Text Formatting
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 };
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では行頭に > を置くことで引用になります。</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>
);