feat: add Folio/IndexBox/Factbox/JumpLine P0 components, redesign Landing Page as full newspaper

This commit is contained in:
sunzhongyi
2026-05-21 10:57:58 +08:00
parent e38372e34d
commit 6d29e1a3e6
8 changed files with 478 additions and 260 deletions
+10
View File
@@ -52,6 +52,16 @@ export type { PullQuoteProps } from './media/PullQuote';
export { RelatedArticles } from './layout/RelatedArticles';
export type { RelatedArticlesProps, RelatedArticle } from './layout/RelatedArticles';
// additional layout
export { Folio } from './layout/Folio';
export type { FolioProps } from './layout/Folio';
export { IndexBox } from './layout/IndexBox';
export type { IndexBoxProps, IndexItem } from './layout/IndexBox';
export { Factbox } from './layout/Factbox';
export type { FactboxProps } from './layout/Factbox';
// additional text
export { AuthorCard } from './text/AuthorCard';
export type { AuthorCardProps } from './text/AuthorCard';
export { JumpLine } from './text/JumpLine';
export type { JumpLineProps } from './text/JumpLine';
@@ -0,0 +1,83 @@
'use client';
import React, { ReactNode, CSSProperties } from 'react';
import { clampSpan, cx } from 'newspaperui-utils';
import { useSection } from './Section';
export interface FactboxProps {
title?: string;
span?: number;
variant?: 'default' | 'highlight' | 'timeline';
className?: string;
style?: CSSProperties;
children: ReactNode;
}
/**
* Factbox — 嵌入正文的信息框
*
* 用于放关键数据、时间线、人物简介等结构化信息。
* InDesign 中称为 "Anchored Object"。
*
* @example
* <Factbox title="Key Facts" variant="highlight">
* <ul><li>GDP growth: 3.2%</li></ul>
* </Factbox>
*/
export const Factbox: React.FC<FactboxProps> = ({
title, span, variant = 'default', className, style, children,
}) => {
const section = useSection();
const cols = span ? clampSpan(span, section.columns) : undefined;
const variantStyles: Record<string, CSSProperties> = {
default: {
border: '1px solid var(--nui-rule-hairline)',
borderTop: '3px solid var(--nui-rule-decorative)',
background: 'var(--nui-bg-surface)',
},
highlight: {
border: '1px solid var(--nui-rule-hairline)',
borderLeft: '4px solid var(--nui-accent-primary)',
background: 'var(--nui-bg-surface)',
},
timeline: {
border: '1px solid var(--nui-rule-hairline)',
borderTop: '3px solid var(--nui-accent-ink-blue)',
background: 'var(--nui-bg-surface)',
},
};
return (
<aside
className={cx('nui-factbox nui-avoid-break', className)}
style={{
...variantStyles[variant],
padding: 'var(--nui-space-4)',
gridColumn: cols ? `span ${cols}` : undefined,
...style,
}}
>
{title && (
<h4 style={{
fontFamily: 'var(--font-family-meta)',
fontSize: '11px',
fontWeight: 700,
fontVariantCaps: 'small-caps',
letterSpacing: '0.08em',
color: variant === 'highlight' ? 'var(--nui-accent-primary)' : 'var(--nui-text-primary)',
margin: '0 0 var(--nui-space-3) 0',
paddingBottom: 'var(--nui-space-2)',
borderBottom: '1px solid var(--nui-rule-hairline)',
}}>{title}</h4>
)}
<div style={{
fontFamily: 'var(--font-family-body)',
fontSize: '13px',
lineHeight: 1.5,
color: 'var(--nui-text-body)',
}}>
{children}
</div>
</aside>
);
};
+50
View File
@@ -0,0 +1,50 @@
'use client';
import React, { CSSProperties } from 'react';
import { cx } from 'newspaperui-utils';
export interface FolioProps {
page: string; // e.g. "A2"
section?: string; // e.g. "要闻" / "National"
date?: string; // e.g. "2026年5月21日"
publication?: string; // e.g. "人民周报"
align?: 'left' | 'center' | 'between';
className?: string;
style?: CSSProperties;
}
/**
* Folio — 版面页眉(页码 + 版名 + 日期)
*
* 报纸每个内页顶部的标识条,告诉读者当前在哪个版面。
*
* @example
* <Folio page="A2" section="要闻" date="2026年5月21日" publication="人民周报" />
*/
export const Folio: React.FC<FolioProps> = ({
page, section, date, publication, align = 'between', className, style,
}) => (
<div
className={cx('nui-folio nui-small-caps', className)}
style={{
gridColumn: '1 / -1',
display: 'flex',
justifyContent: align === 'between' ? 'space-between' : align === 'center' ? 'center' : 'flex-start',
alignItems: 'baseline',
gap: 'var(--nui-space-4)',
fontFamily: 'var(--font-family-meta)',
fontSize: '10px',
fontWeight: 500,
letterSpacing: '0.08em',
color: 'var(--nui-text-muted)',
borderBottom: '1px solid var(--nui-rule-hairline)',
paddingBottom: 'var(--nui-space-2)',
marginBottom: 'var(--nui-space-4)',
...style,
}}
>
<span style={{ fontWeight: 700, color: 'var(--nui-text-secondary)' }}>{page}</span>
{section && <span>{section}</span>}
{publication && <span>{publication}</span>}
{date && <span>{date}</span>}
</div>
);
@@ -0,0 +1,92 @@
'use client';
import React, { CSSProperties } from 'react';
import { cx } from 'newspaperui-utils';
export interface IndexItem {
page: string; // "A2"
title: string; // "经济" / "National"
headline?: string; // optional headline preview
}
export interface IndexBoxProps {
title?: string;
items: IndexItem[];
className?: string;
style?: CSSProperties;
}
/**
* IndexBox — 头版内容索引框
*
* 告诉读者今天各版有什么内容。通常放在头版右上角或底部。
*
* @example
* <IndexBox title="Inside" items={[
* { page: 'A2', title: 'Economy', headline: 'Markets rally...' },
* { page: 'B1', title: 'Sports', headline: 'Championship final...' },
* ]} />
*/
export const IndexBox: React.FC<IndexBoxProps> = ({
title = 'Inside', items, className, style,
}) => (
<div
className={cx('nui-index-box', className)}
style={{
border: '1px solid var(--nui-rule-hairline)',
borderTop: '3px solid var(--nui-rule-decorative)',
padding: 'var(--nui-space-3)',
background: 'var(--nui-bg-surface)',
...style,
}}
>
<h4 style={{
fontFamily: 'var(--font-family-meta)',
fontSize: '11px',
fontWeight: 700,
fontVariantCaps: 'small-caps',
letterSpacing: '0.08em',
color: 'var(--nui-accent-primary)',
margin: '0 0 var(--nui-space-2) 0',
paddingBottom: 'var(--nui-space-2)',
borderBottom: '1px solid var(--nui-rule-hairline)',
}}>{title}</h4>
<ul style={{ listStyle: 'none', padding: 0, margin: 0 }}>
{items.map((item, i) => (
<li key={i} style={{
display: 'flex',
gap: 'var(--nui-space-2)',
padding: 'var(--nui-space-1) 0',
borderBottom: i < items.length - 1 ? '1px solid var(--nui-rule-hairline)' : 'none',
alignItems: 'baseline',
}}>
<span style={{
fontFamily: 'var(--font-family-meta)',
fontSize: '10px',
fontWeight: 700,
color: 'var(--nui-text-secondary)',
flexShrink: 0,
width: '24px',
}}>{item.page}</span>
<span style={{
fontFamily: 'var(--font-family-meta)',
fontSize: '10px',
fontVariantCaps: 'small-caps',
letterSpacing: '0.06em',
color: 'var(--nui-text-muted)',
flexShrink: 0,
width: '48px',
}}>{item.title}</span>
{item.headline && (
<span style={{
fontFamily: 'var(--font-family-body)',
fontSize: '12px',
color: 'var(--nui-text-body)',
lineHeight: 1.3,
flex: 1,
}}>{item.headline}</span>
)}
</li>
))}
</ul>
</div>
);
+41
View File
@@ -0,0 +1,41 @@
'use client';
import React, { CSSProperties } from 'react';
import { cx } from 'newspaperui-utils';
export interface JumpLineProps {
direction: 'to' | 'from'; // 'to' = "续转第X版", 'from' = "接第X版"
page: string; // e.g. "A6"
className?: string;
style?: CSSProperties;
}
/**
* JumpLine — 跨版续转标注
*
* 报纸文章从一个版面跳转到另一个版面时的标注。
*
* @example
* <JumpLine direction="to" page="A6" />
* <JumpLine direction="from" page="A1" />
*/
export const JumpLine: React.FC<JumpLineProps> = ({
direction, page, className, style,
}) => (
<div
className={cx('nui-jump-line nui-small-caps', className)}
style={{
fontFamily: 'var(--font-family-meta)',
fontSize: '10px',
fontWeight: 600,
letterSpacing: '0.06em',
color: 'var(--nui-text-muted)',
textAlign: direction === 'to' ? 'right' : 'left',
marginTop: direction === 'to' ? 'var(--nui-space-3)' : '0',
marginBottom: direction === 'from' ? 'var(--nui-space-3)' : '0',
fontStyle: 'italic',
...style,
}}
>
{direction === 'to' ? `Continued on ${page}` : `← Continued from ${page}`}
</div>
);