feat: responsive system, engineering infra, new components, performance
- Section: responsive prop with media query injection - visual-weights: fontSize clamp() for responsive sizing - variables.css: add border-radius/shadow/transition/z-index tokens - ESLint flat config + Prettier + Changeset init - New components: Footer, NewsSidebar, BreakingNewsBanner - Image/Figure: loading=lazy, aspectRatio, sizes props
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@newspaperui/components",
|
||||
"version": "0.0.0",
|
||||
"description": "React components for newspaperui",
|
||||
"name": "newspaperui-components",
|
||||
"version": "0.1.0",
|
||||
"description": "Production-grade newspaper layout React components",
|
||||
"type": "module",
|
||||
"main": "./dist/index.cjs",
|
||||
"module": "./dist/index.js",
|
||||
@@ -29,8 +29,8 @@
|
||||
"react-dom": "^18.3.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@newspaperui/theme": "workspace:*",
|
||||
"@newspaperui/utils": "workspace:*"
|
||||
"newspaperui-theme": "workspace:*",
|
||||
"newspaperui-utils": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@testing-library/react": "^16.1.0",
|
||||
@@ -45,5 +45,23 @@
|
||||
"vite": "^5.4.11",
|
||||
"vite-plugin-dts": "^4.3.0",
|
||||
"vitest": "^2.1.8"
|
||||
},
|
||||
"license": "MIT",
|
||||
"author": "sunzhongyi",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/joisun/newspaperui.git"
|
||||
},
|
||||
"keywords": [
|
||||
"newspaper",
|
||||
"react",
|
||||
"components",
|
||||
"layout",
|
||||
"typography",
|
||||
"css-grid",
|
||||
"multi-column"
|
||||
],
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import '@newspaperui/theme';
|
||||
import 'newspaperui-theme';
|
||||
|
||||
// layout
|
||||
export { Layout, useLayout } from './layout/Layout';
|
||||
@@ -13,6 +13,12 @@ export { Masthead } from './layout/Masthead';
|
||||
export type { MastheadProps } from './layout/Masthead';
|
||||
export { Rule } from './layout/Rule';
|
||||
export type { RuleProps } from './layout/Rule';
|
||||
export { Footer } from './layout/Footer';
|
||||
export type { FooterProps } from './layout/Footer';
|
||||
export { Sidebar as NewsSidebar } from './layout/Sidebar';
|
||||
export type { SidebarProps as NewsSidebarProps } from './layout/Sidebar';
|
||||
export { BreakingNewsBanner } from './layout/BreakingNewsBanner';
|
||||
export type { BreakingNewsBannerProps } from './layout/BreakingNewsBanner';
|
||||
|
||||
// text
|
||||
export { Headline } from './text/Headline';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
import React, { ReactNode, CSSProperties } from 'react';
|
||||
import { clampSpan, cx } from '@newspaperui/utils';
|
||||
import { clampSpan, cx } from 'newspaperui-utils';
|
||||
import { useSection } from './Section';
|
||||
|
||||
export interface ArticleProps {
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
'use client';
|
||||
import React, { ReactNode, CSSProperties } from 'react';
|
||||
import { cx } from 'newspaperui-utils';
|
||||
|
||||
export interface BreakingNewsBannerProps {
|
||||
label?: string;
|
||||
className?: string;
|
||||
style?: CSSProperties;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* BreakingNewsBanner — 突发新闻横幅
|
||||
*
|
||||
* @example
|
||||
* <BreakingNewsBanner label="BREAKING">
|
||||
* Major earthquake strikes coastal region
|
||||
* </BreakingNewsBanner>
|
||||
*/
|
||||
export const BreakingNewsBanner: React.FC<BreakingNewsBannerProps> = ({
|
||||
label = 'BREAKING', className, style, children,
|
||||
}) => (
|
||||
<div
|
||||
className={cx('nui-breaking', className)}
|
||||
role="alert"
|
||||
style={{
|
||||
gridColumn: '1 / -1',
|
||||
background: 'var(--nui-accent-primary)',
|
||||
color: 'var(--nui-bg-page)',
|
||||
padding: 'var(--nui-space-3) var(--nui-space-6)',
|
||||
fontFamily: 'var(--font-family-meta)',
|
||||
fontSize: '13px',
|
||||
fontWeight: 600,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 'var(--nui-space-4)',
|
||||
...style,
|
||||
}}
|
||||
>
|
||||
<span style={{
|
||||
fontVariantCaps: 'small-caps',
|
||||
letterSpacing: '0.1em',
|
||||
fontWeight: 700,
|
||||
flexShrink: 0,
|
||||
}}>{label}</span>
|
||||
<span style={{ flex: 1 }}>{children}</span>
|
||||
</div>
|
||||
);
|
||||
@@ -0,0 +1,57 @@
|
||||
'use client';
|
||||
import React, { ReactNode, CSSProperties } from 'react';
|
||||
import { cx } from 'newspaperui-utils';
|
||||
|
||||
export interface FooterProps {
|
||||
copyright?: string;
|
||||
edition?: string;
|
||||
links?: Array<{ label: string; href: string }>;
|
||||
className?: string;
|
||||
style?: CSSProperties;
|
||||
children?: ReactNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Footer — 报纸页脚/版权信息区
|
||||
*
|
||||
* @example
|
||||
* <Footer copyright="© 2026 The Daily Chronicle" edition="Vol. CXLIX" />
|
||||
*/
|
||||
export const Footer: React.FC<FooterProps> = ({
|
||||
copyright, edition, links, className, style, children,
|
||||
}) => (
|
||||
<footer
|
||||
className={cx('nui-footer nui-small-caps', className)}
|
||||
style={{
|
||||
gridColumn: '1 / -1',
|
||||
borderTop: '2px solid var(--nui-rule-decorative)',
|
||||
paddingTop: 'var(--nui-space-4)',
|
||||
marginTop: 'var(--nui-space-8)',
|
||||
fontFamily: 'var(--font-family-meta)',
|
||||
fontSize: '11px',
|
||||
letterSpacing: '0.06em',
|
||||
color: 'var(--nui-text-muted)',
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
flexWrap: 'wrap',
|
||||
gap: 'var(--nui-space-4)',
|
||||
...style,
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
{copyright && <span>{copyright}</span>}
|
||||
{edition && <span style={{ marginLeft: 'var(--nui-space-4)' }}>{edition}</span>}
|
||||
</div>
|
||||
{links && (
|
||||
<nav style={{ display: 'flex', gap: 'var(--nui-space-4)' }}>
|
||||
{links.map(link => (
|
||||
<a key={link.href} href={link.href} style={{ color: 'var(--nui-text-muted)', textDecoration: 'none' }}>
|
||||
{link.label}
|
||||
</a>
|
||||
))}
|
||||
</nav>
|
||||
)}
|
||||
{children}
|
||||
</footer>
|
||||
);
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
import React from 'react';
|
||||
import { cx } from '@newspaperui/utils';
|
||||
import { cx } from 'newspaperui-utils';
|
||||
|
||||
export interface MastheadProps {
|
||||
title: string;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
import React, { CSSProperties } from 'react';
|
||||
import { cx } from '@newspaperui/utils';
|
||||
import { cx } from 'newspaperui-utils';
|
||||
|
||||
export interface RuleProps {
|
||||
variant?: 'hairline' | 'double' | 'thick';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
import React, { createContext, useContext, ReactNode, CSSProperties } from 'react';
|
||||
import { clampSpan, cx } from '@newspaperui/utils';
|
||||
import React, { createContext, useContext, useId, ReactNode, CSSProperties } from 'react';
|
||||
import { clampSpan, cx } from 'newspaperui-utils';
|
||||
import { useLayout } from './Layout';
|
||||
|
||||
export interface SectionProps {
|
||||
@@ -8,6 +8,7 @@ export interface SectionProps {
|
||||
gap?: string; // 默认 'var(--nui-gutter)'
|
||||
breakable?: boolean; // 是否允许 print 分页断开,默认 true
|
||||
divider?: 'none' | 'top' | 'bottom' | 'both'; // hairline 分隔
|
||||
responsive?: { sm?: number; md?: number; lg?: number }; // 响应式列数覆盖
|
||||
className?: string;
|
||||
style?: CSSProperties;
|
||||
children: ReactNode;
|
||||
@@ -32,10 +33,17 @@ export const useSection = () => useContext(SectionContext);
|
||||
*/
|
||||
export const Section: React.FC<SectionProps> = ({
|
||||
columns, gap = 'var(--nui-gutter)', breakable = true, divider = 'none',
|
||||
className, style, children,
|
||||
responsive, className, style, children,
|
||||
}) => {
|
||||
const layout = useLayout();
|
||||
const cols = clampSpan(columns, layout.columns);
|
||||
const id = useId().replace(/:/g, '');
|
||||
const sectionId = `nui-s-${id}`;
|
||||
|
||||
const responsiveCSS = responsive ? `
|
||||
@media (max-width: 768px) { .${sectionId} { grid-template-columns: repeat(${responsive.sm ?? 12}, 1fr) !important; } }
|
||||
@media (min-width: 769px) and (max-width: 1024px) { .${sectionId} { grid-template-columns: repeat(${responsive.md ?? 16}, 1fr) !important; } }
|
||||
` : '';
|
||||
|
||||
const dividerStyle: CSSProperties = {};
|
||||
if (divider === 'top' || divider === 'both')
|
||||
@@ -45,8 +53,9 @@ export const Section: React.FC<SectionProps> = ({
|
||||
|
||||
return (
|
||||
<SectionContext.Provider value={{ columns: cols }}>
|
||||
{responsiveCSS && <style dangerouslySetInnerHTML={{ __html: responsiveCSS }} />}
|
||||
<section
|
||||
className={cx('nui-section', className)}
|
||||
className={cx('nui-section', sectionId, className)}
|
||||
style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: `repeat(${cols}, 1fr)`,
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
'use client';
|
||||
import React, { ReactNode, CSSProperties } from 'react';
|
||||
import { clampSpan, cx } from 'newspaperui-utils';
|
||||
import { useSection } from './Section';
|
||||
|
||||
export interface SidebarProps {
|
||||
span?: number;
|
||||
position?: 'left' | 'right';
|
||||
divider?: boolean;
|
||||
className?: string;
|
||||
style?: CSSProperties;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sidebar — 侧边栏内容区(天气、股票、快讯摘要等)
|
||||
*
|
||||
* @example
|
||||
* <Section columns={24}>
|
||||
* <Article span={18}>...</Article>
|
||||
* <Sidebar span={6} position="right" divider>...</Sidebar>
|
||||
* </Section>
|
||||
*/
|
||||
export const Sidebar: React.FC<SidebarProps> = ({
|
||||
span, position = 'right', divider = false, className, style, children,
|
||||
}) => {
|
||||
const section = useSection();
|
||||
const cols = span ? clampSpan(span, section.columns) : 6;
|
||||
const borderStyle: CSSProperties = divider
|
||||
? position === 'left'
|
||||
? { borderRight: '1px solid var(--nui-rule-hairline)', paddingRight: 'var(--nui-space-4)' }
|
||||
: { borderLeft: '1px solid var(--nui-rule-hairline)', paddingLeft: 'var(--nui-space-4)' }
|
||||
: {};
|
||||
|
||||
return (
|
||||
<aside
|
||||
className={cx('nui-sidebar', className)}
|
||||
style={{
|
||||
gridColumn: `span ${cols}`,
|
||||
...borderStyle,
|
||||
...style,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</aside>
|
||||
);
|
||||
};
|
||||
@@ -1,17 +1,21 @@
|
||||
'use client';
|
||||
import React, { CSSProperties } from 'react';
|
||||
import { clampSpan, cx } from '@newspaperui/utils';
|
||||
import React, { ReactNode, CSSProperties } from 'react';
|
||||
import { clampSpan, cx } from 'newspaperui-utils';
|
||||
import { useSection } from '../layout/Section';
|
||||
import { Caption } from '../text/Caption';
|
||||
|
||||
export interface FigureProps {
|
||||
src: string;
|
||||
alt: string;
|
||||
src?: string;
|
||||
alt?: string;
|
||||
caption?: string;
|
||||
credit?: string;
|
||||
span?: number;
|
||||
loading?: 'lazy' | 'eager';
|
||||
aspectRatio?: string;
|
||||
sizes?: string;
|
||||
className?: string;
|
||||
style?: CSSProperties;
|
||||
children?: ReactNode;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -20,6 +24,7 @@ export interface FigureProps {
|
||||
* - 将图片与 Caption 组合为语义化 figure 元素
|
||||
* - 支持 caption 文字说明和 credit 来源标注
|
||||
* - 自动避免 print 分页断开(break-inside: avoid)
|
||||
* - 支持 children 自定义内容替代 img
|
||||
*
|
||||
* @example
|
||||
* <Figure
|
||||
@@ -28,10 +33,12 @@ export interface FigureProps {
|
||||
* caption="Downtown at dusk"
|
||||
* credit="Photo by John"
|
||||
* span={10}
|
||||
* aspectRatio="16/9"
|
||||
* />
|
||||
*/
|
||||
export const Figure: React.FC<FigureProps> = ({
|
||||
src, alt, caption, credit, span, className, style,
|
||||
src, alt, caption, credit, span, loading, aspectRatio, sizes,
|
||||
className, style, children,
|
||||
}) => {
|
||||
const section = useSection();
|
||||
const cols = span ? clampSpan(span, section.columns) : undefined;
|
||||
@@ -44,7 +51,15 @@ export const Figure: React.FC<FigureProps> = ({
|
||||
...style,
|
||||
}}
|
||||
>
|
||||
<img src={src} alt={alt} style={{ display: 'block', width: '100%', height: 'auto' }} />
|
||||
{src ? (
|
||||
<img
|
||||
src={src}
|
||||
alt={alt ?? ''}
|
||||
loading={loading ?? 'lazy'}
|
||||
sizes={sizes}
|
||||
style={{ display: 'block', width: '100%', height: 'auto', aspectRatio }}
|
||||
/>
|
||||
) : children}
|
||||
{(caption || credit) && <Caption credit={credit}>{caption}</Caption>}
|
||||
</figure>
|
||||
);
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
'use client';
|
||||
import React, { CSSProperties } from 'react';
|
||||
import { clampSpan, cx } from '@newspaperui/utils';
|
||||
import { clampSpan, cx } from 'newspaperui-utils';
|
||||
import { useSection } from '../layout/Section';
|
||||
|
||||
export interface ImageProps {
|
||||
src: string;
|
||||
alt: string;
|
||||
span?: number;
|
||||
loading?: 'lazy' | 'eager';
|
||||
aspectRatio?: string;
|
||||
sizes?: string;
|
||||
className?: string;
|
||||
style?: CSSProperties;
|
||||
}
|
||||
@@ -16,23 +19,29 @@ export interface ImageProps {
|
||||
*
|
||||
* - 响应式全宽图片,自动适配栅格列宽
|
||||
* - 通过 span 控制在 Section 中占据的列数
|
||||
* - 默认 lazy loading 优化性能
|
||||
* - 必须提供 alt 属性以确保无障碍访问
|
||||
*
|
||||
* @example
|
||||
* <Image src="/photo.jpg" alt="City skyline" span={12} />
|
||||
* <Image src="/photo.jpg" alt="City skyline" span={12} aspectRatio="16/9" />
|
||||
*/
|
||||
export const Image: React.FC<ImageProps> = ({ src, alt, span, className, style }) => {
|
||||
export const Image: React.FC<ImageProps> = ({
|
||||
src, alt, span, loading, aspectRatio, sizes, className, style,
|
||||
}) => {
|
||||
const section = useSection();
|
||||
const cols = span ? clampSpan(span, section.columns) : undefined;
|
||||
return (
|
||||
<img
|
||||
src={src}
|
||||
alt={alt}
|
||||
loading={loading ?? 'lazy'}
|
||||
sizes={sizes}
|
||||
className={cx('nui-image', className)}
|
||||
style={{
|
||||
display: 'block',
|
||||
width: '100%',
|
||||
height: 'auto',
|
||||
aspectRatio,
|
||||
gridColumn: cols ? `span ${cols}` : undefined,
|
||||
...style,
|
||||
}}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'use client';
|
||||
import React, { ReactNode, CSSProperties } from 'react';
|
||||
import { visualWeights, resolveFontSize } from '@newspaperui/theme';
|
||||
import { clampSpan, cx } from '@newspaperui/utils';
|
||||
import { visualWeights, resolveFontSize } from 'newspaperui-theme';
|
||||
import { clampSpan, cx } from 'newspaperui-utils';
|
||||
import { useSection } from '../layout/Section';
|
||||
|
||||
export interface PullQuoteProps {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
import React, { CSSProperties } from 'react';
|
||||
import { clampSpan, cx } from '@newspaperui/utils';
|
||||
import { clampSpan, cx } from 'newspaperui-utils';
|
||||
import { useSection } from '../layout/Section';
|
||||
import { Caption } from '../text/Caption';
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'use client';
|
||||
import React, { ReactNode, CSSProperties } from 'react';
|
||||
import { visualWeights, resolveFontSize } from '@newspaperui/theme';
|
||||
import { clampSpan, cx } from '@newspaperui/utils';
|
||||
import { visualWeights, resolveFontSize } from 'newspaperui-theme';
|
||||
import { clampSpan, cx } from 'newspaperui-utils';
|
||||
import { useSection } from '../layout/Section';
|
||||
|
||||
export interface BodyTextProps {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'use client';
|
||||
import React, { ReactNode, CSSProperties } from 'react';
|
||||
import { visualWeights, resolveFontSize } from '@newspaperui/theme';
|
||||
import { cx } from '@newspaperui/utils';
|
||||
import { visualWeights, resolveFontSize } from 'newspaperui-theme';
|
||||
import { cx } from 'newspaperui-utils';
|
||||
|
||||
export interface BylineProps {
|
||||
className?: string;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'use client';
|
||||
import React, { ReactNode, CSSProperties } from 'react';
|
||||
import { visualWeights, resolveFontSize } from '@newspaperui/theme';
|
||||
import { cx } from '@newspaperui/utils';
|
||||
import { visualWeights, resolveFontSize } from 'newspaperui-theme';
|
||||
import { cx } from 'newspaperui-utils';
|
||||
|
||||
export interface CaptionProps {
|
||||
credit?: string; // e.g. "Photograph by Jane Doe"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'use client';
|
||||
import React, { ReactNode, CSSProperties } from 'react';
|
||||
import { visualWeights, resolveFontSize } from '@newspaperui/theme';
|
||||
import { cx } from '@newspaperui/utils';
|
||||
import { visualWeights, resolveFontSize } from 'newspaperui-theme';
|
||||
import { cx } from 'newspaperui-utils';
|
||||
|
||||
export interface DatelineProps {
|
||||
className?: string;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'use client';
|
||||
import React, { ReactNode, CSSProperties } from 'react';
|
||||
import { visualWeights, resolveFontSize } from '@newspaperui/theme';
|
||||
import { clampSpan, cx } from '@newspaperui/utils';
|
||||
import { visualWeights, resolveFontSize } from 'newspaperui-theme';
|
||||
import { clampSpan, cx } from 'newspaperui-utils';
|
||||
import { useSection } from '../layout/Section';
|
||||
|
||||
const weightToTag: Record<'High' | 'Medium' | 'Low', 'h1' | 'h2' | 'h3'> = {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'use client';
|
||||
import React, { ReactNode, CSSProperties } from 'react';
|
||||
import { visualWeights, resolveFontSize } from '@newspaperui/theme';
|
||||
import { cx } from '@newspaperui/utils';
|
||||
import { visualWeights, resolveFontSize } from 'newspaperui-theme';
|
||||
import { cx } from 'newspaperui-utils';
|
||||
|
||||
export interface KickerProps {
|
||||
className?: string;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'use client';
|
||||
import React, { ReactNode, CSSProperties } from 'react';
|
||||
import { visualWeights, resolveFontSize } from '@newspaperui/theme';
|
||||
import { clampSpan, cx } from '@newspaperui/utils';
|
||||
import { visualWeights, resolveFontSize } from 'newspaperui-theme';
|
||||
import { clampSpan, cx } from 'newspaperui-utils';
|
||||
import { useSection } from '../layout/Section';
|
||||
|
||||
export interface QuoteProps {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'use client';
|
||||
import React, { ReactNode, CSSProperties } from 'react';
|
||||
import { visualWeights, resolveFontSize } from '@newspaperui/theme';
|
||||
import { clampSpan, cx } from '@newspaperui/utils';
|
||||
import { visualWeights, resolveFontSize } from 'newspaperui-theme';
|
||||
import { clampSpan, cx } from 'newspaperui-utils';
|
||||
import { useSection } from '../layout/Section';
|
||||
|
||||
export interface SubheadProps {
|
||||
|
||||
Reference in New Issue
Block a user