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:
sunzhongyi
2026-05-21 10:04:35 +08:00
parent 184353cfb0
commit 5f65d741ed
54 changed files with 2759 additions and 101 deletions
+8
View File
@@ -0,0 +1,8 @@
# Changesets
Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works
with multi-package repos, or single-package repos to help you version and publish your code. You can
find the full documentation for it [in our repository](https://github.com/changesets/changesets).
We have a quick list of common questions to get you started engaging with this project in
[our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md).
+11
View File
@@ -0,0 +1,11 @@
{
"$schema": "https://unpkg.com/@changesets/config@3.1.4/schema.json",
"changelog": "@changesets/cli/changelog",
"commit": false,
"fixed": [],
"linked": [],
"access": "restricted",
"baseBranch": "main",
"updateInternalDependencies": "patch",
"ignore": []
}
+5
View File
@@ -0,0 +1,5 @@
dist
out
.next
node_modules
pnpm-lock.yaml
+7
View File
@@ -0,0 +1,7 @@
{
"semi": true,
"singleQuote": true,
"trailingComma": "all",
"printWidth": 100,
"tabWidth": 2
}
+22
View File
@@ -0,0 +1,22 @@
import js from '@eslint/js';
import tseslint from 'typescript-eslint';
import react from 'eslint-plugin-react';
import reactHooks from 'eslint-plugin-react-hooks';
export default tseslint.config(
js.configs.recommended,
...tseslint.configs.recommended,
{
files: ['**/*.{ts,tsx}'],
plugins: { react, 'react-hooks': reactHooks },
rules: {
'react/react-in-jsx-scope': 'off',
'@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_' }],
'@typescript-eslint/no-explicit-any': 'warn',
'react-hooks/rules-of-hooks': 'error',
'react-hooks/exhaustive-deps': 'warn',
},
settings: { react: { version: 'detect' } },
},
{ ignores: ['**/dist/**', '**/node_modules/**', '**/.next/**', '**/out/**', 'packages/docs/**'] },
);
+10 -2
View File
@@ -2,17 +2,25 @@
"name": "newspaperui", "name": "newspaperui",
"version": "0.0.0", "version": "0.0.0",
"private": true, "private": true,
"type": "module",
"description": "A newspaper-style UI component library", "description": "A newspaper-style UI component library",
"scripts": { "scripts": {
"build": "turbo run build", "build": "turbo run build",
"dev": "turbo run dev", "dev": "turbo run dev",
"lint": "turbo run lint", "lint": "eslint packages/*/src/",
"test": "turbo run test", "test": "turbo run test",
"clean": "turbo run clean && rm -rf node_modules" "clean": "turbo run clean && rm -rf node_modules"
}, },
"devDependencies": { "devDependencies": {
"@changesets/cli": "^2.31.0",
"@eslint/js": "^10.0.1",
"eslint": "^10.4.0",
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-hooks": "^7.1.1",
"prettier": "^3.8.3",
"turbo": "^2.3.3", "turbo": "^2.3.3",
"typescript": "^5.7.2" "typescript": "^5.7.2",
"typescript-eslint": "^8.59.4"
}, },
"packageManager": "pnpm@9.15.4", "packageManager": "pnpm@9.15.4",
"engines": { "engines": {
+23 -5
View File
@@ -1,7 +1,7 @@
{ {
"name": "@newspaperui/components", "name": "newspaperui-components",
"version": "0.0.0", "version": "0.1.0",
"description": "React components for newspaperui", "description": "Production-grade newspaper layout React components",
"type": "module", "type": "module",
"main": "./dist/index.cjs", "main": "./dist/index.cjs",
"module": "./dist/index.js", "module": "./dist/index.js",
@@ -29,8 +29,8 @@
"react-dom": "^18.3.1" "react-dom": "^18.3.1"
}, },
"dependencies": { "dependencies": {
"@newspaperui/theme": "workspace:*", "newspaperui-theme": "workspace:*",
"@newspaperui/utils": "workspace:*" "newspaperui-utils": "workspace:*"
}, },
"devDependencies": { "devDependencies": {
"@testing-library/react": "^16.1.0", "@testing-library/react": "^16.1.0",
@@ -45,5 +45,23 @@
"vite": "^5.4.11", "vite": "^5.4.11",
"vite-plugin-dts": "^4.3.0", "vite-plugin-dts": "^4.3.0",
"vitest": "^2.1.8" "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"
} }
} }
+7 -1
View File
@@ -1,4 +1,4 @@
import '@newspaperui/theme'; import 'newspaperui-theme';
// layout // layout
export { Layout, useLayout } from './layout/Layout'; export { Layout, useLayout } from './layout/Layout';
@@ -13,6 +13,12 @@ export { Masthead } from './layout/Masthead';
export type { MastheadProps } from './layout/Masthead'; export type { MastheadProps } from './layout/Masthead';
export { Rule } from './layout/Rule'; export { Rule } from './layout/Rule';
export type { RuleProps } 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 // text
export { Headline } from './text/Headline'; export { Headline } from './text/Headline';
+1 -1
View File
@@ -1,6 +1,6 @@
'use client'; 'use client';
import React, { ReactNode, CSSProperties } from 'react'; import React, { ReactNode, CSSProperties } from 'react';
import { clampSpan, cx } from '@newspaperui/utils'; import { clampSpan, cx } from 'newspaperui-utils';
import { useSection } from './Section'; import { useSection } from './Section';
export interface ArticleProps { 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>
);
+57
View File
@@ -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 -1
View File
@@ -1,6 +1,6 @@
'use client'; 'use client';
import React from 'react'; import React from 'react';
import { cx } from '@newspaperui/utils'; import { cx } from 'newspaperui-utils';
export interface MastheadProps { export interface MastheadProps {
title: string; title: string;
+1 -1
View File
@@ -1,6 +1,6 @@
'use client'; 'use client';
import React, { CSSProperties } from 'react'; import React, { CSSProperties } from 'react';
import { cx } from '@newspaperui/utils'; import { cx } from 'newspaperui-utils';
export interface RuleProps { export interface RuleProps {
variant?: 'hairline' | 'double' | 'thick'; variant?: 'hairline' | 'double' | 'thick';
+13 -4
View File
@@ -1,6 +1,6 @@
'use client'; 'use client';
import React, { createContext, useContext, ReactNode, CSSProperties } from 'react'; import React, { createContext, useContext, useId, ReactNode, CSSProperties } from 'react';
import { clampSpan, cx } from '@newspaperui/utils'; import { clampSpan, cx } from 'newspaperui-utils';
import { useLayout } from './Layout'; import { useLayout } from './Layout';
export interface SectionProps { export interface SectionProps {
@@ -8,6 +8,7 @@ export interface SectionProps {
gap?: string; // 默认 'var(--nui-gutter)' gap?: string; // 默认 'var(--nui-gutter)'
breakable?: boolean; // 是否允许 print 分页断开,默认 true breakable?: boolean; // 是否允许 print 分页断开,默认 true
divider?: 'none' | 'top' | 'bottom' | 'both'; // hairline 分隔 divider?: 'none' | 'top' | 'bottom' | 'both'; // hairline 分隔
responsive?: { sm?: number; md?: number; lg?: number }; // 响应式列数覆盖
className?: string; className?: string;
style?: CSSProperties; style?: CSSProperties;
children: ReactNode; children: ReactNode;
@@ -32,10 +33,17 @@ export const useSection = () => useContext(SectionContext);
*/ */
export const Section: React.FC<SectionProps> = ({ export const Section: React.FC<SectionProps> = ({
columns, gap = 'var(--nui-gutter)', breakable = true, divider = 'none', columns, gap = 'var(--nui-gutter)', breakable = true, divider = 'none',
className, style, children, responsive, className, style, children,
}) => { }) => {
const layout = useLayout(); const layout = useLayout();
const cols = clampSpan(columns, layout.columns); 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 = {}; const dividerStyle: CSSProperties = {};
if (divider === 'top' || divider === 'both') if (divider === 'top' || divider === 'both')
@@ -45,8 +53,9 @@ export const Section: React.FC<SectionProps> = ({
return ( return (
<SectionContext.Provider value={{ columns: cols }}> <SectionContext.Provider value={{ columns: cols }}>
{responsiveCSS && <style dangerouslySetInnerHTML={{ __html: responsiveCSS }} />}
<section <section
className={cx('nui-section', className)} className={cx('nui-section', sectionId, className)}
style={{ style={{
display: 'grid', display: 'grid',
gridTemplateColumns: `repeat(${cols}, 1fr)`, 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>
);
};
+21 -6
View File
@@ -1,17 +1,21 @@
'use client'; 'use client';
import React, { CSSProperties } from 'react'; import React, { ReactNode, CSSProperties } from 'react';
import { clampSpan, cx } from '@newspaperui/utils'; import { clampSpan, cx } from 'newspaperui-utils';
import { useSection } from '../layout/Section'; import { useSection } from '../layout/Section';
import { Caption } from '../text/Caption'; import { Caption } from '../text/Caption';
export interface FigureProps { export interface FigureProps {
src: string; src?: string;
alt: string; alt?: string;
caption?: string; caption?: string;
credit?: string; credit?: string;
span?: number; span?: number;
loading?: 'lazy' | 'eager';
aspectRatio?: string;
sizes?: string;
className?: string; className?: string;
style?: CSSProperties; style?: CSSProperties;
children?: ReactNode;
} }
/** /**
@@ -20,6 +24,7 @@ export interface FigureProps {
* - 将图片与 Caption 组合为语义化 figure 元素 * - 将图片与 Caption 组合为语义化 figure 元素
* - 支持 caption 文字说明和 credit 来源标注 * - 支持 caption 文字说明和 credit 来源标注
* - 自动避免 print 分页断开(break-inside: avoid * - 自动避免 print 分页断开(break-inside: avoid
* - 支持 children 自定义内容替代 img
* *
* @example * @example
* <Figure * <Figure
@@ -28,10 +33,12 @@ export interface FigureProps {
* caption="Downtown at dusk" * caption="Downtown at dusk"
* credit="Photo by John" * credit="Photo by John"
* span={10} * span={10}
* aspectRatio="16/9"
* /> * />
*/ */
export const Figure: React.FC<FigureProps> = ({ 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 section = useSection();
const cols = span ? clampSpan(span, section.columns) : undefined; const cols = span ? clampSpan(span, section.columns) : undefined;
@@ -44,7 +51,15 @@ export const Figure: React.FC<FigureProps> = ({
...style, ...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>} {(caption || credit) && <Caption credit={credit}>{caption}</Caption>}
</figure> </figure>
); );
+12 -3
View File
@@ -1,12 +1,15 @@
'use client'; 'use client';
import React, { CSSProperties } from 'react'; import React, { CSSProperties } from 'react';
import { clampSpan, cx } from '@newspaperui/utils'; import { clampSpan, cx } from 'newspaperui-utils';
import { useSection } from '../layout/Section'; import { useSection } from '../layout/Section';
export interface ImageProps { export interface ImageProps {
src: string; src: string;
alt: string; alt: string;
span?: number; span?: number;
loading?: 'lazy' | 'eager';
aspectRatio?: string;
sizes?: string;
className?: string; className?: string;
style?: CSSProperties; style?: CSSProperties;
} }
@@ -16,23 +19,29 @@ export interface ImageProps {
* *
* - 响应式全宽图片,自动适配栅格列宽 * - 响应式全宽图片,自动适配栅格列宽
* - 通过 span 控制在 Section 中占据的列数 * - 通过 span 控制在 Section 中占据的列数
* - 默认 lazy loading 优化性能
* - 必须提供 alt 属性以确保无障碍访问 * - 必须提供 alt 属性以确保无障碍访问
* *
* @example * @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 section = useSection();
const cols = span ? clampSpan(span, section.columns) : undefined; const cols = span ? clampSpan(span, section.columns) : undefined;
return ( return (
<img <img
src={src} src={src}
alt={alt} alt={alt}
loading={loading ?? 'lazy'}
sizes={sizes}
className={cx('nui-image', className)} className={cx('nui-image', className)}
style={{ style={{
display: 'block', display: 'block',
width: '100%', width: '100%',
height: 'auto', height: 'auto',
aspectRatio,
gridColumn: cols ? `span ${cols}` : undefined, gridColumn: cols ? `span ${cols}` : undefined,
...style, ...style,
}} }}
+2 -2
View File
@@ -1,7 +1,7 @@
'use client'; 'use client';
import React, { ReactNode, CSSProperties } from 'react'; import React, { ReactNode, CSSProperties } from 'react';
import { visualWeights, resolveFontSize } from '@newspaperui/theme'; import { visualWeights, resolveFontSize } from 'newspaperui-theme';
import { clampSpan, cx } from '@newspaperui/utils'; import { clampSpan, cx } from 'newspaperui-utils';
import { useSection } from '../layout/Section'; import { useSection } from '../layout/Section';
export interface PullQuoteProps { export interface PullQuoteProps {
+1 -1
View File
@@ -1,6 +1,6 @@
'use client'; 'use client';
import React, { CSSProperties } from 'react'; import React, { CSSProperties } from 'react';
import { clampSpan, cx } from '@newspaperui/utils'; import { clampSpan, cx } from 'newspaperui-utils';
import { useSection } from '../layout/Section'; import { useSection } from '../layout/Section';
import { Caption } from '../text/Caption'; import { Caption } from '../text/Caption';
+2 -2
View File
@@ -1,7 +1,7 @@
'use client'; 'use client';
import React, { ReactNode, CSSProperties } from 'react'; import React, { ReactNode, CSSProperties } from 'react';
import { visualWeights, resolveFontSize } from '@newspaperui/theme'; import { visualWeights, resolveFontSize } from 'newspaperui-theme';
import { clampSpan, cx } from '@newspaperui/utils'; import { clampSpan, cx } from 'newspaperui-utils';
import { useSection } from '../layout/Section'; import { useSection } from '../layout/Section';
export interface BodyTextProps { export interface BodyTextProps {
+2 -2
View File
@@ -1,7 +1,7 @@
'use client'; 'use client';
import React, { ReactNode, CSSProperties } from 'react'; import React, { ReactNode, CSSProperties } from 'react';
import { visualWeights, resolveFontSize } from '@newspaperui/theme'; import { visualWeights, resolveFontSize } from 'newspaperui-theme';
import { cx } from '@newspaperui/utils'; import { cx } from 'newspaperui-utils';
export interface BylineProps { export interface BylineProps {
className?: string; className?: string;
+2 -2
View File
@@ -1,7 +1,7 @@
'use client'; 'use client';
import React, { ReactNode, CSSProperties } from 'react'; import React, { ReactNode, CSSProperties } from 'react';
import { visualWeights, resolveFontSize } from '@newspaperui/theme'; import { visualWeights, resolveFontSize } from 'newspaperui-theme';
import { cx } from '@newspaperui/utils'; import { cx } from 'newspaperui-utils';
export interface CaptionProps { export interface CaptionProps {
credit?: string; // e.g. "Photograph by Jane Doe" credit?: string; // e.g. "Photograph by Jane Doe"
+2 -2
View File
@@ -1,7 +1,7 @@
'use client'; 'use client';
import React, { ReactNode, CSSProperties } from 'react'; import React, { ReactNode, CSSProperties } from 'react';
import { visualWeights, resolveFontSize } from '@newspaperui/theme'; import { visualWeights, resolveFontSize } from 'newspaperui-theme';
import { cx } from '@newspaperui/utils'; import { cx } from 'newspaperui-utils';
export interface DatelineProps { export interface DatelineProps {
className?: string; className?: string;
+2 -2
View File
@@ -1,7 +1,7 @@
'use client'; 'use client';
import React, { ReactNode, CSSProperties } from 'react'; import React, { ReactNode, CSSProperties } from 'react';
import { visualWeights, resolveFontSize } from '@newspaperui/theme'; import { visualWeights, resolveFontSize } from 'newspaperui-theme';
import { clampSpan, cx } from '@newspaperui/utils'; import { clampSpan, cx } from 'newspaperui-utils';
import { useSection } from '../layout/Section'; import { useSection } from '../layout/Section';
const weightToTag: Record<'High' | 'Medium' | 'Low', 'h1' | 'h2' | 'h3'> = { const weightToTag: Record<'High' | 'Medium' | 'Low', 'h1' | 'h2' | 'h3'> = {
+2 -2
View File
@@ -1,7 +1,7 @@
'use client'; 'use client';
import React, { ReactNode, CSSProperties } from 'react'; import React, { ReactNode, CSSProperties } from 'react';
import { visualWeights, resolveFontSize } from '@newspaperui/theme'; import { visualWeights, resolveFontSize } from 'newspaperui-theme';
import { cx } from '@newspaperui/utils'; import { cx } from 'newspaperui-utils';
export interface KickerProps { export interface KickerProps {
className?: string; className?: string;
+2 -2
View File
@@ -1,7 +1,7 @@
'use client'; 'use client';
import React, { ReactNode, CSSProperties } from 'react'; import React, { ReactNode, CSSProperties } from 'react';
import { visualWeights, resolveFontSize } from '@newspaperui/theme'; import { visualWeights, resolveFontSize } from 'newspaperui-theme';
import { clampSpan, cx } from '@newspaperui/utils'; import { clampSpan, cx } from 'newspaperui-utils';
import { useSection } from '../layout/Section'; import { useSection } from '../layout/Section';
export interface QuoteProps { export interface QuoteProps {
+2 -2
View File
@@ -1,7 +1,7 @@
'use client'; 'use client';
import React, { ReactNode, CSSProperties } from 'react'; import React, { ReactNode, CSSProperties } from 'react';
import { visualWeights, resolveFontSize } from '@newspaperui/theme'; import { visualWeights, resolveFontSize } from 'newspaperui-theme';
import { clampSpan, cx } from '@newspaperui/utils'; import { clampSpan, cx } from 'newspaperui-utils';
import { useSection } from '../layout/Section'; import { useSection } from '../layout/Section';
export interface SubheadProps { export interface SubheadProps {
@@ -7,7 +7,7 @@ import {
Headline, Headline,
Subhead, Subhead,
BodyText, BodyText,
} from '@newspaperui/components'; } from 'newspaperui-components';
import { Demo } from '@/components/Demo'; import { Demo } from '@/components/Demo';
import { PropsTable } from '@/components/PropsTable'; import { PropsTable } from '@/components/PropsTable';
@@ -7,7 +7,7 @@ import {
Subhead, Subhead,
BodyText, BodyText,
Masthead, Masthead,
} from '@newspaperui/components'; } from 'newspaperui-components';
import { Demo } from '@/components/Demo'; import { Demo } from '@/components/Demo';
import { PropsTable } from '@/components/PropsTable'; import { PropsTable } from '@/components/PropsTable';
@@ -8,7 +8,7 @@ import {
BodyText, BodyText,
Figure, Figure,
PullQuote, PullQuote,
} from '@newspaperui/components'; } from 'newspaperui-components';
import { Demo } from '@/components/Demo'; import { Demo } from '@/components/Demo';
import { PropsTable } from '@/components/PropsTable'; import { PropsTable } from '@/components/PropsTable';
@@ -7,7 +7,7 @@ import {
Subhead, Subhead,
BodyText, BodyText,
Rule, Rule,
} from '@newspaperui/components'; } from 'newspaperui-components';
import { Demo } from '@/components/Demo'; import { Demo } from '@/components/Demo';
import { PropsTable } from '@/components/PropsTable'; import { PropsTable } from '@/components/PropsTable';
@@ -7,7 +7,7 @@ import {
Subhead, Subhead,
BodyText, BodyText,
Rule, Rule,
} from '@newspaperui/components'; } from 'newspaperui-components';
import { CodeBlock } from '@/components/CodeBlock'; import { CodeBlock } from '@/components/CodeBlock';
export default function ResponsivePage() { export default function ResponsivePage() {
@@ -7,7 +7,7 @@ import {
Subhead, Subhead,
BodyText, BodyText,
Rule, Rule,
} from '@newspaperui/components'; } from 'newspaperui-components';
import { Demo } from '@/components/Demo'; import { Demo } from '@/components/Demo';
export default function SpanningPage() { export default function SpanningPage() {
@@ -6,7 +6,7 @@ import {
Headline, Headline,
Subhead, Subhead,
BodyText, BodyText,
} from '@newspaperui/components'; } from 'newspaperui-components';
import { Demo } from '@/components/Demo'; import { Demo } from '@/components/Demo';
import { PropsTable } from '@/components/PropsTable'; import { PropsTable } from '@/components/PropsTable';
+2 -2
View File
@@ -11,8 +11,8 @@ import {
Byline, Byline,
Dateline, Dateline,
Caption, Caption,
} from '@newspaperui/components'; } from 'newspaperui-components';
import { visualWeights, resolveFontSize } from '@newspaperui/theme'; import { visualWeights, resolveFontSize } from 'newspaperui-theme';
import { Demo } from '@/components/Demo'; import { Demo } from '@/components/Demo';
const longText = `Whitehall officials confirmed late Tuesday that the long-anticipated review of national infrastructure funding will be tabled before the recess. The 248-page document, drafted across three departments, recommends a recalibration of regional priorities and a measured shift toward rail electrification. Critics inside the cabinet caution that the timing risks overshadowing the chancellor's autumn statement, while supporters describe the proposals as the most coherent strategic blueprint in a generation.`; const longText = `Whitehall officials confirmed late Tuesday that the long-anticipated review of national infrastructure funding will be tabled before the recess. The 248-page document, drafted across three departments, recommends a recalibration of regional priorities and a measured shift toward rail electrification. Critics inside the cabinet caution that the timing risks overshadowing the chancellor's autumn statement, while supporters describe the proposals as the most coherent strategic blueprint in a generation.`;
+1 -1
View File
@@ -6,7 +6,7 @@ import {
Headline, Headline,
Subhead, Subhead,
BodyText, BodyText,
} from '@newspaperui/components'; } from 'newspaperui-components';
import { ThemeToggle } from '@/components/ThemeToggle'; import { ThemeToggle } from '@/components/ThemeToggle';
interface Swatch { interface Swatch {
+1 -1
View File
@@ -1,5 +1,5 @@
'use client'; 'use client';
import { Layout, Section, Article, Rule, Headline, Subhead, Kicker, BodyText, Byline, Dateline, Figure, PullQuote } from '@newspaperui/components'; import { Layout, Section, Article, Rule, Headline, Subhead, Kicker, BodyText, Byline, Dateline, Figure, PullQuote } from 'newspaperui-components';
export default function EnFeature() { export default function EnFeature() {
return ( return (
@@ -1,5 +1,5 @@
'use client'; 'use client';
import { Layout, Section, Article, Rule, BodyText, Figure } from '@newspaperui/components'; import { Layout, Section, Article, Rule, BodyText, Figure } from 'newspaperui-components';
const jp = { fontFamily: 'var(--font-family-cjk-jp)' }; const jp = { fontFamily: 'var(--font-family-cjk-jp)' };
const jpAccent = { color: 'var(--nui-accent-ink-blue)' }; const jpAccent = { color: 'var(--nui-accent-ink-blue)' };
+1 -1
View File
@@ -1,6 +1,6 @@
'use client'; 'use client';
import Link from 'next/link'; import Link from 'next/link';
import { Layout, Section, Article, Headline, Subhead, Kicker } from '@newspaperui/components'; import { Layout, Section, Article, Headline, Subhead, Kicker } from 'newspaperui-components';
const blocks = [ const blocks = [
{ {
@@ -1,5 +1,5 @@
'use client'; 'use client';
import { Layout, Section, Article, Rule, BodyText, Quote } from '@newspaperui/components'; import { Layout, Section, Article, Rule, BodyText, Quote } from 'newspaperui-components';
const cn = { fontFamily: 'var(--font-family-cjk-serif)' }; const cn = { fontFamily: 'var(--font-family-cjk-serif)' };
const cnRed = { color: 'var(--nui-accent-cjk-red)' }; const cnRed = { color: 'var(--nui-accent-cjk-red)' };
+1 -1
View File
@@ -1,5 +1,5 @@
'use client'; 'use client';
import { Layout, Section, Article, BodyText, Quote, Figure, PullQuote } from '@newspaperui/components'; import { Layout, Section, Article, BodyText, Quote, Figure, PullQuote } from 'newspaperui-components';
const cn = { fontFamily: 'var(--font-family-cjk-serif)' }; const cn = { fontFamily: 'var(--font-family-cjk-serif)' };
const cnRed = { color: 'var(--nui-accent-cjk-red)' }; const cnRed = { color: 'var(--nui-accent-cjk-red)' };
@@ -1,5 +1,5 @@
'use client'; 'use client';
import { Layout, Section, Article, Rule, BodyText, Figure } from '@newspaperui/components'; import { Layout, Section, Article, Rule, BodyText, Figure } from 'newspaperui-components';
const cn = { fontFamily: 'var(--font-family-cjk-serif)' }; const cn = { fontFamily: 'var(--font-family-cjk-serif)' };
const cnRed = { color: 'var(--nui-accent-cjk-red)' }; const cnRed = { color: 'var(--nui-accent-cjk-red)' };
@@ -4,7 +4,7 @@ import {
Layout, Section, Article, Masthead, Layout, Section, Article, Masthead,
Headline, Subhead, Kicker, BodyText, Byline, Dateline, Headline, Subhead, Kicker, BodyText, Byline, Dateline,
Figure, PullQuote, Figure, PullQuote,
} from '@newspaperui/components'; } from 'newspaperui-components';
export default function BlackletterFrontPage() { export default function BlackletterFrontPage() {
return ( return (
@@ -4,7 +4,7 @@ import {
Layout, Section, Article, Masthead, Rule, Layout, Section, Article, Masthead, Rule,
Headline, Subhead, Kicker, BodyText, Byline, Dateline, Headline, Subhead, Kicker, BodyText, Byline, Dateline,
Figure, PullQuote, Figure, PullQuote,
} from '@newspaperui/components'; } from 'newspaperui-components';
export default function FrontPage() { export default function FrontPage() {
return ( return (
+1 -1
View File
@@ -1,4 +1,4 @@
@import "@newspaperui/theme/dist/style.css"; @import "newspaperui-theme/dist/style.css";
@tailwind base; @tailwind base;
@tailwind components; @tailwind components;
+4 -4
View File
@@ -9,7 +9,7 @@ import {
Subhead, Subhead,
Kicker, Kicker,
BodyText, BodyText,
} from '@newspaperui/components'; } from 'newspaperui-components';
import Link from 'next/link'; import Link from 'next/link';
const demos = [ const demos = [
@@ -108,7 +108,7 @@ export default function LandingPage() {
</Headline> </Headline>
<BodyText weight="Medium"> <BodyText weight="Medium">
<p style={{ fontFamily: 'var(--font-family-meta)', fontSize: '13px' }}> <p style={{ fontFamily: 'var(--font-family-meta)', fontSize: '13px' }}>
<code>pnpm add @newspaperui/components @newspaperui/theme</code> <code>pnpm add newspaperui-components newspaperui-theme</code>
</p> </p>
</BodyText> </BodyText>
<div style={{ marginTop: '1rem' }}> <div style={{ marginTop: '1rem' }}>
@@ -267,8 +267,8 @@ export default function LandingPage() {
</Headline> </Headline>
<BodyText weight="Low"> <BodyText weight="Low">
<p> <p>
4 packages<code>@newspaperui/theme</code><code>@newspaperui/utils</code> 4 packages<code>newspaperui-theme</code><code>newspaperui-utils</code>
<code>@newspaperui/components</code>18 components<code>@newspaperui/docs</code> <code>newspaperui-components</code>18 components<code>@newspaperui/docs</code>
</p> </p>
<p>Built with pnpm workspaces + Turborepo. Vite for libraries, Next.js 15 for docs.</p> <p>Built with pnpm workspaces + Turborepo. Vite for libraries, Next.js 15 for docs.</p>
</BodyText> </BodyText>
+1 -1
View File
@@ -1,7 +1,7 @@
'use client'; 'use client';
import Link from 'next/link'; import Link from 'next/link';
import { usePathname } from 'next/navigation'; import { usePathname } from 'next/navigation';
import { cx } from '@newspaperui/utils'; import { cx } from 'newspaperui-utils';
interface NavItem { interface NavItem {
label: string; label: string;
+1 -1
View File
@@ -7,7 +7,7 @@ const nextConfig = {
basePath: process.env.NEXT_PUBLIC_BASE_PATH || '', basePath: process.env.NEXT_PUBLIC_BASE_PATH || '',
images: { unoptimized: true }, images: { unoptimized: true },
pageExtensions: ['js', 'jsx', 'md', 'mdx', 'ts', 'tsx'], pageExtensions: ['js', 'jsx', 'md', 'mdx', 'ts', 'tsx'],
transpilePackages: ['@newspaperui/components', '@newspaperui/theme', '@newspaperui/utils'], transpilePackages: ['newspaperui-components', 'newspaperui-theme', 'newspaperui-utils'],
}; };
const withMDX = createMDX({ const withMDX = createMDX({
+4 -4
View File
@@ -13,16 +13,16 @@
"dependencies": { "dependencies": {
"@mdx-js/loader": "^3.1.1", "@mdx-js/loader": "^3.1.1",
"@mdx-js/react": "^3.1.1", "@mdx-js/react": "^3.1.1",
"@newspaperui/components": "workspace:*",
"@newspaperui/theme": "workspace:*",
"@newspaperui/utils": "workspace:*",
"@next/mdx": "^16.2.6", "@next/mdx": "^16.2.6",
"@types/mdx": "^2.0.13", "@types/mdx": "^2.0.13",
"next": "^15.1.6", "next": "^15.1.6",
"react": "^18.3.1", "react": "^18.3.1",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
"rehype-pretty-code": "^0.14.3", "rehype-pretty-code": "^0.14.3",
"shiki": "^4.1.0" "shiki": "^4.1.0",
"newspaperui-components": "workspace:*",
"newspaperui-theme": "workspace:*",
"newspaperui-utils": "workspace:*"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^22.10.5", "@types/node": "^22.10.5",
+19 -3
View File
@@ -1,7 +1,7 @@
{ {
"name": "@newspaperui/theme", "name": "newspaperui-theme",
"version": "0.0.0", "version": "0.1.0",
"description": "Theme tokens and CSS variables for newspaperui", "description": "CSS variables, visual weights, and typography utilities for NewspaperUI",
"type": "module", "type": "module",
"main": "./dist/index.cjs", "main": "./dist/index.cjs",
"module": "./dist/index.js", "module": "./dist/index.js",
@@ -31,5 +31,21 @@
"typescript": "^5.7.2", "typescript": "^5.7.2",
"vite": "^5.4.11", "vite": "^5.4.11",
"vite-plugin-dts": "^4.3.0" "vite-plugin-dts": "^4.3.0"
},
"license": "MIT",
"author": "sunzhongyi",
"repository": {
"type": "git",
"url": "https://github.com/joisun/newspaperui.git"
},
"keywords": [
"newspaper",
"typography",
"css-variables",
"theme",
"design-tokens"
],
"publishConfig": {
"access": "public"
} }
} }
+22
View File
@@ -42,6 +42,26 @@
--nui-space-4: 1rem; --nui-space-4: 1rem;
--nui-space-6: 1.5rem; --nui-space-6: 1.5rem;
--nui-space-8: 2rem; --nui-space-8: 2rem;
/* border-radius */
--nui-radius-none: 0;
--nui-radius-sm: 2px;
--nui-radius-md: 4px;
/* shadow */
--nui-shadow-sm: 0 1px 2px rgba(0,0,0,0.05);
--nui-shadow-md: 0 2px 8px rgba(0,0,0,0.08);
/* transition */
--nui-transition-fast: 150ms ease;
--nui-transition-normal: 250ms ease;
/* z-index layers */
--nui-z-base: 0;
--nui-z-sticky: 10;
--nui-z-header: 50;
--nui-z-modal: 100;
--nui-z-tooltip: 150;
} }
[data-theme="dark"] { [data-theme="dark"] {
@@ -57,6 +77,8 @@
--nui-accent-primary: #C97A6E; --nui-accent-primary: #C97A6E;
--nui-accent-ink-blue: #7E94C2; --nui-accent-ink-blue: #7E94C2;
--nui-highlight: #3A2F18; --nui-highlight: #3A2F18;
--nui-shadow-sm: 0 1px 2px rgba(0,0,0,0.3);
--nui-shadow-md: 0 2px 8px rgba(0,0,0,0.4);
} }
html, body { background: var(--nui-bg-page); color: var(--nui-text-body); } html, body { background: var(--nui-bg-page); color: var(--nui-text-body); }
+10 -2
View File
@@ -236,9 +236,17 @@ export const visualWeights: Record<ComponentType, Partial<Record<VisualWeight, V
}, },
}; };
/** Resolve fontSize: tuple → first value (lower bound used by default). */ /** Resolve fontSize: tuple → clamp(min, preferred, max) for responsive sizing. */
export function resolveFontSize(value: string | [string, string]): string { export function resolveFontSize(value: string | [string, string]): string {
return Array.isArray(value) ? value[0] : value; if (Array.isArray(value)) {
const min = value[0];
const max = value[1];
const minNum = parseFloat(min);
const maxNum = parseFloat(max);
const vw = ((minNum + maxNum) / 2 / 16 * 1.5).toFixed(2);
return `clamp(${min}, ${vw}vw, ${max})`;
}
return value;
} }
/** Resolve span: tuple → first value (lower bound). */ /** Resolve span: tuple → first value (lower bound). */
+17 -3
View File
@@ -1,7 +1,7 @@
{ {
"name": "@newspaperui/utils", "name": "newspaperui-utils",
"version": "0.0.0", "version": "0.1.0",
"description": "Utility functions for newspaperui", "description": "Grid validation and utility functions for NewspaperUI",
"type": "module", "type": "module",
"main": "./dist/index.cjs", "main": "./dist/index.cjs",
"module": "./dist/index.js", "module": "./dist/index.js",
@@ -29,5 +29,19 @@
"vite": "^5.4.11", "vite": "^5.4.11",
"vite-plugin-dts": "^4.3.0", "vite-plugin-dts": "^4.3.0",
"vitest": "^2.1.8" "vitest": "^2.1.8"
},
"license": "MIT",
"author": "sunzhongyi",
"repository": {
"type": "git",
"url": "https://github.com/joisun/newspaperui.git"
},
"keywords": [
"newspaper",
"grid",
"utilities"
],
"publishConfig": {
"access": "public"
} }
} }
+2345 -17
View File
File diff suppressed because it is too large Load Diff