feat: add Folio/IndexBox/Factbox/JumpLine P0 components, redesign Landing Page as full newspaper
This commit is contained in:
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
@@ -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>
|
||||
);
|
||||
Reference in New Issue
Block a user