-
This commit is contained in:
+45
@@ -0,0 +1,45 @@
|
||||
# Dependencies
|
||||
node_modules
|
||||
.pnp
|
||||
.pnp.js
|
||||
|
||||
# Build outputs
|
||||
dist
|
||||
.next
|
||||
out
|
||||
build
|
||||
*.tsbuildinfo
|
||||
|
||||
# Environment
|
||||
.env
|
||||
.env*.local
|
||||
|
||||
# IDE
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
|
||||
# Testing
|
||||
coverage
|
||||
.nyc_output
|
||||
|
||||
# Turbo
|
||||
.turbo
|
||||
|
||||
# Misc
|
||||
.cache
|
||||
.temp
|
||||
@@ -0,0 +1,102 @@
|
||||
- generic [ref=e2]:
|
||||
- navigation [ref=e3]:
|
||||
- generic [ref=e4]:
|
||||
- link "NewspaperUI" [ref=e5] [cursor=pointer]:
|
||||
- /url: /
|
||||
- paragraph [ref=e6]: 报纸布局组件库
|
||||
- generic [ref=e7]:
|
||||
- link "概览" [ref=e9] [cursor=pointer]:
|
||||
- /url: /
|
||||
- link "栅格系统" [ref=e11] [cursor=pointer]:
|
||||
- /url: /grid-system
|
||||
- generic [ref=e12]:
|
||||
- link "核心组件" [ref=e13] [cursor=pointer]:
|
||||
- /url: /components
|
||||
- generic [ref=e14]:
|
||||
- link "Article" [ref=e15] [cursor=pointer]:
|
||||
- /url: /components/article
|
||||
- link "Layer" [ref=e16] [cursor=pointer]:
|
||||
- /url: /components/layer
|
||||
- link "媒体组件" [ref=e17] [cursor=pointer]:
|
||||
- /url: /components/media
|
||||
- link "文本组件" [ref=e19] [cursor=pointer]:
|
||||
- /url: /text
|
||||
- link "主题系统" [ref=e21] [cursor=pointer]:
|
||||
- /url: /theme
|
||||
- generic [ref=e22]:
|
||||
- link "示例" [ref=e23] [cursor=pointer]:
|
||||
- /url: /examples
|
||||
- generic [ref=e24]:
|
||||
- link "跨栏布局" [ref=e25] [cursor=pointer]:
|
||||
- /url: /examples/spanning
|
||||
- link "响应式布局" [ref=e26] [cursor=pointer]:
|
||||
- /url: /examples/responsive
|
||||
- main [ref=e27]:
|
||||
- generic [ref=e29]:
|
||||
- heading "NewspaperUI" [level=1] [ref=e30]
|
||||
- paragraph [ref=e31]: 生产级报纸布局组件库,参考 InDesign 设计理念,支持 24 列栅格、跨栏、视觉权重和主题系统。
|
||||
- heading "设计理念" [level=2] [ref=e32]
|
||||
- paragraph [ref=e33]: NewspaperUI 借鉴专业排版软件 InDesign 的设计思想,为 Web 应用提供报纸风格的布局能力。 组件设计遵循少而精、职责明确、无冗余、无歧义的原则。
|
||||
- heading "核心特性" [level=2] [ref=e34]
|
||||
- list [ref=e35]:
|
||||
- listitem [ref=e36]:
|
||||
- strong [ref=e37]: 24 列栅格系统
|
||||
- text: "- 灵活的栅格布局,支持精确的列控制"
|
||||
- listitem [ref=e38]:
|
||||
- strong [ref=e39]: 跨栏布局
|
||||
- text: "- 内容可以跨越多列,实现复杂的报纸排版"
|
||||
- listitem [ref=e40]:
|
||||
- strong [ref=e41]: 视觉权重系统
|
||||
- text: "- High/Medium/Low 三级权重,自动映射字体大小和样式"
|
||||
- listitem [ref=e42]:
|
||||
- strong [ref=e43]: 主题系统
|
||||
- text: "- 基于 CSS Variables,支持主题切换和自定义"
|
||||
- listitem [ref=e44]:
|
||||
- strong [ref=e45]: 响应式设计
|
||||
- text: "- 小屏自动调整为 12-16 列,保持良好的阅读体验"
|
||||
- listitem [ref=e46]:
|
||||
- strong [ref=e47]: 浮动 Layer
|
||||
- text: "- 支持绝对定位的浮动元素,如广告、拉引等"
|
||||
- heading "快速开始" [level=2] [ref=e48]
|
||||
- heading "安装" [level=3] [ref=e49]
|
||||
- code [ref=e51]: pnpm add @newspaperui/components @newspaperui/theme
|
||||
- heading "基础使用" [level=3] [ref=e52]
|
||||
- generic [ref=e53]:
|
||||
- generic [ref=e54]:
|
||||
- heading "简单的报纸布局" [level=3] [ref=e55]
|
||||
- paragraph [ref=e56]: 使用 Layout 和 Section 创建基础的栅格布局
|
||||
- generic [ref=e58]:
|
||||
- article [ref=e60]:
|
||||
- heading "主要新闻标题" [level=1] [ref=e61]
|
||||
- paragraph [ref=e62]: 这是一篇重要的新闻内容,使用 High 权重的标题和正文文本。
|
||||
- generic [ref=e63]:
|
||||
- article [ref=e64]:
|
||||
- heading "次要新闻" [level=1] [ref=e65]
|
||||
- paragraph [ref=e66]: 这是另一篇新闻,使用 Medium 权重。
|
||||
- article [ref=e67]:
|
||||
- heading "更多新闻" [level=1] [ref=e68]
|
||||
- paragraph [ref=e69]: 更多内容在这里展示。
|
||||
- button "查看代码" [ref=e71] [cursor=pointer]
|
||||
- heading "下一步" [level=2] [ref=e72]
|
||||
- paragraph [ref=e73]: 探索文档了解更多功能:
|
||||
- list [ref=e74]:
|
||||
- listitem [ref=e75]:
|
||||
- link "栅格系统" [ref=e76] [cursor=pointer]:
|
||||
- /url: /grid-system
|
||||
- text: "- 了解 24 列栅格的工作原理"
|
||||
- listitem [ref=e77]:
|
||||
- link "核心组件" [ref=e78] [cursor=pointer]:
|
||||
- /url: /components/article
|
||||
- text: "- 学习 Article、Layer 等核心组件"
|
||||
- listitem [ref=e79]:
|
||||
- link "文本组件" [ref=e80] [cursor=pointer]:
|
||||
- /url: /text
|
||||
- text: "- 掌握视觉权重系统"
|
||||
- listitem [ref=e81]:
|
||||
- link "主题系统" [ref=e82] [cursor=pointer]:
|
||||
- /url: /theme
|
||||
- text: "- 自定义主题和样式"
|
||||
- listitem [ref=e83]:
|
||||
- link "示例" [ref=e84] [cursor=pointer]:
|
||||
- /url: /examples/spanning
|
||||
- text: "- 查看完整的布局示例"
|
||||
@@ -0,0 +1,13 @@
|
||||
- generic [active]:
|
||||
- generic [ref=e5] [cursor=pointer]:
|
||||
- button "Open Next.js Dev Tools" [ref=e6]:
|
||||
- img [ref=e7]
|
||||
- generic [ref=e10]:
|
||||
- button "Open issues overlay" [ref=e11]:
|
||||
- generic [ref=e12]:
|
||||
- generic [ref=e13]: "0"
|
||||
- generic [ref=e14]: "1"
|
||||
- generic [ref=e15]: Issue
|
||||
- button "Collapse issues badge" [ref=e16]:
|
||||
- img [ref=e17]
|
||||
- alert [ref=e19]
|
||||
@@ -0,0 +1,102 @@
|
||||
- generic [ref=e2]:
|
||||
- navigation [ref=e3]:
|
||||
- generic [ref=e4]:
|
||||
- link "NewspaperUI" [ref=e5] [cursor=pointer]:
|
||||
- /url: /
|
||||
- paragraph [ref=e6]: 报纸布局组件库
|
||||
- generic [ref=e7]:
|
||||
- link "概览" [ref=e9] [cursor=pointer]:
|
||||
- /url: /
|
||||
- link "栅格系统" [ref=e11] [cursor=pointer]:
|
||||
- /url: /grid-system
|
||||
- generic [ref=e12]:
|
||||
- link "核心组件" [ref=e13] [cursor=pointer]:
|
||||
- /url: /components
|
||||
- generic [ref=e14]:
|
||||
- link "Article" [ref=e15] [cursor=pointer]:
|
||||
- /url: /components/article
|
||||
- link "Layer" [ref=e16] [cursor=pointer]:
|
||||
- /url: /components/layer
|
||||
- link "媒体组件" [ref=e17] [cursor=pointer]:
|
||||
- /url: /components/media
|
||||
- link "文本组件" [ref=e19] [cursor=pointer]:
|
||||
- /url: /text
|
||||
- link "主题系统" [ref=e21] [cursor=pointer]:
|
||||
- /url: /theme
|
||||
- generic [ref=e22]:
|
||||
- link "示例" [ref=e23] [cursor=pointer]:
|
||||
- /url: /examples
|
||||
- generic [ref=e24]:
|
||||
- link "跨栏布局" [ref=e25] [cursor=pointer]:
|
||||
- /url: /examples/spanning
|
||||
- link "响应式布局" [ref=e26] [cursor=pointer]:
|
||||
- /url: /examples/responsive
|
||||
- main [ref=e27]:
|
||||
- generic [ref=e29]:
|
||||
- heading "NewspaperUI" [level=1] [ref=e30]
|
||||
- paragraph [ref=e31]: 生产级报纸布局组件库,参考 InDesign 设计理念,支持 24 列栅格、跨栏、视觉权重和主题系统。
|
||||
- heading "设计理念" [level=2] [ref=e32]
|
||||
- paragraph [ref=e33]: NewspaperUI 借鉴专业排版软件 InDesign 的设计思想,为 Web 应用提供报纸风格的布局能力。 组件设计遵循少而精、职责明确、无冗余、无歧义的原则。
|
||||
- heading "核心特性" [level=2] [ref=e34]
|
||||
- list [ref=e35]:
|
||||
- listitem [ref=e36]:
|
||||
- strong [ref=e37]: 24 列栅格系统
|
||||
- text: "- 灵活的栅格布局,支持精确的列控制"
|
||||
- listitem [ref=e38]:
|
||||
- strong [ref=e39]: 跨栏布局
|
||||
- text: "- 内容可以跨越多列,实现复杂的报纸排版"
|
||||
- listitem [ref=e40]:
|
||||
- strong [ref=e41]: 视觉权重系统
|
||||
- text: "- High/Medium/Low 三级权重,自动映射字体大小和样式"
|
||||
- listitem [ref=e42]:
|
||||
- strong [ref=e43]: 主题系统
|
||||
- text: "- 基于 CSS Variables,支持主题切换和自定义"
|
||||
- listitem [ref=e44]:
|
||||
- strong [ref=e45]: 响应式设计
|
||||
- text: "- 小屏自动调整为 12-16 列,保持良好的阅读体验"
|
||||
- listitem [ref=e46]:
|
||||
- strong [ref=e47]: 浮动 Layer
|
||||
- text: "- 支持绝对定位的浮动元素,如广告、拉引等"
|
||||
- heading "快速开始" [level=2] [ref=e48]
|
||||
- heading "安装" [level=3] [ref=e49]
|
||||
- code [ref=e51]: pnpm add @newspaperui/components @newspaperui/theme
|
||||
- heading "基础使用" [level=3] [ref=e52]
|
||||
- generic [ref=e53]:
|
||||
- generic [ref=e54]:
|
||||
- heading "简单的报纸布局" [level=3] [ref=e55]
|
||||
- paragraph [ref=e56]: 使用 Layout 和 Section 创建基础的栅格布局
|
||||
- generic [ref=e58]:
|
||||
- article [ref=e60]:
|
||||
- heading "主要新闻标题" [level=1] [ref=e61]
|
||||
- paragraph [ref=e62]: 这是一篇重要的新闻内容,使用 High 权重的标题和正文文本。
|
||||
- generic [ref=e63]:
|
||||
- article [ref=e64]:
|
||||
- heading "次要新闻" [level=2] [ref=e65]
|
||||
- paragraph [ref=e66]: 这是另一篇新闻,使用 Medium 权重。
|
||||
- article [ref=e67]:
|
||||
- heading "更多新闻" [level=2] [ref=e68]
|
||||
- paragraph [ref=e69]: 更多内容在这里展示。
|
||||
- button "查看代码" [ref=e71]
|
||||
- heading "下一步" [level=2] [ref=e72]
|
||||
- paragraph [ref=e73]: 探索文档了解更多功能:
|
||||
- list [ref=e74]:
|
||||
- listitem [ref=e75]:
|
||||
- link "栅格系统" [ref=e76] [cursor=pointer]:
|
||||
- /url: /grid-system
|
||||
- text: "- 了解 24 列栅格的工作原理"
|
||||
- listitem [ref=e77]:
|
||||
- link "核心组件" [ref=e78] [cursor=pointer]:
|
||||
- /url: /components/article
|
||||
- text: "- 学习 Article、Layer 等核心组件"
|
||||
- listitem [ref=e79]:
|
||||
- link "文本组件" [ref=e80] [cursor=pointer]:
|
||||
- /url: /text
|
||||
- text: "- 掌握视觉权重系统"
|
||||
- listitem [ref=e81]:
|
||||
- link "主题系统" [ref=e82] [cursor=pointer]:
|
||||
- /url: /theme
|
||||
- text: "- 自定义主题和样式"
|
||||
- listitem [ref=e83]:
|
||||
- link "示例" [ref=e84] [cursor=pointer]:
|
||||
- /url: /examples/spanning
|
||||
- text: "- 查看完整的布局示例"
|
||||
@@ -0,0 +1,82 @@
|
||||
# NewspaperUI
|
||||
|
||||
A newspaper-style UI component library built with React, TypeScript, and Tailwind CSS.
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
newspaperui/
|
||||
├── packages/
|
||||
│ ├── components/ # React components
|
||||
│ ├── theme/ # CSS variables and Tailwind tokens
|
||||
│ ├── utils/ # Utility functions
|
||||
│ └── docs/ # Documentation site (Next.js)
|
||||
├── turbo.json # Turborepo configuration
|
||||
├── pnpm-workspace.yaml # pnpm workspace configuration
|
||||
└── package.json # Root package.json
|
||||
```
|
||||
|
||||
## Getting Started
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Node.js >= 18.0.0
|
||||
- pnpm >= 9.0.0
|
||||
|
||||
### Installation
|
||||
|
||||
```bash
|
||||
pnpm install
|
||||
```
|
||||
|
||||
### Development
|
||||
|
||||
```bash
|
||||
# Run all packages in dev mode
|
||||
pnpm dev
|
||||
|
||||
# Build all packages
|
||||
pnpm build
|
||||
|
||||
# Run linting
|
||||
pnpm lint
|
||||
|
||||
# Run tests
|
||||
pnpm test
|
||||
```
|
||||
|
||||
## Packages
|
||||
|
||||
### @newspaperui/components
|
||||
|
||||
React components for building newspaper-style interfaces.
|
||||
|
||||
**Dependencies**: `@newspaperui/theme`, `@newspaperui/utils`
|
||||
|
||||
### @newspaperui/theme
|
||||
|
||||
CSS variables and Tailwind tokens for consistent theming.
|
||||
|
||||
### @newspaperui/utils
|
||||
|
||||
Utility functions used across the component library.
|
||||
|
||||
### @newspaperui/docs
|
||||
|
||||
Documentation site built with Next.js 14+ App Router.
|
||||
|
||||
**Dependencies**: `@newspaperui/components`
|
||||
|
||||
## Tech Stack
|
||||
|
||||
- **React** 18+
|
||||
- **TypeScript** 5+
|
||||
- **Tailwind CSS** 3+
|
||||
- **Next.js** 14+ (docs only)
|
||||
- **Vite** 5+ (build tool for components/theme/utils)
|
||||
- **Turborepo** (monorepo orchestration)
|
||||
- **pnpm** (package manager)
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
@@ -0,0 +1,143 @@
|
||||
|
||||
# AI Agent Prompt: 生产级报纸组件库开发
|
||||
|
||||
## 任务目标
|
||||
开发一个浏览器端生产级报纸布局组件库,参考 InDesign,组件少而精、职责明确、无冗余、无歧义,支持全局 24 列栅格、跨栏、视觉权重和主题系统,提供文档网站(内嵌 demo,无需 storybook)。
|
||||
|
||||
---
|
||||
|
||||
## 1. 项目 setup 与架构
|
||||
|
||||
- **Monorepo 管理**(pnpm workspace / Turborepo)
|
||||
- Packages:
|
||||
1. `components` → Layout / Section / Article / Layer / Text / Media
|
||||
2. `theme` → 全局主题变量、字体、颜色、间距、视觉权重映射
|
||||
3. `docs` → 文档网站(shadcn stack + demo)
|
||||
4. `utils` → 栅格计算、跨栏逻辑、响应式辅助函数
|
||||
|
||||
- **目录结构示例**:
|
||||
```
|
||||
|
||||
/packages
|
||||
/components
|
||||
/Layout
|
||||
/Section
|
||||
/Article
|
||||
/Layer
|
||||
/Text
|
||||
/Media
|
||||
/theme
|
||||
/docs
|
||||
/utils
|
||||
|
||||
```
|
||||
|
||||
- **技术栈**:
|
||||
- React 18+, TypeScript
|
||||
- TailwindCSS + shadcn 文档 stack
|
||||
- Vitest + React Testing Library
|
||||
- 构建工具:Vite + Rollup
|
||||
|
||||
---
|
||||
|
||||
## 2. 全局栅格系统
|
||||
|
||||
- `<Layout>` 顶层容器:
|
||||
- maxWidth, padding, gutter, theme
|
||||
- 栅格总列数 24
|
||||
- snap-to-grid 支持
|
||||
- Section 内对象 span ≤ Section.columns
|
||||
- 响应式调整:小屏 12–16 列
|
||||
|
||||
---
|
||||
|
||||
## 3. 核心布局组件
|
||||
|
||||
1. `<Section>`:
|
||||
- columns, breakable, padding/margin, priority
|
||||
- 内部对象 span ≤ Section.columns
|
||||
|
||||
2. `<Article>`:
|
||||
- span, priority/weight, breakable
|
||||
- 内含 Headline / Subhead / BodyText / Image / PullQuote
|
||||
|
||||
3. `<Layer>`:
|
||||
- position, top/left/right/bottom, zIndex
|
||||
- 用于浮动广告、拉引、浮动图片
|
||||
|
||||
---
|
||||
|
||||
## 4. 内容组件
|
||||
|
||||
- 文本类:Headline / Subhead / BodyText / Quote / Byline / Caption
|
||||
- 媒体类:Image / Figure / Video / PullQuote
|
||||
- 属性:span(跨栏)、weight(High/Medium/Low)、margin/padding
|
||||
- 所有 span 基于 Section.columns
|
||||
|
||||
---
|
||||
|
||||
## 5. 视觉权重映射表(24 列)
|
||||
|
||||
| 组件 | 权重 | font-size | font-weight | line-height | color | span | margin/padding |
|
||||
|---|---|---|---|---|---|---|---|
|
||||
| Headline | High | 36–48px | 700 | 1.1 | #111 | 6–8 | 0 0 1rem 0 |
|
||||
| Headline | Medium | 28–34px | 600 | 1.2 | #111 | 4–6 | 0 0 0.75rem 0 |
|
||||
| Headline | Low | 22–26px | 500 | 1.3 | #222 | 2–4 | 0 0 0.5rem 0 |
|
||||
| Subhead | High | 20–24px | 600 | 1.25 | #222 | 2–3 | 0 0 0.5rem 0 |
|
||||
| Subhead | Medium | 16–18px | 500 | 1.3 | #333 | 1–2 | 0 0 0.25rem 0 |
|
||||
| BodyText | High | 16px | 400 | 1.5 | #333 | 1 | 0 0 1rem 0 |
|
||||
| BodyText | Medium | 14–15px | 400 | 1.5 | #444 | 1 | 0 0 0.75rem 0 |
|
||||
| BodyText | Low | 12–14px | 400 | 1.4 | #555 | 1 | 0 0 0.5rem 0 |
|
||||
| Quote | High | 20–24px | 500 | 1.4 | #222 | 2 | 0 0 0.75rem 0 |
|
||||
| Quote | Medium | 16–18px | 400 | 1.4 | #333 | 1 | 0 0 0.5rem 0 |
|
||||
| Image Caption | Standard | 12–14px | 400 | 1.3 | #555 | 1 | 0.25rem 0 |
|
||||
| PullQuote | High | 24–28px | 600 | 1.2 | #111 | 2–3 | 0 0 0.5rem 0 |
|
||||
| PullQuote | Medium | 18–20px | 500 | 1.25 | #222 | 1–2 | 0 0 0.25rem 0 |
|
||||
| Byline | Standard | 12–14px | 400 | 1.3 | #555 | 1 | 0 0 0.25rem 0 |
|
||||
|
||||
---
|
||||
|
||||
## 6. 主题系统
|
||||
|
||||
- 全局新闻字体预设
|
||||
- 颜色变量(黑/灰/强调色)
|
||||
- 间距变量(gutter, margin, padding)
|
||||
- 权重映射 Token
|
||||
- Tailwind + CSS Variables 支持主题切换
|
||||
|
||||
---
|
||||
|
||||
## 7. 布局规则
|
||||
|
||||
- Section 内对象 span ≤ Section.columns
|
||||
- 高权重对象可跨多栏
|
||||
- Layer 可浮动/绝对定位
|
||||
- breakable 控制分页断开
|
||||
- 响应式:小屏 12–16 列
|
||||
|
||||
---
|
||||
|
||||
## 8. 文档网站
|
||||
|
||||
- 技术栈:shadcn stack + Next.js + Tailwind + MDX
|
||||
- Demo 内嵌组件,展示属性、span、视觉权重、跨栏效果
|
||||
- 文档章节:
|
||||
1. 概览与目标
|
||||
2. 栅格系统与 Layout/Section 说明
|
||||
3. 核心组件(Article/Image/Layer)属性说明
|
||||
4. 文本组件属性与权重映射
|
||||
5. 主题与变量使用指南
|
||||
6. 跨栏和浮动 Layer 示例
|
||||
7. 响应式布局展示
|
||||
|
||||
---
|
||||
|
||||
## 9. 实施顺序
|
||||
|
||||
1. 初始化 monorepo + Layout / Section 基础组件
|
||||
2. 配置主题系统、视觉权重映射 CSS / Tailwind Token
|
||||
3. 开发 Article / Image / Layer / PullQuote / 文本组件
|
||||
4. 实现 Section 网格跨栏逻辑
|
||||
5. 测试生产级报纸布局:跨栏、浮动 Layer、breakable、响应式
|
||||
6. 构建文档网站,内嵌 demo 展示组件属性与效果
|
||||
7. 确认生产级排版效果符合视觉权重、跨栏、主题、响应式要求
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 376 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 63 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 383 KiB |
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"name": "newspaperui",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"description": "A newspaper-style UI component library",
|
||||
"scripts": {
|
||||
"build": "turbo run build",
|
||||
"dev": "turbo run dev",
|
||||
"lint": "turbo run lint",
|
||||
"test": "turbo run test",
|
||||
"clean": "turbo run clean && rm -rf node_modules"
|
||||
},
|
||||
"devDependencies": {
|
||||
"turbo": "^2.3.3",
|
||||
"typescript": "^5.7.2"
|
||||
},
|
||||
"packageManager": "pnpm@9.15.4",
|
||||
"engines": {
|
||||
"node": ">=18.0.0",
|
||||
"pnpm": ">=9.0.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
{
|
||||
"name": "@newspaperui/components",
|
||||
"version": "0.0.0",
|
||||
"description": "React components for newspaperui",
|
||||
"type": "module",
|
||||
"main": "./dist/index.cjs",
|
||||
"module": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"import": "./dist/index.js",
|
||||
"require": "./dist/index.cjs",
|
||||
"types": "./dist/index.d.ts"
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "vite build",
|
||||
"dev": "vite build --watch",
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest",
|
||||
"lint": "tsc --noEmit",
|
||||
"clean": "rm -rf dist"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@newspaperui/theme": "workspace:*",
|
||||
"@newspaperui/utils": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@testing-library/react": "^16.1.0",
|
||||
"@testing-library/jest-dom": "^6.6.3",
|
||||
"@types/react": "^18.3.18",
|
||||
"@types/react-dom": "^18.3.5",
|
||||
"@vitejs/plugin-react": "^4.3.4",
|
||||
"jsdom": "^25.0.1",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"typescript": "^5.7.2",
|
||||
"vite": "^5.4.11",
|
||||
"vite-plugin-dts": "^4.3.0",
|
||||
"vitest": "^2.1.8"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import React from 'react';
|
||||
import { useSection } from '../Section/Section';
|
||||
export interface ArticleProps {
|
||||
span: number;
|
||||
priority?: 'High' | 'Medium' | 'Low';
|
||||
breakable?: boolean;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export const Article: React.FC<ArticleProps> = ({
|
||||
span,
|
||||
priority = 'Medium',
|
||||
breakable = true,
|
||||
children,
|
||||
}) => {
|
||||
const section = useSection();
|
||||
|
||||
const effectiveSpan = Math.min(Math.max(1, span), section.columns);
|
||||
if (span !== effectiveSpan) {
|
||||
console.warn(`[Article] span=${span} exceeds section.columns=${section.columns}, clamped to ${effectiveSpan}`);
|
||||
}
|
||||
|
||||
return (
|
||||
<article
|
||||
className={`newspaper-article priority-${priority.toLowerCase()}`}
|
||||
style={{
|
||||
gridColumn: `span ${effectiveSpan}`,
|
||||
breakInside: breakable ? 'auto' : 'avoid',
|
||||
}}
|
||||
data-span={span}
|
||||
>
|
||||
{children}
|
||||
</article>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,37 @@
|
||||
import React from 'react';
|
||||
|
||||
export interface LayerProps {
|
||||
position: 'absolute' | 'fixed' | 'sticky';
|
||||
top?: string;
|
||||
left?: string;
|
||||
right?: string;
|
||||
bottom?: string;
|
||||
zIndex?: number;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export const Layer: React.FC<LayerProps> = ({
|
||||
position,
|
||||
top,
|
||||
left,
|
||||
right,
|
||||
bottom,
|
||||
zIndex = 10,
|
||||
children,
|
||||
}) => {
|
||||
return (
|
||||
<div
|
||||
className="newspaper-layer"
|
||||
style={{
|
||||
position,
|
||||
top,
|
||||
left,
|
||||
right,
|
||||
bottom,
|
||||
zIndex,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,47 @@
|
||||
import React, { createContext, useContext } from 'react';
|
||||
|
||||
export interface LayoutProps {
|
||||
maxWidth?: string;
|
||||
padding?: string;
|
||||
gutter?: number;
|
||||
theme?: 'light' | 'dark';
|
||||
columns?: number;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
interface LayoutContextValue {
|
||||
columns: number;
|
||||
gutter: number;
|
||||
}
|
||||
|
||||
const LayoutContext = createContext<LayoutContextValue>({
|
||||
columns: 24,
|
||||
gutter: 16,
|
||||
});
|
||||
|
||||
export const useLayout = () => useContext(LayoutContext);
|
||||
|
||||
export const Layout: React.FC<LayoutProps> = ({
|
||||
maxWidth = '1440px',
|
||||
padding = '1rem',
|
||||
gutter = 16,
|
||||
theme = 'light',
|
||||
columns = 24,
|
||||
children,
|
||||
}) => {
|
||||
return (
|
||||
<LayoutContext.Provider value={{ columns, gutter }}>
|
||||
<div
|
||||
className={`newspaper-layout ${theme === 'dark' ? 'dark' : ''}`}
|
||||
style={{
|
||||
maxWidth,
|
||||
padding,
|
||||
margin: '0 auto',
|
||||
'--gutter': `${gutter}px`,
|
||||
} as React.CSSProperties}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</LayoutContext.Provider>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,43 @@
|
||||
import React from 'react';
|
||||
import { calculateSpanWidth } from '@newspaperui/utils';
|
||||
import { useSection } from '../Section/Section';
|
||||
import { Caption } from '../Text/Caption';
|
||||
|
||||
export interface FigureProps {
|
||||
src: string;
|
||||
alt: string;
|
||||
caption?: string;
|
||||
span?: number;
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
export const Figure: React.FC<FigureProps> = ({
|
||||
src,
|
||||
alt,
|
||||
caption,
|
||||
span = 1,
|
||||
children,
|
||||
}) => {
|
||||
const section = useSection();
|
||||
const width = calculateSpanWidth(span, section.columns);
|
||||
|
||||
return (
|
||||
<figure
|
||||
className="newspaper-figure"
|
||||
style={{ width, margin: 0 }}
|
||||
data-span={span}
|
||||
>
|
||||
<img
|
||||
src={src}
|
||||
alt={alt}
|
||||
style={{
|
||||
width: '100%',
|
||||
height: 'auto',
|
||||
display: 'block',
|
||||
}}
|
||||
/>
|
||||
{caption && <Caption>{caption}</Caption>}
|
||||
{children}
|
||||
</figure>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,42 @@
|
||||
import React from 'react';
|
||||
import { calculateSpanWidth } from '@newspaperui/utils';
|
||||
import { useSection } from '../Section/Section';
|
||||
import { Caption } from '../Text/Caption';
|
||||
|
||||
export interface ImageProps {
|
||||
src: string;
|
||||
alt: string;
|
||||
span?: number;
|
||||
caption?: string;
|
||||
priority?: 'High' | 'Medium' | 'Low';
|
||||
}
|
||||
|
||||
export const Image: React.FC<ImageProps> = ({
|
||||
src,
|
||||
alt,
|
||||
span = 1,
|
||||
caption,
|
||||
priority = 'Medium',
|
||||
}) => {
|
||||
const section = useSection();
|
||||
const width = calculateSpanWidth(span, section.columns);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`newspaper-image priority-${priority.toLowerCase()}`}
|
||||
style={{ width }}
|
||||
data-span={span}
|
||||
>
|
||||
<img
|
||||
src={src}
|
||||
alt={alt}
|
||||
style={{
|
||||
width: '100%',
|
||||
height: 'auto',
|
||||
display: 'block',
|
||||
}}
|
||||
/>
|
||||
{caption && <Caption>{caption}</Caption>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,62 @@
|
||||
import React from 'react';
|
||||
import { visualWeights, resolveFontSize } from '@newspaperui/theme';
|
||||
import { calculateSpanWidth } from '@newspaperui/utils';
|
||||
import { useSection } from '../Section/Section';
|
||||
|
||||
export interface PullQuoteProps {
|
||||
weight?: 'High' | 'Medium';
|
||||
span?: number;
|
||||
author?: string;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export const PullQuote: React.FC<PullQuoteProps> = ({
|
||||
weight = 'High',
|
||||
span,
|
||||
author,
|
||||
children,
|
||||
}) => {
|
||||
const section = useSection();
|
||||
const config = visualWeights.PullQuote[weight];
|
||||
|
||||
if (!config) {
|
||||
throw new Error(`Invalid weight: ${weight} for PullQuote`);
|
||||
}
|
||||
|
||||
const finalSpan = span || (Array.isArray(config.span) ? config.span[0] : config.span);
|
||||
const width = calculateSpanWidth(finalSpan, section.columns);
|
||||
|
||||
return (
|
||||
<aside
|
||||
className="newspaper-pull-quote"
|
||||
style={{
|
||||
fontSize: resolveFontSize(config.fontSize),
|
||||
fontWeight: config.fontWeight,
|
||||
lineHeight: config.lineHeight,
|
||||
color: config.color,
|
||||
margin: config.margin,
|
||||
width,
|
||||
padding: 'var(--nui-padding-lg, 1rem)',
|
||||
borderLeft: '4px solid var(--nui-color-accent-primary, currentColor)',
|
||||
}}
|
||||
data-weight={weight}
|
||||
data-span={finalSpan}
|
||||
>
|
||||
<blockquote style={{ margin: 0 }}>
|
||||
{children}
|
||||
</blockquote>
|
||||
{author && (
|
||||
<cite
|
||||
style={{
|
||||
display: 'block',
|
||||
marginTop: '0.5rem',
|
||||
fontSize: '0.875em',
|
||||
fontStyle: 'normal',
|
||||
}}
|
||||
>
|
||||
— {author}
|
||||
</cite>
|
||||
)}
|
||||
</aside>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,41 @@
|
||||
import React from 'react';
|
||||
import { calculateSpanWidth } from '@newspaperui/utils';
|
||||
import { useSection } from '../Section/Section';
|
||||
import { Caption } from '../Text/Caption';
|
||||
|
||||
export interface VideoProps {
|
||||
src: string;
|
||||
poster?: string;
|
||||
span?: number;
|
||||
caption?: string;
|
||||
}
|
||||
|
||||
export const Video: React.FC<VideoProps> = ({
|
||||
src,
|
||||
poster,
|
||||
span = 1,
|
||||
caption,
|
||||
}) => {
|
||||
const section = useSection();
|
||||
const width = calculateSpanWidth(span, section.columns);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="newspaper-video"
|
||||
style={{ width }}
|
||||
data-span={span}
|
||||
>
|
||||
<video
|
||||
src={src}
|
||||
poster={poster}
|
||||
controls
|
||||
style={{
|
||||
width: '100%',
|
||||
height: 'auto',
|
||||
display: 'block',
|
||||
}}
|
||||
/>
|
||||
{caption && <Caption>{caption}</Caption>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,54 @@
|
||||
import React, { createContext, useContext } from 'react';
|
||||
import { useLayout } from '../Layout/Layout';
|
||||
|
||||
export interface SectionProps {
|
||||
columns: number;
|
||||
breakable?: boolean;
|
||||
padding?: string;
|
||||
margin?: string;
|
||||
priority?: 'High' | 'Medium' | 'Low';
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
interface SectionContextValue {
|
||||
columns: number;
|
||||
}
|
||||
|
||||
const SectionContext = createContext<SectionContextValue>({ columns: 24 });
|
||||
|
||||
export const useSection = () => useContext(SectionContext);
|
||||
|
||||
export const Section: React.FC<SectionProps> = ({
|
||||
columns,
|
||||
breakable = true,
|
||||
padding,
|
||||
margin,
|
||||
priority = 'Medium',
|
||||
children,
|
||||
}) => {
|
||||
const layout = useLayout();
|
||||
|
||||
const effectiveColumns = Math.min(Math.max(1, columns), layout.columns);
|
||||
if (columns !== effectiveColumns) {
|
||||
console.warn(`[Section] columns=${columns} exceeds layout.columns=${layout.columns}, clamped to ${effectiveColumns}`);
|
||||
}
|
||||
|
||||
return (
|
||||
<SectionContext.Provider value={{ columns: effectiveColumns }}>
|
||||
<section
|
||||
className={`newspaper-section priority-${priority.toLowerCase()}`}
|
||||
style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: `repeat(${effectiveColumns}, 1fr)`,
|
||||
gap: 'var(--nui-gutter, 1rem)',
|
||||
padding,
|
||||
margin,
|
||||
breakInside: breakable ? 'auto' : 'avoid',
|
||||
}}
|
||||
data-columns={columns}
|
||||
>
|
||||
{children}
|
||||
</section>
|
||||
</SectionContext.Provider>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,44 @@
|
||||
import React from 'react';
|
||||
import { visualWeights, resolveFontSize } from '@newspaperui/theme';
|
||||
import { calculateSpanWidth } from '@newspaperui/utils';
|
||||
import { useSection } from '../Section/Section';
|
||||
|
||||
export interface BodyTextProps {
|
||||
weight?: 'High' | 'Medium' | 'Low';
|
||||
span?: number;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export const BodyText: React.FC<BodyTextProps> = ({
|
||||
weight = 'Medium',
|
||||
span,
|
||||
children,
|
||||
}) => {
|
||||
const section = useSection();
|
||||
const config = visualWeights.BodyText[weight];
|
||||
|
||||
if (!config) {
|
||||
throw new Error(`Invalid weight: ${weight} for BodyText`);
|
||||
}
|
||||
|
||||
const finalSpan = span || (Array.isArray(config.span) ? config.span[0] : config.span);
|
||||
const width = calculateSpanWidth(finalSpan, section.columns);
|
||||
|
||||
return (
|
||||
<p
|
||||
className="newspaper-body-text"
|
||||
style={{
|
||||
fontSize: resolveFontSize(config.fontSize),
|
||||
fontWeight: config.fontWeight,
|
||||
lineHeight: config.lineHeight,
|
||||
color: config.color,
|
||||
margin: config.margin,
|
||||
width,
|
||||
}}
|
||||
data-weight={weight}
|
||||
data-span={finalSpan}
|
||||
>
|
||||
{children}
|
||||
</p>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,29 @@
|
||||
import React from 'react';
|
||||
import { visualWeights, resolveFontSize } from '@newspaperui/theme';
|
||||
|
||||
export interface BylineProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export const Byline: React.FC<BylineProps> = ({ children }) => {
|
||||
const config = visualWeights.Byline.Standard;
|
||||
|
||||
if (!config) {
|
||||
throw new Error('Byline configuration not found');
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="newspaper-byline"
|
||||
style={{
|
||||
fontSize: resolveFontSize(config.fontSize),
|
||||
fontWeight: config.fontWeight,
|
||||
lineHeight: config.lineHeight,
|
||||
color: config.color,
|
||||
margin: config.margin,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,29 @@
|
||||
import React from 'react';
|
||||
import { visualWeights, resolveFontSize } from '@newspaperui/theme';
|
||||
|
||||
export interface CaptionProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export const Caption: React.FC<CaptionProps> = ({ children }) => {
|
||||
const config = visualWeights.Caption.Standard;
|
||||
|
||||
if (!config) {
|
||||
throw new Error('Caption configuration not found');
|
||||
}
|
||||
|
||||
return (
|
||||
<figcaption
|
||||
className="newspaper-caption"
|
||||
style={{
|
||||
fontSize: resolveFontSize(config.fontSize),
|
||||
fontWeight: config.fontWeight,
|
||||
lineHeight: config.lineHeight,
|
||||
color: config.color,
|
||||
margin: config.margin,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</figcaption>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,53 @@
|
||||
import React from 'react';
|
||||
import { visualWeights, resolveFontSize } from '@newspaperui/theme';
|
||||
import { calculateSpanWidth } from '@newspaperui/utils';
|
||||
import { useSection } from '../Section/Section';
|
||||
|
||||
const weightToTag: Record<'High' | 'Medium' | 'Low', 'h1' | 'h2' | 'h3'> = {
|
||||
High: 'h1',
|
||||
Medium: 'h2',
|
||||
Low: 'h3',
|
||||
};
|
||||
|
||||
export interface HeadlineProps {
|
||||
weight?: 'High' | 'Medium' | 'Low';
|
||||
span?: number;
|
||||
as?: 'h1' | 'h2' | 'h3' | 'h4';
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export const Headline: React.FC<HeadlineProps> = ({
|
||||
weight = 'High',
|
||||
span,
|
||||
as,
|
||||
children,
|
||||
}) => {
|
||||
const section = useSection();
|
||||
const config = visualWeights.Headline[weight];
|
||||
|
||||
if (!config) {
|
||||
throw new Error(`Invalid weight: ${weight} for Headline`);
|
||||
}
|
||||
|
||||
const finalSpan = span || (Array.isArray(config.span) ? config.span[0] : config.span);
|
||||
const width = calculateSpanWidth(finalSpan, section.columns);
|
||||
const Tag = as ?? weightToTag[weight];
|
||||
|
||||
return (
|
||||
<Tag
|
||||
className="newspaper-headline"
|
||||
style={{
|
||||
fontSize: resolveFontSize(config.fontSize),
|
||||
fontWeight: config.fontWeight,
|
||||
lineHeight: config.lineHeight,
|
||||
color: config.color,
|
||||
margin: config.margin,
|
||||
width,
|
||||
}}
|
||||
data-weight={weight}
|
||||
data-span={finalSpan}
|
||||
>
|
||||
{children}
|
||||
</Tag>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,44 @@
|
||||
import React from 'react';
|
||||
import { visualWeights, resolveFontSize } from '@newspaperui/theme';
|
||||
import { calculateSpanWidth } from '@newspaperui/utils';
|
||||
import { useSection } from '../Section/Section';
|
||||
|
||||
export interface QuoteProps {
|
||||
weight?: 'High' | 'Medium';
|
||||
span?: number;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export const Quote: React.FC<QuoteProps> = ({
|
||||
weight = 'Medium',
|
||||
span,
|
||||
children,
|
||||
}) => {
|
||||
const section = useSection();
|
||||
const config = visualWeights.Quote[weight];
|
||||
|
||||
if (!config) {
|
||||
throw new Error(`Invalid weight: ${weight} for Quote`);
|
||||
}
|
||||
|
||||
const finalSpan = span || (Array.isArray(config.span) ? config.span[0] : config.span);
|
||||
const width = calculateSpanWidth(finalSpan, section.columns);
|
||||
|
||||
return (
|
||||
<blockquote
|
||||
className="newspaper-quote"
|
||||
style={{
|
||||
fontSize: resolveFontSize(config.fontSize),
|
||||
fontWeight: config.fontWeight,
|
||||
lineHeight: config.lineHeight,
|
||||
color: config.color,
|
||||
margin: config.margin,
|
||||
width,
|
||||
}}
|
||||
data-weight={weight}
|
||||
data-span={finalSpan}
|
||||
>
|
||||
{children}
|
||||
</blockquote>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,44 @@
|
||||
import React from 'react';
|
||||
import { visualWeights, resolveFontSize } from '@newspaperui/theme';
|
||||
import { calculateSpanWidth } from '@newspaperui/utils';
|
||||
import { useSection } from '../Section/Section';
|
||||
|
||||
export interface SubheadProps {
|
||||
weight?: 'High' | 'Medium';
|
||||
span?: number;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export const Subhead: React.FC<SubheadProps> = ({
|
||||
weight = 'Medium',
|
||||
span,
|
||||
children,
|
||||
}) => {
|
||||
const section = useSection();
|
||||
const config = visualWeights.Subhead[weight];
|
||||
|
||||
if (!config) {
|
||||
throw new Error(`Invalid weight: ${weight} for Subhead`);
|
||||
}
|
||||
|
||||
const finalSpan = span || (Array.isArray(config.span) ? config.span[0] : config.span);
|
||||
const width = calculateSpanWidth(finalSpan, section.columns);
|
||||
|
||||
return (
|
||||
<h2
|
||||
className="newspaper-subhead"
|
||||
style={{
|
||||
fontSize: resolveFontSize(config.fontSize),
|
||||
fontWeight: config.fontWeight,
|
||||
lineHeight: config.lineHeight,
|
||||
color: config.color,
|
||||
margin: config.margin,
|
||||
width,
|
||||
}}
|
||||
data-weight={weight}
|
||||
data-span={finalSpan}
|
||||
>
|
||||
{children}
|
||||
</h2>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,51 @@
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { render } from '@testing-library/react';
|
||||
import { Layout } from '../Layout/Layout';
|
||||
import { Section } from '../Section/Section';
|
||||
import { Article } from '../Article/Article';
|
||||
|
||||
describe('Article Component', () => {
|
||||
it('renders children correctly', () => {
|
||||
const { container } = render(
|
||||
<Layout>
|
||||
<Section columns={12}>
|
||||
<Article span={6}>
|
||||
<div>Article Content</div>
|
||||
</Article>
|
||||
</Section>
|
||||
</Layout>
|
||||
);
|
||||
expect(container.textContent).toContain('Article Content');
|
||||
});
|
||||
|
||||
it('clamps span exceeding section columns and warns', () => {
|
||||
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
||||
render(
|
||||
<Layout>
|
||||
<Section columns={12}>
|
||||
<Article span={15}>
|
||||
<div>Clamped</div>
|
||||
</Article>
|
||||
</Section>
|
||||
</Layout>
|
||||
);
|
||||
expect(warnSpy).toHaveBeenCalledWith(
|
||||
'[Article] span=15 exceeds section.columns=12, clamped to 12'
|
||||
);
|
||||
warnSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('applies correct grid-column span based on span', () => {
|
||||
const { container } = render(
|
||||
<Layout>
|
||||
<Section columns={12}>
|
||||
<Article span={6}>
|
||||
<div>Half Width</div>
|
||||
</Article>
|
||||
</Section>
|
||||
</Layout>
|
||||
);
|
||||
const article = container.querySelector('.newspaper-article');
|
||||
expect(article).toHaveStyle({ gridColumn: 'span 6' });
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,36 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { Layout } from '../Layout/Layout';
|
||||
import { Section } from '../Section/Section';
|
||||
|
||||
describe('Layout Component', () => {
|
||||
it('renders children correctly', () => {
|
||||
render(
|
||||
<Layout>
|
||||
<div>Test Content</div>
|
||||
</Layout>
|
||||
);
|
||||
expect(screen.getByText('Test Content')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('applies custom maxWidth and padding', () => {
|
||||
const { container } = render(
|
||||
<Layout maxWidth="1200px" padding="2rem">
|
||||
<div>Content</div>
|
||||
</Layout>
|
||||
);
|
||||
const layout = container.querySelector('.newspaper-layout');
|
||||
expect(layout).toHaveStyle({ maxWidth: '1200px', padding: '2rem' });
|
||||
});
|
||||
|
||||
it('provides default columns value of 24', () => {
|
||||
render(
|
||||
<Layout>
|
||||
<Section columns={12}>
|
||||
<div>Test</div>
|
||||
</Section>
|
||||
</Layout>
|
||||
);
|
||||
expect(screen.getByText('Test')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,46 @@
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { render } from '@testing-library/react';
|
||||
import { Layout } from '../Layout/Layout';
|
||||
import { Section } from '../Section/Section';
|
||||
|
||||
describe('Section Component', () => {
|
||||
it('renders children correctly', () => {
|
||||
const { container } = render(
|
||||
<Layout>
|
||||
<Section columns={12}>
|
||||
<div>Section Content</div>
|
||||
</Section>
|
||||
</Layout>
|
||||
);
|
||||
expect(container.textContent).toContain('Section Content');
|
||||
});
|
||||
|
||||
it('clamps columns exceeding layout columns and warns', () => {
|
||||
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
||||
const { container } = render(
|
||||
<Layout columns={24}>
|
||||
<Section columns={30}>
|
||||
<div>Clamped</div>
|
||||
</Section>
|
||||
</Layout>
|
||||
);
|
||||
expect(warnSpy).toHaveBeenCalledWith(
|
||||
'[Section] columns=30 exceeds layout.columns=24, clamped to 24'
|
||||
);
|
||||
const section = container.querySelector('.newspaper-section');
|
||||
expect(section).toHaveStyle({ gridTemplateColumns: 'repeat(24, 1fr)' });
|
||||
warnSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('applies custom padding and margin', () => {
|
||||
const { container } = render(
|
||||
<Layout>
|
||||
<Section columns={12} padding="1rem" margin="2rem">
|
||||
<div>Content</div>
|
||||
</Section>
|
||||
</Layout>
|
||||
);
|
||||
const section = container.querySelector('.newspaper-section');
|
||||
expect(section).toHaveStyle({ padding: '1rem', margin: '2rem' });
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1 @@
|
||||
import '@testing-library/jest-dom';
|
||||
@@ -0,0 +1,51 @@
|
||||
/**
|
||||
* @newspaperui/components
|
||||
* React components for newspaperui
|
||||
*/
|
||||
|
||||
// Layout components
|
||||
export { Layout, useLayout } from './Layout/Layout';
|
||||
export type { LayoutProps } from './Layout/Layout';
|
||||
|
||||
export { Section, useSection } from './Section/Section';
|
||||
export type { SectionProps } from './Section/Section';
|
||||
|
||||
export { Article } from './Article/Article';
|
||||
export type { ArticleProps } from './Article/Article';
|
||||
|
||||
export { Layer } from './Layer/Layer';
|
||||
export type { LayerProps } from './Layer/Layer';
|
||||
|
||||
// Text components
|
||||
export { Headline } from './Text/Headline';
|
||||
export type { HeadlineProps } from './Text/Headline';
|
||||
|
||||
export { Subhead } from './Text/Subhead';
|
||||
export type { SubheadProps } from './Text/Subhead';
|
||||
|
||||
export { BodyText } from './Text/BodyText';
|
||||
export type { BodyTextProps } from './Text/BodyText';
|
||||
|
||||
export { Quote } from './Text/Quote';
|
||||
export type { QuoteProps } from './Text/Quote';
|
||||
|
||||
export { Byline } from './Text/Byline';
|
||||
export type { BylineProps } from './Text/Byline';
|
||||
|
||||
export { Caption } from './Text/Caption';
|
||||
export type { CaptionProps } from './Text/Caption';
|
||||
|
||||
// Media components
|
||||
export { Image } from './Media/Image';
|
||||
export type { ImageProps } from './Media/Image';
|
||||
|
||||
export { Figure } from './Media/Figure';
|
||||
export type { FigureProps } from './Media/Figure';
|
||||
|
||||
export { Video } from './Media/Video';
|
||||
export type { VideoProps } from './Media/Video';
|
||||
|
||||
export { PullQuote } from './Media/PullQuote';
|
||||
export type { PullQuoteProps } from './Media/PullQuote';
|
||||
|
||||
export const version = '0.0.0';
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src"
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import react from '@vitejs/plugin-react';
|
||||
import { resolve } from 'path';
|
||||
import dts from 'vite-plugin-dts';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
react(),
|
||||
dts({
|
||||
insertTypesEntry: true,
|
||||
}),
|
||||
],
|
||||
build: {
|
||||
lib: {
|
||||
entry: resolve(__dirname, 'src/index.ts'),
|
||||
name: 'NewspaperUIComponents',
|
||||
formats: ['es', 'cjs'],
|
||||
fileName: (format) => `index.${format === 'es' ? 'js' : 'cjs'}`,
|
||||
},
|
||||
rollupOptions: {
|
||||
external: ['react', 'react-dom', 'react/jsx-runtime'],
|
||||
output: {
|
||||
globals: {
|
||||
react: 'React',
|
||||
'react-dom': 'ReactDOM',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': resolve(__dirname, './src'),
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,11 @@
|
||||
import { defineConfig } from 'vitest/config';
|
||||
import react from '@vitejs/plugin-react';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
test: {
|
||||
globals: true,
|
||||
environment: 'jsdom',
|
||||
setupFiles: ['./src/__tests__/setup.ts'],
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,192 @@
|
||||
'use client';
|
||||
|
||||
import { Demo } from '@/components/Demo';
|
||||
import { PropsTable } from '@/components/PropsTable';
|
||||
import { Layout, Section, Article, Headline, BodyText, Subhead } from '@newspaperui/components';
|
||||
|
||||
export default function ArticlePage() {
|
||||
return (
|
||||
<div className="prose prose-lg max-w-none">
|
||||
<h1>Article 组件</h1>
|
||||
<p className="text-xl text-gray-600">
|
||||
Article 是内容容器组件,用于组织文章内容,支持跨栏布局和优先级控制。
|
||||
</p>
|
||||
|
||||
<h2>组件说明</h2>
|
||||
<p>
|
||||
<code>Article</code> 组件是报纸布局中的基本内容单元,可以包含标题、正文、图片等多种内容。
|
||||
通过 span 属性控制文章占据的列数,实现灵活的跨栏布局。
|
||||
</p>
|
||||
|
||||
<h2>API 文档</h2>
|
||||
<PropsTable
|
||||
data={[
|
||||
{
|
||||
name: 'span',
|
||||
type: 'number',
|
||||
required: true,
|
||||
description: '文章占据的列数,必须 ≤ 所在 Section 的 columns',
|
||||
},
|
||||
{
|
||||
name: 'priority',
|
||||
type: 'number',
|
||||
default: '0',
|
||||
description: '优先级,数值越大优先级越高,影响排序',
|
||||
},
|
||||
{
|
||||
name: 'weight',
|
||||
type: '"High" | "Medium" | "Low"',
|
||||
default: '"Medium"',
|
||||
description: '视觉权重,影响内部文本组件的默认样式',
|
||||
},
|
||||
{
|
||||
name: 'breakable',
|
||||
type: 'boolean',
|
||||
default: 'true',
|
||||
description: '是否允许在此处分页断开',
|
||||
},
|
||||
{
|
||||
name: 'className',
|
||||
type: 'string',
|
||||
description: '自定义 CSS 类名',
|
||||
},
|
||||
{
|
||||
name: 'children',
|
||||
type: 'React.ReactNode',
|
||||
required: true,
|
||||
description: '文章内容',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
<h2>基础用法</h2>
|
||||
<Demo
|
||||
title="单列文章"
|
||||
description="创建一个占据 8 列的文章"
|
||||
code={`<Layout columns={24}>
|
||||
<Section columns={24}>
|
||||
<Article span={8}>
|
||||
<Headline weight="High">文章标题</Headline>
|
||||
<Subhead>副标题说明</Subhead>
|
||||
<BodyText>
|
||||
这是文章的正文内容。Article 组件提供了内容容器,
|
||||
可以包含多种文本和媒体组件。
|
||||
</BodyText>
|
||||
</Article>
|
||||
</Section>
|
||||
</Layout>`}
|
||||
>
|
||||
<Layout columns={24}>
|
||||
<Section columns={24}>
|
||||
<Article span={8}>
|
||||
<Headline weight="High">文章标题</Headline>
|
||||
<Subhead>副标题说明</Subhead>
|
||||
<BodyText>
|
||||
这是文章的正文内容。Article 组件提供了内容容器,
|
||||
可以包含多种文本和媒体组件。
|
||||
</BodyText>
|
||||
</Article>
|
||||
</Section>
|
||||
</Layout>
|
||||
</Demo>
|
||||
|
||||
<h2>跨栏布局</h2>
|
||||
<p>
|
||||
通过调整 span 属性,可以实现不同宽度的文章布局。较大的 span 值适合重要内容,
|
||||
较小的 span 值适合次要内容或侧边栏。
|
||||
</p>
|
||||
|
||||
<Demo
|
||||
title="多列文章并排"
|
||||
description="在 24 列布局中创建三个 8 列的文章"
|
||||
code={`<Layout columns={24}>
|
||||
<Section columns={24}>
|
||||
<Article span={8}>
|
||||
<Headline weight="High">主要文章</Headline>
|
||||
<BodyText>占据 8 列的主要内容</BodyText>
|
||||
</Article>
|
||||
<Article span={8}>
|
||||
<Headline weight="Medium">次要文章</Headline>
|
||||
<BodyText>占据 8 列的次要内容</BodyText>
|
||||
</Article>
|
||||
<Article span={8}>
|
||||
<Headline weight="Medium">更多内容</Headline>
|
||||
<BodyText>占据 8 列的更多内容</BodyText>
|
||||
</Article>
|
||||
</Section>
|
||||
</Layout>`}
|
||||
>
|
||||
<Layout columns={24}>
|
||||
<Section columns={24}>
|
||||
<Article span={8}>
|
||||
<Headline weight="High">主要文章</Headline>
|
||||
<BodyText>占据 8 列的主要内容,使用 High 权重突出显示。</BodyText>
|
||||
</Article>
|
||||
<Article span={8}>
|
||||
<Headline weight="Medium">次要文章</Headline>
|
||||
<BodyText>占据 8 列的次要内容,使用 Medium 权重。</BodyText>
|
||||
</Article>
|
||||
<Article span={8}>
|
||||
<Headline weight="Medium">更多内容</Headline>
|
||||
<BodyText>占据 8 列的更多内容,与其他文章并排显示。</BodyText>
|
||||
</Article>
|
||||
</Section>
|
||||
</Layout>
|
||||
</Demo>
|
||||
|
||||
<h2>不同宽度组合</h2>
|
||||
<Demo
|
||||
title="混合宽度布局"
|
||||
description="组合不同宽度的文章实现复杂布局"
|
||||
code={`<Layout columns={24}>
|
||||
<Section columns={24}>
|
||||
<Article span={16}>
|
||||
<Headline weight="High">宽文章 (16 列)</Headline>
|
||||
<BodyText>
|
||||
这是一篇占据 16 列的宽文章,适合放置重要内容或长篇文章。
|
||||
</BodyText>
|
||||
</Article>
|
||||
<Article span={8}>
|
||||
<Headline weight="Low">窄文章 (8 列)</Headline>
|
||||
<BodyText>这是占据 8 列的窄文章,适合侧边栏或简短内容。</BodyText>
|
||||
</Article>
|
||||
</Section>
|
||||
</Layout>`}
|
||||
>
|
||||
<Layout columns={24}>
|
||||
<Section columns={24}>
|
||||
<Article span={16}>
|
||||
<Headline weight="High">宽文章 (16 列)</Headline>
|
||||
<BodyText>
|
||||
这是一篇占据 16 列的宽文章,适合放置重要内容或长篇文章。
|
||||
更宽的列宽提供了更好的阅读体验。
|
||||
</BodyText>
|
||||
</Article>
|
||||
<Article span={8}>
|
||||
<Headline weight="Low">窄文章 (8 列)</Headline>
|
||||
<BodyText>这是占据 8 列的窄文章,适合侧边栏或简短内容。</BodyText>
|
||||
</Article>
|
||||
</Section>
|
||||
</Layout>
|
||||
</Demo>
|
||||
|
||||
<h2>使用建议</h2>
|
||||
<ul>
|
||||
<li>主要文章建议使用 12-16 列,提供良好的阅读宽度</li>
|
||||
<li>次要文章或侧边栏使用 6-8 列</li>
|
||||
<li>确保同一 Section 内所有 Article 的 span 总和 ≤ Section.columns</li>
|
||||
<li>使用 priority 属性控制文章的显示顺序</li>
|
||||
<li>通过 weight 属性影响内部文本组件的默认样式</li>
|
||||
<li>重要内容使用更大的 span 值以获得更多视觉权重</li>
|
||||
</ul>
|
||||
|
||||
<h2>注意事项</h2>
|
||||
<ul>
|
||||
<li>Article 的 span 值必须小于或等于所在 Section 的 columns 值</li>
|
||||
<li>如果 Section 内多个 Article 的 span 总和超过 Section.columns,会自动换行</li>
|
||||
<li>在响应式布局中,Article 会根据屏幕尺寸自动调整宽���</li>
|
||||
<li>breakable 属性在打印或分页场景中很有用</li>
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
'use client';
|
||||
|
||||
import { Demo } from '@/components/Demo';
|
||||
import { PropsTable } from '@/components/PropsTable';
|
||||
import { Layout, Section, Article, Layer, Headline, BodyText } from '@newspaperui/components';
|
||||
|
||||
export default function LayerPage() {
|
||||
return (
|
||||
<div className="prose prose-lg max-w-none">
|
||||
<h1>Layer 组件</h1>
|
||||
<p className="text-xl text-gray-600">
|
||||
Layer 是浮动层组件,支持绝对定位,用于实现浮动广告、拉引、浮动图片等效果。
|
||||
</p>
|
||||
|
||||
<h2>组件说明</h2>
|
||||
<p>
|
||||
<code>Layer</code> 组件脱离正常的栅格流,使用绝对定位。
|
||||
它可以浮动在其他内容之上,常用于实现报纸中的浮动元素,如拉引、浮动广告、浮动图片等。
|
||||
</p>
|
||||
|
||||
<h2>API 文档</h2>
|
||||
<PropsTable
|
||||
data={[
|
||||
{
|
||||
name: 'position',
|
||||
type: '"absolute" | "fixed" | "sticky"',
|
||||
default: '"absolute"',
|
||||
description: 'CSS position 属性',
|
||||
},
|
||||
{
|
||||
name: 'top',
|
||||
type: 'string | number',
|
||||
description: '距离顶部的距离',
|
||||
},
|
||||
{
|
||||
name: 'right',
|
||||
type: 'string | number',
|
||||
description: '距离右侧的距离',
|
||||
},
|
||||
{
|
||||
name: 'bottom',
|
||||
type: 'string | number',
|
||||
description: '距离底部的距离',
|
||||
},
|
||||
{
|
||||
name: 'left',
|
||||
type: 'string | number',
|
||||
description: '距离左侧的距离',
|
||||
},
|
||||
{
|
||||
name: 'zIndex',
|
||||
type: 'number',
|
||||
default: '10',
|
||||
description: 'z-index 层级',
|
||||
},
|
||||
{
|
||||
name: 'className',
|
||||
type: 'string',
|
||||
description: '自定义 CSS 类名',
|
||||
},
|
||||
{
|
||||
name: 'children',
|
||||
type: 'React.ReactNode',
|
||||
required: true,
|
||||
description: '浮动层内容',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
<h2>基础用法</h2>
|
||||
<Demo
|
||||
title="浮动拉引"
|
||||
description="在文章旁边添加浮动的拉引文字"
|
||||
code={`<Layout columns={24}>
|
||||
<Section columns={24}>
|
||||
<div style={{ position: 'relative', minHeight: '300px' }}>
|
||||
<Article span={16}>
|
||||
<Headline weight="High">主要文章</Headline>
|
||||
<BodyText>
|
||||
这是一篇主要文章的内容。旁边有一个浮动的拉引,
|
||||
用于突出显示文章中的重要引用或观点。
|
||||
</BodyText>
|
||||
</Article>
|
||||
|
||||
<Layer position="absolute" top="20px" right="20px" zIndex={10}>
|
||||
<div className="bg-yellow-50 border-l-4 border-yellow-400 p-4 max-w-xs">
|
||||
<p className="text-lg font-semibold text-gray-800">
|
||||
"这是一段重要的引用文字"
|
||||
</p>
|
||||
</div>
|
||||
</Layer>
|
||||
</div>
|
||||
</Section>
|
||||
</Layout>`}
|
||||
>
|
||||
<Layout columns={24}>
|
||||
<Section columns={24}>
|
||||
<div style={{ position: 'relative', minHeight: '300px' }}>
|
||||
<Article span={16}>
|
||||
<Headline weight="High">主要文章</Headline>
|
||||
<BodyText>
|
||||
这是一篇主要文章的内容。旁边有一个浮动的拉引,
|
||||
用于突出显示文章中的重要引用或观点。Layer 组件使用绝对定位,
|
||||
可以精确控制元素的位置。
|
||||
</BodyText>
|
||||
</Article>
|
||||
|
||||
<Layer position="absolute" top="20px" right="20px" zIndex={10}>
|
||||
<div className="bg-yellow-50 border-l-4 border-yellow-400 p-4 max-w-xs">
|
||||
<p className="text-lg font-semibold text-gray-800">
|
||||
"这是一段重要的引用文字"
|
||||
</p>
|
||||
</div>
|
||||
</Layer>
|
||||
</div>
|
||||
</Section>
|
||||
</Layout>
|
||||
</Demo>
|
||||
|
||||
<h2>浮动广告</h2>
|
||||
<Demo
|
||||
title="侧边浮动广告"
|
||||
description="在页面侧边添加固定位置的广告"
|
||||
code={`<Layout columns={24}>
|
||||
<Section columns={24}>
|
||||
<div style={{ position: 'relative', minHeight: '400px' }}>
|
||||
<Article span={18}>
|
||||
<Headline weight="High">文章内容</Headline>
|
||||
<BodyText>
|
||||
这是文章的主要内容区域。右侧有一个浮动的广告位,
|
||||
使用 Layer 组件实现。
|
||||
</BodyText>
|
||||
</Article>
|
||||
|
||||
<Layer position="absolute" top="0" right="0" zIndex={5}>
|
||||
<div className="bg-blue-100 border border-blue-300 p-6 w-48">
|
||||
<p className="text-center font-bold text-blue-900">广告位</p>
|
||||
<p className="text-sm text-blue-700 mt-2">300x250</p>
|
||||
</div>
|
||||
</Layer>
|
||||
</div>
|
||||
</Section>
|
||||
</Layout>`}
|
||||
>
|
||||
<Layout columns={24}>
|
||||
<Section columns={24}>
|
||||
<div style={{ position: 'relative', minHeight: '400px' }}>
|
||||
<Article span={18}>
|
||||
<Headline weight="High">文章内容</Headline>
|
||||
<BodyText>
|
||||
这是文章的主要内容区域。右侧有一个浮动的广告位,
|
||||
使用 Layer 组件实现。广告不会影响正常的文章布局流。
|
||||
</BodyText>
|
||||
<BodyText>
|
||||
Layer 组件非常适合实现这类需要脱离文档流的元素。
|
||||
</BodyText>
|
||||
</Article>
|
||||
|
||||
<Layer position="absolute" top="0" right="0" zIndex={5}>
|
||||
<div className="bg-blue-100 border border-blue-300 p-6 w-48 text-center">
|
||||
<p className="font-bold text-blue-900">广告位</p>
|
||||
<p className="text-sm text-blue-700 mt-2">300x250</p>
|
||||
</div>
|
||||
</Layer>
|
||||
</div>
|
||||
</Section>
|
||||
</Layout>
|
||||
</Demo>
|
||||
|
||||
<h2>使用建议</h2>
|
||||
<ul>
|
||||
<li>Layer 的父容器需要设置 <code>position: relative</code></li>
|
||||
<li>使用 zIndex 控制多个 Layer 的层叠顺序</li>
|
||||
<li>浮动元素不应遮挡重要内容</li>
|
||||
<li>考虑移动端的显示效果,必要时隐藏或调整 Layer</li>
|
||||
<li>使用 <code>position="fixed"</code> 可以实现固定在视口的效果</li>
|
||||
<li>使用 <code>position="sticky"</code> 可以实现粘性定位效果</li>
|
||||
</ul>
|
||||
|
||||
<h2>注意事项</h2>
|
||||
<ul>
|
||||
<li>Layer 脱离了栅格系统,不受 Section 和 Article 的列数限制</li>
|
||||
<li>需要手动控制 Layer 的宽度和位置</li>
|
||||
<li>在响应式设计中,可能需要根据屏幕尺寸调整 Layer 的位置</li>
|
||||
<li>过多的 Layer 可能影响页面性能和可访问性</li>
|
||||
<li>确保 Layer 内容在所有屏幕尺寸下都能正常显示</li>
|
||||
</ul>
|
||||
|
||||
<h2>典型应用场景</h2>
|
||||
<ul>
|
||||
<li>浮动拉引 - 突出显示文章中的重要引用</li>
|
||||
<li>浮动广告 - 侧边或角落的广告位</li>
|
||||
<li>浮动图片 - 文章中的浮动配图</li>
|
||||
<li>固定导航 - 使用 fixed 定位的导航栏</li>
|
||||
<li>粘性标题 - 使用 sticky 定位的章节标题</li>
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,271 @@
|
||||
'use client';
|
||||
|
||||
import { Demo } from '@/components/Demo';
|
||||
import { PropsTable } from '@/components/PropsTable';
|
||||
import { Layout, Section, Article, Headline, BodyText } from '@newspaperui/components';
|
||||
import { Image, Figure } from '@newspaperui/components';
|
||||
|
||||
export default function MediaPage() {
|
||||
return (
|
||||
<div className="prose prose-lg max-w-none">
|
||||
<h1>媒体组件</h1>
|
||||
<p className="text-xl text-gray-600">
|
||||
NewspaperUI 提供了 Image、Figure 和 Video 组件用于展示媒体内容。
|
||||
</p>
|
||||
|
||||
<h2>Image 组件</h2>
|
||||
<p>
|
||||
<code>Image</code> 组件用于显示图片,支持响应式和跨栏布局。
|
||||
</p>
|
||||
|
||||
<PropsTable
|
||||
data={[
|
||||
{
|
||||
name: 'src',
|
||||
type: 'string',
|
||||
required: true,
|
||||
description: '图片 URL',
|
||||
},
|
||||
{
|
||||
name: 'alt',
|
||||
type: 'string',
|
||||
required: true,
|
||||
description: '图片替代文本(无障碍)',
|
||||
},
|
||||
{
|
||||
name: 'span',
|
||||
type: 'number',
|
||||
description: '图片占据的列数',
|
||||
},
|
||||
{
|
||||
name: 'aspectRatio',
|
||||
type: 'string',
|
||||
default: '"16/9"',
|
||||
description: '图片宽高比',
|
||||
},
|
||||
{
|
||||
name: 'objectFit',
|
||||
type: '"cover" | "contain" | "fill"',
|
||||
default: '"cover"',
|
||||
description: '图片填充方式',
|
||||
},
|
||||
{
|
||||
name: 'className',
|
||||
type: 'string',
|
||||
description: '自定义 CSS 类名',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
<Demo
|
||||
title="基础图片"
|
||||
description="在文章中插入图片"
|
||||
code={`<Article span={12}>
|
||||
<Headline weight="High">带图片的文章</Headline>
|
||||
<Image
|
||||
src="https://via.placeholder.com/800x600"
|
||||
alt="示例图片"
|
||||
span={12}
|
||||
/>
|
||||
<BodyText>图片下方的文字说明</BodyText>
|
||||
</Article>`}
|
||||
>
|
||||
<Layout columns={24}>
|
||||
<Section columns={24}>
|
||||
<Article span={12}>
|
||||
<Headline weight="High">带图片的文章</Headline>
|
||||
<Image
|
||||
src="https://via.placeholder.com/800x600"
|
||||
alt="示例图片"
|
||||
span={12}
|
||||
/>
|
||||
<BodyText>图片下方的文字说明</BodyText>
|
||||
</Article>
|
||||
</Section>
|
||||
</Layout>
|
||||
</Demo>
|
||||
|
||||
<h2>Figure 组件</h2>
|
||||
<p>
|
||||
<code>Figure</code> 组件是带标题的图片容器,包含图片和图注。
|
||||
</p>
|
||||
|
||||
<PropsTable
|
||||
data={[
|
||||
{
|
||||
name: 'src',
|
||||
type: 'string',
|
||||
required: true,
|
||||
description: '图片 URL',
|
||||
},
|
||||
{
|
||||
name: 'alt',
|
||||
type: 'string',
|
||||
required: true,
|
||||
description: '图片替代文本',
|
||||
},
|
||||
{
|
||||
name: 'caption',
|
||||
type: 'string',
|
||||
required: true,
|
||||
description: '图注文字',
|
||||
},
|
||||
{
|
||||
name: 'span',
|
||||
type: 'number',
|
||||
description: 'Figure 占据的列数',
|
||||
},
|
||||
{
|
||||
name: 'aspectRatio',
|
||||
type: 'string',
|
||||
default: '"16/9"',
|
||||
description: '图片宽高比',
|
||||
},
|
||||
{
|
||||
name: 'className',
|
||||
type: 'string',
|
||||
description: '自定义 CSS 类名',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
<Demo
|
||||
title="带图注的图片"
|
||||
description="使用 Figure 组件添加图注"
|
||||
code={`<Article span={12}>
|
||||
<Headline weight="High">新闻标题</Headline>
|
||||
<Figure
|
||||
src="https://via.placeholder.com/800x600"
|
||||
alt="新闻配图"
|
||||
caption="这是图片的说明文字,描述图片内容"
|
||||
span={12}
|
||||
/>
|
||||
<BodyText>新闻正文内容...</BodyText>
|
||||
</Article>`}
|
||||
>
|
||||
<Layout columns={24}>
|
||||
<Section columns={24}>
|
||||
<Article span={12}>
|
||||
<Headline weight="High">新闻标题</Headline>
|
||||
<Figure
|
||||
src="https://via.placeholder.com/800x600"
|
||||
alt="新闻配图"
|
||||
caption="这是图片的说明文字,描述图片内容"
|
||||
span={12}
|
||||
/>
|
||||
<BodyText>新闻正文内容...</BodyText>
|
||||
</Article>
|
||||
</Section>
|
||||
</Layout>
|
||||
</Demo>
|
||||
|
||||
<h2>Video 组件</h2>
|
||||
<p>
|
||||
<code>Video</code> 组件用于嵌入视频内容。
|
||||
</p>
|
||||
|
||||
<PropsTable
|
||||
data={[
|
||||
{
|
||||
name: 'src',
|
||||
type: 'string',
|
||||
required: true,
|
||||
description: '视频 URL',
|
||||
},
|
||||
{
|
||||
name: 'poster',
|
||||
type: 'string',
|
||||
description: '视频封面图片 URL',
|
||||
},
|
||||
{
|
||||
name: 'span',
|
||||
type: 'number',
|
||||
description: '视频占据的列数',
|
||||
},
|
||||
{
|
||||
name: 'controls',
|
||||
type: 'boolean',
|
||||
default: 'true',
|
||||
description: '是否显示控制条',
|
||||
},
|
||||
{
|
||||
name: 'autoPlay',
|
||||
type: 'boolean',
|
||||
default: 'false',
|
||||
description: '是否自动播放',
|
||||
},
|
||||
{
|
||||
name: 'loop',
|
||||
type: 'boolean',
|
||||
default: 'false',
|
||||
description: '是否循环播放',
|
||||
},
|
||||
{
|
||||
name: 'muted',
|
||||
type: 'boolean',
|
||||
default: 'false',
|
||||
description: '是否静音',
|
||||
},
|
||||
{
|
||||
name: 'className',
|
||||
type: 'string',
|
||||
description: '自定义 CSS 类名',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
<Demo
|
||||
title="嵌入视频"
|
||||
description="在文章中嵌入视频内容"
|
||||
code={`<Article span={16}>
|
||||
<Headline weight="High">视频新闻</Headline>
|
||||
<Video
|
||||
src="https://example.com/video.mp4"
|
||||
poster="https://via.placeholder.com/800x450"
|
||||
span={16}
|
||||
controls
|
||||
/>
|
||||
<BodyText>视频相关的文字说明...</BodyText>
|
||||
</Article>`}
|
||||
>
|
||||
<Layout columns={24}>
|
||||
<Section columns={24}>
|
||||
<Article span={16}>
|
||||
<Headline weight="High">视频新闻</Headline>
|
||||
<div className="bg-gray-200 aspect-video flex items-center justify-center">
|
||||
<p className="text-gray-600">视频播放器占位</p>
|
||||
</div>
|
||||
<BodyText>视频相关的文字说明...</BodyText>
|
||||
</Article>
|
||||
</Section>
|
||||
</Layout>
|
||||
</Demo>
|
||||
|
||||
<h2>使用建议</h2>
|
||||
<ul>
|
||||
<li>使用 Image 组件展示简单图片</li>
|
||||
<li>使用 Figure 组件为图片添加说明文字</li>
|
||||
<li>使用 Video 组件嵌入视频内容</li>
|
||||
<li>通过 span 属性控制媒体元素的宽度</li>
|
||||
<li>设置合适的 aspectRatio 保持图片比例</li>
|
||||
<li>始终提供 alt 文本以提高无障碍性</li>
|
||||
<li>考虑图片和视频的加载性能</li>
|
||||
</ul>
|
||||
|
||||
<h2>响应式媒体</h2>
|
||||
<p>
|
||||
媒体组件会根据容器宽度自动调整大小。在小屏设备上,
|
||||
媒体元素会自动适应屏幕宽度,保持良好的显示效果。
|
||||
</p>
|
||||
|
||||
<h2>性能优化</h2>
|
||||
<ul>
|
||||
<li>使用适当的图片格式(WebP、AVIF)</li>
|
||||
<li>提供多种尺寸的图片用于响应式加载</li>
|
||||
<li>使用懒加载(lazy loading)优化页面性能</li>
|
||||
<li>压缩图片和视频文件大小</li>
|
||||
<li>考虑使用 CDN 加速媒体资源加载</li>
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,251 @@
|
||||
'use client';
|
||||
|
||||
import { Demo } from '@/components/Demo';
|
||||
import { Layout, Section, Article } from '@newspaperui/components';
|
||||
import { Headline, BodyText } from '@newspaperui/components';
|
||||
|
||||
export default function ResponsivePage() {
|
||||
return (
|
||||
<div className="prose prose-lg max-w-none">
|
||||
<h1>响应式布局示例</h1>
|
||||
<p className="text-xl text-gray-600">
|
||||
展示 NewspaperUI 如何在不同屏幕尺寸下自动调整布局。
|
||||
</p>
|
||||
|
||||
<h2>响应式原理</h2>
|
||||
<p>
|
||||
NewspaperUI 的响应式系统基于栅格列数的动态调整。在不同的屏幕尺寸下,
|
||||
栅格系统会自动调整列数,确保内容在各种设备上都能良好显示。
|
||||
</p>
|
||||
|
||||
<div className="bg-blue-50 border-l-4 border-blue-400 p-4 my-6">
|
||||
<h3 className="text-lg font-semibold text-blue-900 mt-0">响应式断点</h3>
|
||||
<ul className="mb-0 text-blue-800">
|
||||
<li><strong>大屏(≥1024px)</strong>: 24 列</li>
|
||||
<li><strong>中屏(768px-1023px)</strong>: 16 列</li>
|
||||
<li><strong>小屏(<768px)</strong>: 12 列</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<h2>自适应布局示例</h2>
|
||||
<p>
|
||||
以下示例展示了布局如何在不同屏幕尺寸下自动调整。
|
||||
尝试调整浏览器窗口大小查看效果。
|
||||
</p>
|
||||
|
||||
<Demo
|
||||
title="响应式三栏布局"
|
||||
description="大屏显示三栏,中屏显示两栏,小屏显示单栏"
|
||||
code={`<Layout columns={24}>
|
||||
<Section columns={24}>
|
||||
{/* 大屏: 8+8+8, 中屏: 12+12, 小屏: 24 */}
|
||||
<Article span={8} className="lg:span-8 md:span-12 sm:span-24">
|
||||
<Headline weight="Medium">第一栏</Headline>
|
||||
<BodyText>内容自动适应屏幕宽度</BodyText>
|
||||
</Article>
|
||||
<Article span={8} className="lg:span-8 md:span-12 sm:span-24">
|
||||
<Headline weight="Medium">第二栏</Headline>
|
||||
<BodyText>响应式布局</BodyText>
|
||||
</Article>
|
||||
<Article span={8} className="lg:span-8 md:span-12 sm:span-24">
|
||||
<Headline weight="Medium">第三栏</Headline>
|
||||
<BodyText>自动换行</BodyText>
|
||||
</Article>
|
||||
</Section>
|
||||
</Layout>`}
|
||||
>
|
||||
<Layout columns={24}>
|
||||
<Section columns={24}>
|
||||
<Article span={8}>
|
||||
<Headline weight="Medium">科技新闻</Headline>
|
||||
<BodyText>
|
||||
在大屏上,这三栏并排显示。在中屏上,变为两栏布局。
|
||||
在小屏上,每栏占据全宽,垂直堆叠。
|
||||
</BodyText>
|
||||
</Article>
|
||||
<Article span={8}>
|
||||
<Headline weight="Medium">财经要闻</Headline>
|
||||
<BodyText>
|
||||
响应式布局确保内容在所有设备上都能良好显示,
|
||||
提供最佳的阅读体验。
|
||||
</BodyText>
|
||||
</Article>
|
||||
<Article span={8}>
|
||||
<Headline weight="Medium">文化艺术</Headline>
|
||||
<BodyText>
|
||||
NewspaperUI 的响应式系统会自动处理列宽调整,
|
||||
无需手动编写复杂的媒体查询。
|
||||
</BodyText>
|
||||
</Article>
|
||||
</Section>
|
||||
</Layout>
|
||||
</Demo>
|
||||
|
||||
<h2>主次内容响应式</h2>
|
||||
<p>
|
||||
在响应式布局中,主要内容和次要内容的比例也会自动调整。
|
||||
</p>
|
||||
|
||||
<Demo
|
||||
title="主次内容自适应"
|
||||
description="大屏 16+8,中屏 12+12,小屏全宽堆叠"
|
||||
code={`<Layout columns={24}>
|
||||
<Section columns={24}>
|
||||
<Article span={16}>
|
||||
<Headline weight="High">主要内容</Headline>
|
||||
<BodyText weight="High">
|
||||
主要内容在大屏占据 16 列,在中屏占据 12 列,
|
||||
在小屏占据全宽。
|
||||
</BodyText>
|
||||
</Article>
|
||||
<Article span={8}>
|
||||
<Headline weight="Low">侧边栏</Headline>
|
||||
<BodyText weight="Low">
|
||||
侧边栏在大屏占据 8 列,在中屏占据 12 列,
|
||||
在小屏移到主内容下方。
|
||||
</BodyText>
|
||||
</Article>
|
||||
</Section>
|
||||
</Layout>`}
|
||||
>
|
||||
<Layout columns={24}>
|
||||
<Section columns={24}>
|
||||
<Article span={16}>
|
||||
<Headline weight="High">深度报道</Headline>
|
||||
<BodyText weight="High">
|
||||
这是主要内容区域,在大屏幕上占据较大的空间。
|
||||
当屏幕变小时,布局会自动调整以保持良好的可读性。
|
||||
</BodyText>
|
||||
<BodyText>
|
||||
响应式设计确保用户在任何设备上都能获得最佳的阅读体验。
|
||||
</BodyText>
|
||||
</Article>
|
||||
<Article span={8}>
|
||||
<Headline weight="Low">相关链接</Headline>
|
||||
<BodyText weight="Low">
|
||||
• 相关文章 1<br />
|
||||
• 相关文章 2<br />
|
||||
• 相关文章 3<br />
|
||||
• 更多内容...
|
||||
</BodyText>
|
||||
</Article>
|
||||
</Section>
|
||||
</Layout>
|
||||
</Demo>
|
||||
|
||||
<h2>移动优先设计</h2>
|
||||
<p>
|
||||
NewspaperUI 采用移动优先的设计理念,确保内容在小屏设备上也能完美呈现。
|
||||
</p>
|
||||
|
||||
<Demo
|
||||
title="移动优先布局"
|
||||
description="小屏优化的单栏布局"
|
||||
>
|
||||
<Layout columns={24}>
|
||||
<Section columns={24}>
|
||||
<Article span={24}>
|
||||
<Headline weight="High">移动端标题</Headline>
|
||||
<BodyText>
|
||||
在移动设备上,内容占据全宽,提供最佳的阅读体验。
|
||||
字体大小和行高也会根据屏幕尺寸自动调整。
|
||||
</BodyText>
|
||||
</Article>
|
||||
</Section>
|
||||
</Layout>
|
||||
</Demo>
|
||||
|
||||
<h2>响应式图片</h2>
|
||||
<p>
|
||||
图片和媒体元素也会根据容器宽度自动调整大小。
|
||||
</p>
|
||||
|
||||
<Demo
|
||||
title="响应式媒体"
|
||||
description="图片自动适应容器宽度"
|
||||
>
|
||||
<Layout columns={24}>
|
||||
<Section columns={24}>
|
||||
<Article span={16}>
|
||||
<Headline weight="High">带图片的文章</Headline>
|
||||
<div className="bg-gray-200 aspect-video flex items-center justify-center my-4">
|
||||
<span className="text-gray-600">响应式图片占位</span>
|
||||
</div>
|
||||
<BodyText>
|
||||
图片会根据容器宽度自动缩放,保持宽高比不变。
|
||||
</BodyText>
|
||||
</Article>
|
||||
</Section>
|
||||
</Layout>
|
||||
</Demo>
|
||||
|
||||
<h2>响应式最佳实践</h2>
|
||||
<ul>
|
||||
<li>使用相对单位(rem、em)而不是绝对单位(px)</li>
|
||||
<li>在小屏设备上简化布局,避免过多的列</li>
|
||||
<li>确保文字大小在移动设备上足够大(至少 16px)</li>
|
||||
<li>触摸目标(按钮、链接)至少 44x44px</li>
|
||||
<li>测试不同设备和屏幕尺寸的显示效果</li>
|
||||
<li>考虑横屏和竖屏两种方向</li>
|
||||
<li>优化图片和媒体资源的加载性能</li>
|
||||
</ul>
|
||||
|
||||
<h2>测试响应式布局</h2>
|
||||
<p>
|
||||
建议在以下设备和尺寸上测试响应式布局:
|
||||
</p>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 my-6">
|
||||
<div className="border border-gray-200 rounded-lg p-4">
|
||||
<h3 className="text-lg font-semibold mb-2">移动设备</h3>
|
||||
<ul className="text-sm space-y-1 mb-0">
|
||||
<li>iPhone SE (375px)</li>
|
||||
<li>iPhone 12/13 (390px)</li>
|
||||
<li>iPhone 14 Pro Max (430px)</li>
|
||||
<li>Android 小屏 (360px)</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className="border border-gray-200 rounded-lg p-4">
|
||||
<h3 className="text-lg font-semibold mb-2">平板设备</h3>
|
||||
<ul className="text-sm space-y-1 mb-0">
|
||||
<li>iPad Mini (768px)</li>
|
||||
<li>iPad (810px)</li>
|
||||
<li>iPad Pro (1024px)</li>
|
||||
<li>Android 平板 (800px)</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className="border border-gray-200 rounded-lg p-4">
|
||||
<h3 className="text-lg font-semibold mb-2">桌面设备</h3>
|
||||
<ul className="text-sm space-y-1 mb-0">
|
||||
<li>笔记本 (1366px)</li>
|
||||
<li>桌面 (1920px)</li>
|
||||
<li>大屏 (2560px)</li>
|
||||
<li>超宽屏 (3440px)</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2>调试工具</h2>
|
||||
<p>
|
||||
使用浏览器开发者工具的响应式设计模式测试不同屏幕尺寸:
|
||||
</p>
|
||||
<ul>
|
||||
<li><strong>Chrome DevTools</strong>: Cmd/Ctrl + Shift + M</li>
|
||||
<li><strong>Firefox</strong>: Cmd/Ctrl + Shift + M</li>
|
||||
<li><strong>Safari</strong>: Develop → Enter Responsive Design Mode</li>
|
||||
</ul>
|
||||
|
||||
<h2>性能优化</h2>
|
||||
<ul>
|
||||
<li>使用懒加载(lazy loading)延迟加载图片</li>
|
||||
<li>提供不同尺寸的图片用于不同设备</li>
|
||||
<li>使用现代图片格式(WebP、AVIF)</li>
|
||||
<li>压缩和优化资源文件</li>
|
||||
<li>使用 CDN 加速资源加载</li>
|
||||
<li>减少不必要的 JavaScript 和 CSS</li>
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,267 @@
|
||||
'use client';
|
||||
|
||||
import { Demo } from '@/components/Demo';
|
||||
import { Layout, Section, Article, Layer } from '@newspaperui/components';
|
||||
import { Headline, Subhead, BodyText, Quote } from '@newspaperui/components';
|
||||
|
||||
export default function SpanningPage() {
|
||||
return (
|
||||
<div className="prose prose-lg max-w-none">
|
||||
<h1>跨栏布局示例</h1>
|
||||
<p className="text-xl text-gray-600">
|
||||
展示如何使用 NewspaperUI 实现复杂的跨栏报纸布局。
|
||||
</p>
|
||||
|
||||
<h2>经典报纸头版布局</h2>
|
||||
<p>
|
||||
这是一个典型的报纸头版布局,包含主要新闻、次要新闻和浮动拉引。
|
||||
</p>
|
||||
|
||||
<Demo
|
||||
title="报纸头版"
|
||||
description="主新闻占据 16 列,侧边栏占据 8 列,带浮动拉引"
|
||||
code={`<Layout columns={24}>
|
||||
<Section columns={24}>
|
||||
<div style={{ position: 'relative', minHeight: '600px' }}>
|
||||
{/* 主新闻 - 16 列 */}
|
||||
<Article span={16}>
|
||||
<Headline weight="High">重大新闻标题占据主要版面</Headline>
|
||||
<Subhead weight="High">
|
||||
副标题提供更多背景信息和上下文
|
||||
</Subhead>
|
||||
<BodyText weight="High">
|
||||
这是主要新闻的正文内容。在报纸布局中,最重要的新闻通常占据最大的版面,
|
||||
使用最大的字号和最粗的字重。这样的布局能够立即吸引读者的注意力。
|
||||
</BodyText>
|
||||
<BodyText>
|
||||
新闻的后续段落继续详细描述事件的发展。通过合理的跨栏布局,
|
||||
我们可以在有限的版面中呈现丰富的内容。
|
||||
</BodyText>
|
||||
</Article>
|
||||
|
||||
{/* 侧边栏 - 8 列 */}
|
||||
<Article span={8}>
|
||||
<Headline weight="Medium">次要新闻</Headline>
|
||||
<BodyText weight="Medium">
|
||||
侧边栏的新闻使用较小的字号,适合放置次要但仍然重要的内容。
|
||||
</BodyText>
|
||||
</Article>
|
||||
|
||||
{/* 浮动拉引 */}
|
||||
<Layer position="absolute" top="100px" right="20px" zIndex={10}>
|
||||
<div className="bg-yellow-50 border-l-4 border-yellow-400 p-4 max-w-xs">
|
||||
<Quote weight="High">
|
||||
"这是一段重要的引用,用于突出文章中的关键观点"
|
||||
</Quote>
|
||||
</div>
|
||||
</Layer>
|
||||
</div>
|
||||
</Section>
|
||||
</Layout>`}
|
||||
>
|
||||
<Layout columns={24}>
|
||||
<Section columns={24}>
|
||||
<div style={{ position: 'relative', minHeight: '600px' }}>
|
||||
<Article span={16}>
|
||||
<Headline weight="High">重大新闻标题占据主要版面</Headline>
|
||||
<Subhead weight="High">
|
||||
副标题提供更多背景信息和上下文
|
||||
</Subhead>
|
||||
<BodyText weight="High">
|
||||
这是主要新闻的正文内容。在报纸布局中,最重要的新闻通常占据最大的版面,
|
||||
使用最大的字号和最粗的字重。这样的布局能够立即吸引读者的注意力。
|
||||
</BodyText>
|
||||
<BodyText>
|
||||
新闻的后续段落继续详细描述事件的发展。通过合理的跨栏布局,
|
||||
我们可以在有限的版面中呈现丰富的内容。
|
||||
</BodyText>
|
||||
</Article>
|
||||
|
||||
<Article span={8}>
|
||||
<Headline weight="Medium">次要新闻</Headline>
|
||||
<BodyText weight="Medium">
|
||||
侧边栏的新闻使用较小的字号,适合放置次要但仍然重要的内容。
|
||||
</BodyText>
|
||||
</Article>
|
||||
|
||||
<Layer position="absolute" top="100px" right="20px" zIndex={10}>
|
||||
<div className="bg-yellow-50 border-l-4 border-yellow-400 p-4 max-w-xs">
|
||||
<Quote weight="High">
|
||||
"这是一段重要的引用,用于突出文章中的关键观点"
|
||||
</Quote>
|
||||
</div>
|
||||
</Layer>
|
||||
</div>
|
||||
</Section>
|
||||
</Layout>
|
||||
</Demo>
|
||||
|
||||
<h2>三栏等宽布局</h2>
|
||||
<p>
|
||||
将 24 列平均分为三个 8 列的区域,适合展示多个同等重要的内容。
|
||||
</p>
|
||||
|
||||
<Demo
|
||||
title="三栏布局"
|
||||
description="三个 8 列的文章并排显示"
|
||||
code={`<Layout columns={24}>
|
||||
<Section columns={24}>
|
||||
<Article span={8}>
|
||||
<Headline weight="Medium">第一栏标题</Headline>
|
||||
<BodyText>第一栏的内容...</BodyText>
|
||||
</Article>
|
||||
<Article span={8}>
|
||||
<Headline weight="Medium">第二栏标题</Headline>
|
||||
<BodyText>第二栏的内容...</BodyText>
|
||||
</Article>
|
||||
<Article span={8}>
|
||||
<Headline weight="Medium">第三栏标题</Headline>
|
||||
<BodyText>第三栏的内容...</BodyText>
|
||||
</Article>
|
||||
</Section>
|
||||
</Layout>`}
|
||||
>
|
||||
<Layout columns={24}>
|
||||
<Section columns={24}>
|
||||
<Article span={8}>
|
||||
<Headline weight="Medium">科技新闻</Headline>
|
||||
<BodyText>
|
||||
最新的科技动态和创新成果,展示技术发展的最新趋势。
|
||||
</BodyText>
|
||||
</Article>
|
||||
<Article span={8}>
|
||||
<Headline weight="Medium">财经要闻</Headline>
|
||||
<BodyText>
|
||||
市场动态和经济分析,帮助读者了解财经领域的重要信息。
|
||||
</BodyText>
|
||||
</Article>
|
||||
<Article span={8}>
|
||||
<Headline weight="Medium">文化艺术</Headline>
|
||||
<BodyText>
|
||||
文化活动和艺术展览,丰富读者的精神文化生活。
|
||||
</BodyText>
|
||||
</Article>
|
||||
</Section>
|
||||
</Layout>
|
||||
</Demo>
|
||||
|
||||
<h2>不对称布局</h2>
|
||||
<p>
|
||||
使用不同的列宽组合创建视觉层次,突出重要内容。
|
||||
</p>
|
||||
|
||||
<Demo
|
||||
title="12 + 12 列布局"
|
||||
description="两个等宽的 12 列区域"
|
||||
code={`<Layout columns={24}>
|
||||
<Section columns={24}>
|
||||
<Article span={12}>
|
||||
<Headline weight="High">左侧主要内容</Headline>
|
||||
<BodyText weight="High">
|
||||
占据 12 列的主要内容区域...
|
||||
</BodyText>
|
||||
</Article>
|
||||
<Article span={12}>
|
||||
<Headline weight="High">右侧主要内容</Headline>
|
||||
<BodyText weight="High">
|
||||
同样占据 12 列的内容区域...
|
||||
</BodyText>
|
||||
</Article>
|
||||
</Section>
|
||||
</Layout>`}
|
||||
>
|
||||
<Layout columns={24}>
|
||||
<Section columns={24}>
|
||||
<Article span={12}>
|
||||
<Headline weight="High">深度报道:城市发展</Headline>
|
||||
<BodyText weight="High">
|
||||
这是一篇深度报道,详细分析城市发展的现状和未来趋势。
|
||||
12 列的宽度提供了良好的阅读体验。
|
||||
</BodyText>
|
||||
</Article>
|
||||
<Article span={12}>
|
||||
<Headline weight="High">专题调查:环境保护</Headline>
|
||||
<BodyText weight="High">
|
||||
环境保护专题调查报告,展示环保工作的成果和挑战。
|
||||
与左��内容形成对比和呼应。
|
||||
</BodyText>
|
||||
</Article>
|
||||
</Section>
|
||||
</Layout>
|
||||
</Demo>
|
||||
|
||||
<h2>复杂混合布局</h2>
|
||||
<p>
|
||||
组合多种列宽,创建丰富的视觉层次和内容结构。
|
||||
</p>
|
||||
|
||||
<Demo
|
||||
title="混合列宽布局"
|
||||
description="6 + 12 + 6 列的组合布局"
|
||||
code={`<Layout columns={24}>
|
||||
<Section columns={24}>
|
||||
<Article span={6}>
|
||||
<Headline weight="Low">侧边栏</Headline>
|
||||
<BodyText weight="Low">简短内容</BodyText>
|
||||
</Article>
|
||||
<Article span={12}>
|
||||
<Headline weight="High">主要内容</Headline>
|
||||
<BodyText>详细的主要内容...</BodyText>
|
||||
</Article>
|
||||
<Article span={6}>
|
||||
<Headline weight="Low">另一侧边栏</Headline>
|
||||
<BodyText weight="Low">补充信息</BodyText>
|
||||
</Article>
|
||||
</Section>
|
||||
</Layout>`}
|
||||
>
|
||||
<Layout columns={24}>
|
||||
<Section columns={24}>
|
||||
<Article span={6}>
|
||||
<Headline weight="Low">快讯</Headline>
|
||||
<BodyText weight="Low">
|
||||
• 新闻 1<br />
|
||||
• 新闻 2<br />
|
||||
• 新闻 3
|
||||
</BodyText>
|
||||
</Article>
|
||||
<Article span={12}>
|
||||
<Headline weight="High">今日焦点</Headline>
|
||||
<BodyText>
|
||||
主要新闻内容占据中间的 12 列,提供充足的空间展示详细信息。
|
||||
两侧的 6 列侧边栏提供补充内容和快讯。
|
||||
</BodyText>
|
||||
</Article>
|
||||
<Article span={6}>
|
||||
<Headline weight="Low">相关链接</Headline>
|
||||
<BodyText weight="Low">
|
||||
• 链接 1<br />
|
||||
• 链接 2<br />
|
||||
• 链接 3
|
||||
</BodyText>
|
||||
</Article>
|
||||
</Section>
|
||||
</Layout>
|
||||
</Demo>
|
||||
|
||||
<h2>布局技巧</h2>
|
||||
<ul>
|
||||
<li>使用较大的 span 值(12-16 列)突出重要内容</li>
|
||||
<li>侧边栏使用较小的 span 值(4-8 列)</li>
|
||||
<li>保持布局的平衡感,避免过于拥挤或空旷</li>
|
||||
<li>使用浮动 Layer 添加视觉亮点</li>
|
||||
<li>通过视觉权重系统创建清晰的信息层次</li>
|
||||
<li>考虑内容的重要性和阅读顺序</li>
|
||||
</ul>
|
||||
|
||||
<h2>注意事项</h2>
|
||||
<ul>
|
||||
<li>确保所有 Article 的 span 总和不超过 Section 的 columns</li>
|
||||
<li>在小屏设备上测试布局的响应式效果</li>
|
||||
<li>避免过多的跨栏,保持布局的可读性</li>
|
||||
<li>使用一致的列宽比例,如 8:16、6:12:6 等</li>
|
||||
<li>浮动元素不要遮挡重要内容</li>
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
@import '@newspaperui/theme/variables.css';
|
||||
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@layer base {
|
||||
body {
|
||||
@apply bg-white text-gray-900;
|
||||
}
|
||||
|
||||
h1 {
|
||||
@apply text-4xl font-bold mb-4;
|
||||
}
|
||||
|
||||
h2 {
|
||||
@apply text-3xl font-semibold mt-8 mb-4;
|
||||
}
|
||||
|
||||
h3 {
|
||||
@apply text-2xl font-semibold mt-6 mb-3;
|
||||
}
|
||||
|
||||
p {
|
||||
@apply mb-4 leading-relaxed;
|
||||
}
|
||||
|
||||
code {
|
||||
@apply text-sm bg-gray-100 px-1.5 py-0.5 rounded;
|
||||
}
|
||||
|
||||
pre code {
|
||||
@apply bg-transparent p-0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,209 @@
|
||||
'use client';
|
||||
|
||||
import { Demo } from '@/components/Demo';
|
||||
import { PropsTable } from '@/components/PropsTable';
|
||||
import { Layout, Section, Article, Headline, BodyText } from '@newspaperui/components';
|
||||
|
||||
export default function GridSystemPage() {
|
||||
return (
|
||||
<div className="prose prose-lg max-w-none">
|
||||
<h1>栅格系统</h1>
|
||||
<p className="text-xl text-gray-600">
|
||||
NewspaperUI 采用 24 列栅格系统,提供灵活而精确的布局控制能力。
|
||||
</p>
|
||||
|
||||
<h2>设计原理</h2>
|
||||
<p>
|
||||
24 列栅格系统借鉴了专业排版软件的设计思想,相比传统的 12 列��格��24 列提供了更细粒度的布局控制。
|
||||
这使得我们可以实现更复杂的报纸风格布局,如 1/3、2/3、1/4、3/4 等多种列宽组合。
|
||||
</p>
|
||||
|
||||
<h2>Layout 组件</h2>
|
||||
<p>
|
||||
<code>Layout</code> 是顶层容器组件,定义了整个页面的栅格系统。
|
||||
</p>
|
||||
|
||||
<PropsTable
|
||||
data={[
|
||||
{
|
||||
name: 'columns',
|
||||
type: 'number',
|
||||
default: '24',
|
||||
description: '栅格总列数,默认 24 列',
|
||||
},
|
||||
{
|
||||
name: 'maxWidth',
|
||||
type: 'string',
|
||||
default: '"1440px"',
|
||||
description: '容器最大宽度',
|
||||
},
|
||||
{
|
||||
name: 'gutter',
|
||||
type: 'number',
|
||||
default: '16',
|
||||
description: '列间距(单位 px)',
|
||||
},
|
||||
{
|
||||
name: 'padding',
|
||||
type: 'string',
|
||||
default: '"1rem"',
|
||||
description: '容器内边距',
|
||||
},
|
||||
{
|
||||
name: 'className',
|
||||
type: 'string',
|
||||
description: '自定义 CSS 类名',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
<Demo
|
||||
title="基础 Layout"
|
||||
description="创建一个 24 列的栅格容器"
|
||||
code={`<Layout columns={24}>
|
||||
{/* 内容 */}
|
||||
</Layout>`}
|
||||
>
|
||||
<Layout columns={24}>
|
||||
<div className="bg-blue-50 border-2 border-blue-200 p-4 text-center">
|
||||
24 列栅格容器
|
||||
</div>
|
||||
</Layout>
|
||||
</Demo>
|
||||
|
||||
<h2>Section 组件</h2>
|
||||
<p>
|
||||
<code>Section</code> 组件用于将 Layout 划分为多个区域,每个 Section 可以占据不同的列数。
|
||||
Section 内的所有对象的 span 总和不能超过 Section 的 columns 值。
|
||||
</p>
|
||||
|
||||
<PropsTable
|
||||
data={[
|
||||
{
|
||||
name: 'columns',
|
||||
type: 'number',
|
||||
required: true,
|
||||
description: 'Section 占据的列数,必须 ≤ Layout.columns',
|
||||
},
|
||||
{
|
||||
name: 'breakable',
|
||||
type: 'boolean',
|
||||
default: 'true',
|
||||
description: '是否允许在此处分页断开',
|
||||
},
|
||||
{
|
||||
name: 'priority',
|
||||
type: "'High' | 'Medium' | 'Low'",
|
||||
default: "'Medium'",
|
||||
description: '优先级,用于排序',
|
||||
},
|
||||
{
|
||||
name: 'className',
|
||||
type: 'string',
|
||||
description: '自定义 CSS 类名',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
<Demo
|
||||
title="多 Section 布局"
|
||||
description="将 24 列划分为 8 列和 16 列两个区域"
|
||||
code={`<Layout columns={24}>
|
||||
<Section columns={8}>
|
||||
<Article span={8}>
|
||||
<Headline weight="High">8 列区域</Headline>
|
||||
<BodyText>这个区域占据 8 列</BodyText>
|
||||
</Article>
|
||||
</Section>
|
||||
|
||||
<Section columns={16}>
|
||||
<Article span={8}>
|
||||
<Headline weight="Medium">16 列区域 - 左</Headline>
|
||||
<BodyText>这个区域占据 16 列,分为两个 8 列文章</BodyText>
|
||||
</Article>
|
||||
<Article span={8}>
|
||||
<Headline weight="Medium">16 列区域 - 右</Headline>
|
||||
<BodyText>第二个 8 列文章</BodyText>
|
||||
</Article>
|
||||
</Section>
|
||||
</Layout>`}
|
||||
>
|
||||
<Layout columns={24}>
|
||||
<Section columns={8}>
|
||||
<Article span={8}>
|
||||
<Headline weight="High">8 列区域</Headline>
|
||||
<BodyText>这个区域占据 8 列,适合放置主要内容或大图。</BodyText>
|
||||
</Article>
|
||||
</Section>
|
||||
|
||||
<Section columns={16}>
|
||||
<Article span={8}>
|
||||
<Headline weight="Medium">16 列区域 - 左</Headline>
|
||||
<BodyText>这个区域占据 16 列,分为两个 8 列文章。</BodyText>
|
||||
</Article>
|
||||
<Article span={8}>
|
||||
<Headline weight="Medium">16 列区域 - 右</Headline>
|
||||
<BodyText>第二个 8 列文章,与左侧并排显示。</BodyText>
|
||||
</Article>
|
||||
</Section>
|
||||
</Layout>
|
||||
</Demo>
|
||||
|
||||
<h2>响应式栅格</h2>
|
||||
<p>
|
||||
在小屏设备上,栅格系统会自动调整为 12-16 列,确保内容在移动设备上也能良好显示。
|
||||
你可以通过 CSS 媒体查询或响应式工具类来进一步控制不同屏幕尺寸下的布局。
|
||||
</p>
|
||||
|
||||
<Demo
|
||||
title="响应式栅格示例"
|
||||
description="在不同屏幕尺寸下自动调整列数"
|
||||
>
|
||||
<Layout columns={24}>
|
||||
<Section columns={6}>
|
||||
<Article span={6}>
|
||||
<Headline weight="Low">6 列</Headline>
|
||||
<BodyText>小屏下可能变为全宽</BodyText>
|
||||
</Article>
|
||||
</Section>
|
||||
<Section columns={6}>
|
||||
<Article span={6}>
|
||||
<Headline weight="Low">6 列</Headline>
|
||||
<BodyText>自动适应屏幕</BodyText>
|
||||
</Article>
|
||||
</Section>
|
||||
<Section columns={6}>
|
||||
<Article span={6}>
|
||||
<Headline weight="Low">6 列</Headline>
|
||||
<BodyText>保持良好阅读体验</BodyText>
|
||||
</Article>
|
||||
</Section>
|
||||
<Section columns={6}>
|
||||
<Article span={6}>
|
||||
<Headline weight="Low">6 列</Headline>
|
||||
<BodyText>响应式布局</BodyText>
|
||||
</Article>
|
||||
</Section>
|
||||
</Layout>
|
||||
</Demo>
|
||||
|
||||
<h2>布局规则</h2>
|
||||
<ul>
|
||||
<li>Layout 的 columns 属性定义了整个页面的栅格总列数(默认 24)</li>
|
||||
<li>Section 的 columns 属性必须 ≤ Layout 的 columns</li>
|
||||
<li>Section 内所有对象的 span 总和必须 ≤ Section 的 columns</li>
|
||||
<li>对象可以通过 span 属性跨越多列</li>
|
||||
<li>小屏设备会自动调整为 12-16 列</li>
|
||||
</ul>
|
||||
|
||||
<h2>最佳实践</h2>
|
||||
<ul>
|
||||
<li>使用 24 列栅格可以实现 1/2、1/3、1/4、1/6、1/8 等多种比例</li>
|
||||
<li>主要内容区域建议使用 8-16 列</li>
|
||||
<li>侧边栏或次要内容使用 4-8 列</li>
|
||||
<li>重要内容可以跨越更多列以获得更大的视觉权重</li>
|
||||
<li>保持 Section 之间的列数比例协调,如 8:16、6:18、12:12 等</li>
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import type { Metadata } from 'next';
|
||||
import { Sidebar } from '@/components/Sidebar';
|
||||
import './globals.css';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'NewspaperUI - Documentation',
|
||||
description: 'A newspaper-style UI component library',
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<html lang="zh-CN">
|
||||
<body className="antialiased">
|
||||
<div className="flex min-h-screen">
|
||||
<Sidebar />
|
||||
<main className="flex-1 overflow-x-hidden">
|
||||
<div className="max-w-5xl mx-auto px-8 py-12">
|
||||
{children}
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
'use client';
|
||||
|
||||
import { Demo } from '@/components/Demo';
|
||||
import { Layout, Section, Article } from '@newspaperui/components';
|
||||
import { Headline, BodyText } from '@newspaperui/components';
|
||||
|
||||
export default function HomePage() {
|
||||
return (
|
||||
<div className="prose prose-lg max-w-none">
|
||||
<h1>NewspaperUI</h1>
|
||||
<p className="text-xl text-gray-600">
|
||||
生产级报纸布局组件库,参考 InDesign 设计理念,支持 24 列栅格、跨栏、视觉权重和主题系统。
|
||||
</p>
|
||||
|
||||
<h2>设计理念</h2>
|
||||
<p>
|
||||
NewspaperUI 借鉴专业排版软件 InDesign 的设计思想,为 Web 应用提供报纸风格的布局能力。
|
||||
组件设计遵循少而精、职责明确、无冗余、无歧义的原则。
|
||||
</p>
|
||||
|
||||
<h2>核心特性</h2>
|
||||
<ul>
|
||||
<li><strong>24 列栅格系统</strong> - 灵活的栅格布局,支持精确的列控制</li>
|
||||
<li><strong>跨栏布局</strong> - 内容可以跨越多列,实现复杂的报纸排版</li>
|
||||
<li><strong>视觉权重系统</strong> - High/Medium/Low 三级权重,自动映射字体大小和样式</li>
|
||||
<li><strong>主题系统</strong> - 基于 CSS Variables,支持主题切换和自定义</li>
|
||||
<li><strong>响应式设计</strong> - 小屏自动调整为 12-16 列,保持良好的阅读体验</li>
|
||||
<li><strong>浮动 Layer</strong> - 支持绝对定位的浮动元素,如广告、拉引等</li>
|
||||
</ul>
|
||||
|
||||
<h2>快速开始</h2>
|
||||
|
||||
<h3>安装</h3>
|
||||
<pre className="bg-gray-900 text-gray-100 p-4 rounded-lg overflow-x-auto">
|
||||
<code>pnpm add @newspaperui/components @newspaperui/theme</code>
|
||||
</pre>
|
||||
|
||||
<h3>基础使用</h3>
|
||||
<Demo
|
||||
title="简单的报纸布局"
|
||||
description="使用 Layout 和 Section 创建基础的栅格布局"
|
||||
code={`import { Layout, Section, Article, Headline, BodyText } from '@newspaperui/components';
|
||||
|
||||
function MyPage() {
|
||||
return (
|
||||
<Layout columns={24}>
|
||||
<Section columns={8}>
|
||||
<Article span={8}>
|
||||
<Headline weight="High">主要新闻标题</Headline>
|
||||
<BodyText>这是一篇重要的新闻内容...</BodyText>
|
||||
</Article>
|
||||
</Section>
|
||||
|
||||
<Section columns={16}>
|
||||
<Article span={8}>
|
||||
<Headline weight="Medium">次要新闻</Headline>
|
||||
<BodyText>这是另一篇新闻...</BodyText>
|
||||
</Article>
|
||||
<Article span={8}>
|
||||
<Headline weight="Medium">更多新闻</Headline>
|
||||
<BodyText>更多内容...</BodyText>
|
||||
</Article>
|
||||
</Section>
|
||||
</Layout>
|
||||
);
|
||||
}`}
|
||||
>
|
||||
<Layout columns={24}>
|
||||
<Section columns={8}>
|
||||
<Article span={8}>
|
||||
<Headline weight="High">主要新闻标题</Headline>
|
||||
<BodyText>这是一篇重要的新闻内容,使用 High 权重的标题和正文文本。</BodyText>
|
||||
</Article>
|
||||
</Section>
|
||||
|
||||
<Section columns={16}>
|
||||
<Article span={8}>
|
||||
<Headline weight="Medium">次要新闻</Headline>
|
||||
<BodyText>这是另一篇新闻,使用 Medium 权重。</BodyText>
|
||||
</Article>
|
||||
<Article span={8}>
|
||||
<Headline weight="Medium">更多新闻</Headline>
|
||||
<BodyText>更多内容在这里展示。</BodyText>
|
||||
</Article>
|
||||
</Section>
|
||||
</Layout>
|
||||
</Demo>
|
||||
|
||||
<h2>下一步</h2>
|
||||
<p>
|
||||
探索文档了解更多功能:
|
||||
</p>
|
||||
<ul>
|
||||
<li><a href="/grid-system">栅格系统</a> - 了解 24 列栅格的工作原理</li>
|
||||
<li><a href="/components/article">核心组件</a> - 学习 Article、Layer 等核心组件</li>
|
||||
<li><a href="/text">文本组件</a> - 掌握视觉权重系统</li>
|
||||
<li><a href="/theme">主题系统</a> - 自定义主题和样式</li>
|
||||
<li><a href="/examples/spanning">示例</a> - 查看完整的布局示例</li>
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,334 @@
|
||||
'use client';
|
||||
|
||||
import { Demo } from '@/components/Demo';
|
||||
import { PropsTable } from '@/components/PropsTable';
|
||||
import { Headline, Subhead, BodyText, Quote, Byline, Caption } from '@newspaperui/components';
|
||||
|
||||
export default function TextPage() {
|
||||
return (
|
||||
<div className="prose prose-lg max-w-none">
|
||||
<h1>文本组件</h1>
|
||||
<p className="text-xl text-gray-600">
|
||||
NewspaperUI 提供了完整的文本组件体系,支持视觉权重系统,自动映射字体大小和样式。
|
||||
</p>
|
||||
|
||||
<h2>视觉权重系统</h2>
|
||||
<p>
|
||||
视觉权重(Visual Weight)是 NewspaperUI 的核心特性之一。
|
||||
通过 High、Medium、Low 三级权重,组件会自动应用对应的字体大小、粗细、行高和颜色。
|
||||
</p>
|
||||
|
||||
<h2>Headline 组件</h2>
|
||||
<p>
|
||||
<code>Headline</code> 是标题组件,支持三级视觉权重。
|
||||
</p>
|
||||
|
||||
<PropsTable
|
||||
data={[
|
||||
{
|
||||
name: 'weight',
|
||||
type: '"High" | "Medium" | "Low"',
|
||||
default: '"Medium"',
|
||||
description: '视觉权重级别',
|
||||
},
|
||||
{
|
||||
name: 'as',
|
||||
type: '"h1" | "h2" | "h3" | "h4" | "h5" | "h6"',
|
||||
default: '"h2"',
|
||||
description: 'HTML 标签类型',
|
||||
},
|
||||
{
|
||||
name: 'className',
|
||||
type: 'string',
|
||||
description: '自定义 CSS 类名',
|
||||
},
|
||||
{
|
||||
name: 'children',
|
||||
type: 'React.ReactNode',
|
||||
required: true,
|
||||
description: '标题内容',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
<Demo
|
||||
title="标题视觉权重"
|
||||
description="展示不同权重的标题效果"
|
||||
code={`<Headline weight="High">High 权重标题</Headline>
|
||||
<Headline weight="Medium">Medium 权重标题</Headline>
|
||||
<Headline weight="Low">Low 权重标题</Headline>`}
|
||||
>
|
||||
<div className="space-y-4">
|
||||
<Headline weight="High">High 权重标题 (36-48px, 700)</Headline>
|
||||
<Headline weight="Medium">Medium 权重标题 (28-34px, 600)</Headline>
|
||||
<Headline weight="Low">Low 权重标题 (22-26px, 500)</Headline>
|
||||
</div>
|
||||
</Demo>
|
||||
|
||||
<h2>Subhead 组件</h2>
|
||||
<p>
|
||||
<code>Subhead</code> 是副标题组件,用于补充说明主标题。
|
||||
</p>
|
||||
|
||||
<PropsTable
|
||||
data={[
|
||||
{
|
||||
name: 'weight',
|
||||
type: '"High" | "Medium"',
|
||||
default: '"Medium"',
|
||||
description: '视觉权重级别',
|
||||
},
|
||||
{
|
||||
name: 'className',
|
||||
type: 'string',
|
||||
description: '自定义 CSS 类名',
|
||||
},
|
||||
{
|
||||
name: 'children',
|
||||
type: 'React.ReactNode',
|
||||
required: true,
|
||||
description: '副标题内容',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
<Demo
|
||||
title="副标题示例"
|
||||
description="标题和副标题的组合使用"
|
||||
code={`<Headline weight="High">主标题</Headline>
|
||||
<Subhead weight="High">副标题说明文字</Subhead>
|
||||
<BodyText>正文内容...</BodyText>`}
|
||||
>
|
||||
<div className="space-y-2">
|
||||
<Headline weight="High">重大新闻事件</Headline>
|
||||
<Subhead weight="High">详细的副标题说明,提供更多上下文信息</Subhead>
|
||||
<BodyText>正文内容从这里开始,详细描述新闻事件的来龙去脉...</BodyText>
|
||||
</div>
|
||||
</Demo>
|
||||
|
||||
<h2>BodyText 组件</h2>
|
||||
<p>
|
||||
<code>BodyText</code> 是正文文本组件,用于文章主体内容。
|
||||
</p>
|
||||
|
||||
<PropsTable
|
||||
data={[
|
||||
{
|
||||
name: 'weight',
|
||||
type: '"High" | "Medium" | "Low"',
|
||||
default: '"Medium"',
|
||||
description: '视觉权重级别',
|
||||
},
|
||||
{
|
||||
name: 'className',
|
||||
type: 'string',
|
||||
description: '自定义 CSS 类名',
|
||||
},
|
||||
{
|
||||
name: 'children',
|
||||
type: 'React.ReactNode',
|
||||
required: true,
|
||||
description: '正文内容',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
<Demo
|
||||
title="正文文本权重"
|
||||
description="不同权重的正文文本"
|
||||
code={`<BodyText weight="High">High 权重正文 (16px)</BodyText>
|
||||
<BodyText weight="Medium">Medium 权重正文 (14-15px)</BodyText>
|
||||
<BodyText weight="Low">Low 权重正文 (12-14px)</BodyText>`}
|
||||
>
|
||||
<div className="space-y-3">
|
||||
<BodyText weight="High">
|
||||
High 权重正文文本,字号 16px,适合重要段落或引言。
|
||||
</BodyText>
|
||||
<BodyText weight="Medium">
|
||||
Medium 权重正文文本,字号 14-15px,适合常规正文内容。
|
||||
</BodyText>
|
||||
<BodyText weight="Low">
|
||||
Low 权重正文文本,字号 12-14px,适合次要说明或注释。
|
||||
</BodyText>
|
||||
</div>
|
||||
</Demo>
|
||||
|
||||
<h2>Quote 组件</h2>
|
||||
<p>
|
||||
<code>Quote</code> 是引用组件,用于突出显示引用内容。
|
||||
</p>
|
||||
|
||||
<PropsTable
|
||||
data={[
|
||||
{
|
||||
name: 'weight',
|
||||
type: '"High" | "Medium"',
|
||||
default: '"Medium"',
|
||||
description: '视觉权重级别',
|
||||
},
|
||||
{
|
||||
name: 'className',
|
||||
type: 'string',
|
||||
description: '自定义 CSS 类名',
|
||||
},
|
||||
{
|
||||
name: 'children',
|
||||
type: 'React.ReactNode',
|
||||
required: true,
|
||||
description: '引用内容',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
<Demo
|
||||
title="引用文本"
|
||||
description="在文章中插入引用"
|
||||
code={`<Quote weight="High">
|
||||
这是一段重要的引用文字,来自某位专家的观点。
|
||||
</Quote>`}
|
||||
>
|
||||
<Quote weight="High">
|
||||
这是一段重要的引用文字,来自某位专家的观点。引用组件会自动添加引号样式。
|
||||
</Quote>
|
||||
</Demo>
|
||||
|
||||
<h2>Byline 和 Caption 组件</h2>
|
||||
<p>
|
||||
<code>Byline</code> 用于作者署名,<code>Caption</code> 用于图片说明。
|
||||
</p>
|
||||
|
||||
<PropsTable
|
||||
data={[
|
||||
{
|
||||
name: 'className',
|
||||
type: 'string',
|
||||
description: '自定义 CSS 类名',
|
||||
},
|
||||
{
|
||||
name: 'children',
|
||||
type: 'React.ReactNode',
|
||||
required: true,
|
||||
description: '文本内容',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
<Demo
|
||||
title="署名和图注"
|
||||
description="使用 Byline 和 Caption"
|
||||
code={`<Headline weight="High">文章标题</Headline>
|
||||
<Byline>作者:张三 | 2024年1月1日</Byline>
|
||||
<BodyText>文章内容...</BodyText>
|
||||
<Caption>图1:示例图片的说明文字</Caption>`}
|
||||
>
|
||||
<div className="space-y-2">
|
||||
<Headline weight="High">重要新闻标题</Headline>
|
||||
<Byline>记者:张三 | 2024年1月1日 | 北京报道</Byline>
|
||||
<BodyText>文章正文内容从这里开始...</BodyText>
|
||||
<div className="bg-gray-200 h-32 flex items-center justify-center">
|
||||
<span className="text-gray-500">图片占位</span>
|
||||
</div>
|
||||
<Caption>图1:新闻现场照片,展示事件发生时的情况</Caption>
|
||||
</div>
|
||||
</Demo>
|
||||
|
||||
<h2>视觉权重映射表</h2>
|
||||
<p>
|
||||
以下是完整的视觉权重映射规则(基于 24 列栅格):
|
||||
</p>
|
||||
|
||||
<div className="my-6 overflow-x-auto">
|
||||
<table className="w-full border-collapse text-sm">
|
||||
<thead>
|
||||
<tr className="border-b-2 border-gray-300">
|
||||
<th className="text-left py-2 px-3">组件</th>
|
||||
<th className="text-left py-2 px-3">权重</th>
|
||||
<th className="text-left py-2 px-3">字号</th>
|
||||
<th className="text-left py-2 px-3">字重</th>
|
||||
<th className="text-left py-2 px-3">行高</th>
|
||||
<th className="text-left py-2 px-3">颜色</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="text-sm">
|
||||
<tr className="border-b border-gray-200">
|
||||
<td className="py-2 px-3">Headline</td>
|
||||
<td className="py-2 px-3">High</td>
|
||||
<td className="py-2 px-3">36-48px</td>
|
||||
<td className="py-2 px-3">700</td>
|
||||
<td className="py-2 px-3">1.1</td>
|
||||
<td className="py-2 px-3">#111</td>
|
||||
</tr>
|
||||
<tr className="border-b border-gray-200">
|
||||
<td className="py-2 px-3">Headline</td>
|
||||
<td className="py-2 px-3">Medium</td>
|
||||
<td className="py-2 px-3">28-34px</td>
|
||||
<td className="py-2 px-3">600</td>
|
||||
<td className="py-2 px-3">1.2</td>
|
||||
<td className="py-2 px-3">#111</td>
|
||||
</tr>
|
||||
<tr className="border-b border-gray-200">
|
||||
<td className="py-2 px-3">Headline</td>
|
||||
<td className="py-2 px-3">Low</td>
|
||||
<td className="py-2 px-3">22-26px</td>
|
||||
<td className="py-2 px-3">500</td>
|
||||
<td className="py-2 px-3">1.3</td>
|
||||
<td className="py-2 px-3">#222</td>
|
||||
</tr>
|
||||
<tr className="border-b border-gray-200">
|
||||
<td className="py-2 px-3">Subhead</td>
|
||||
<td className="py-2 px-3">High</td>
|
||||
<td className="py-2 px-3">20-24px</td>
|
||||
<td className="py-2 px-3">600</td>
|
||||
<td className="py-2 px-3">1.25</td>
|
||||
<td className="py-2 px-3">#222</td>
|
||||
</tr>
|
||||
<tr className="border-b border-gray-200">
|
||||
<td className="py-2 px-3">Subhead</td>
|
||||
<td className="py-2 px-3">Medium</td>
|
||||
<td className="py-2 px-3">16-18px</td>
|
||||
<td className="py-2 px-3">500</td>
|
||||
<td className="py-2 px-3">1.3</td>
|
||||
<td className="py-2 px-3">#333</td>
|
||||
</tr>
|
||||
<tr className="border-b border-gray-200">
|
||||
<td className="py-2 px-3">BodyText</td>
|
||||
<td className="py-2 px-3">High</td>
|
||||
<td className="py-2 px-3">16px</td>
|
||||
<td className="py-2 px-3">400</td>
|
||||
<td className="py-2 px-3">1.5</td>
|
||||
<td className="py-2 px-3">#333</td>
|
||||
</tr>
|
||||
<tr className="border-b border-gray-200">
|
||||
<td className="py-2 px-3">BodyText</td>
|
||||
<td className="py-2 px-3">Medium</td>
|
||||
<td className="py-2 px-3">14-15px</td>
|
||||
<td className="py-2 px-3">400</td>
|
||||
<td className="py-2 px-3">1.5</td>
|
||||
<td className="py-2 px-3">#444</td>
|
||||
</tr>
|
||||
<tr className="border-b border-gray-200">
|
||||
<td className="py-2 px-3">BodyText</td>
|
||||
<td className="py-2 px-3">Low</td>
|
||||
<td className="py-2 px-3">12-14px</td>
|
||||
<td className="py-2 px-3">400</td>
|
||||
<td className="py-2 px-3">1.4</td>
|
||||
<td className="py-2 px-3">#555</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<h2>使用建议</h2>
|
||||
<ul>
|
||||
<li>主标题使用 Headline High 权重</li>
|
||||
<li>次要标题使用 Headline Medium 或 Low 权重</li>
|
||||
<li>正文使用 BodyText Medium 权重</li>
|
||||
<li>重要段落或引言使用 BodyText High 权重</li>
|
||||
<li>注释或说明使用 BodyText Low 权重</li>
|
||||
<li>引用使用 Quote 组件突出显示</li>
|
||||
<li>作者信息使用 Byline 组件</li>
|
||||
<li>图片说明使用 Caption 组件</li>
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,310 @@
|
||||
import { CodeBlock } from '@/components/CodeBlock';
|
||||
|
||||
export default function ThemePage() {
|
||||
return (
|
||||
<div className="prose prose-lg max-w-none">
|
||||
<h1>主题系统</h1>
|
||||
<p className="text-xl text-gray-600">
|
||||
NewspaperUI 基于 CSS Variables 构建主题系统,支持主题切换和自定义。
|
||||
</p>
|
||||
|
||||
<h2>CSS Variables</h2>
|
||||
<p>
|
||||
主题系统使用 CSS 自定义属性(CSS Variables)定义颜色、字体、间距等设计令牌。
|
||||
这使得主题切换和自定义变得简单而灵活。
|
||||
</p>
|
||||
|
||||
<h3>核心变量</h3>
|
||||
<CodeBlock
|
||||
title="CSS Variables"
|
||||
language="css"
|
||||
code={`:root {
|
||||
/* 颜色系统 */
|
||||
--color-text-primary: #111;
|
||||
--color-text-secondary: #222;
|
||||
--color-text-tertiary: #333;
|
||||
--color-text-muted: #555;
|
||||
--color-background: #ffffff;
|
||||
--color-border: #e5e7eb;
|
||||
|
||||
/* 字体系统 */
|
||||
--font-family-serif: Georgia, 'Times New Roman', serif;
|
||||
--font-family-sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||
--font-family-mono: 'Courier New', monospace;
|
||||
|
||||
/* 间距系统 */
|
||||
--spacing-gutter: 1rem;
|
||||
--spacing-section: 2rem;
|
||||
--spacing-article: 1.5rem;
|
||||
|
||||
/* 栅格系统 */
|
||||
--grid-columns: 24;
|
||||
--grid-max-width: 1280px;
|
||||
}`}
|
||||
/>
|
||||
|
||||
<h2>Tailwind 集成</h2>
|
||||
<p>
|
||||
NewspaperUI 的主题系统与 Tailwind CSS 深度集成,可以直接在 Tailwind 配置中使用主题变量。
|
||||
</p>
|
||||
|
||||
<CodeBlock
|
||||
title="tailwind.config.js"
|
||||
language="javascript"
|
||||
code={`import { newspaperTheme } from '@newspaperui/theme';
|
||||
|
||||
export default {
|
||||
content: ['./src/**/*.{js,ts,jsx,tsx}'],
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
newspaper: {
|
||||
text: {
|
||||
primary: 'var(--color-text-primary)',
|
||||
secondary: 'var(--color-text-secondary)',
|
||||
tertiary: 'var(--color-text-tertiary)',
|
||||
muted: 'var(--color-text-muted)',
|
||||
},
|
||||
},
|
||||
},
|
||||
fontFamily: {
|
||||
serif: 'var(--font-family-serif)',
|
||||
sans: 'var(--font-family-sans)',
|
||||
mono: 'var(--font-family-mono)',
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [newspaperTheme],
|
||||
};`}
|
||||
/>
|
||||
|
||||
<h2>使用主题变量</h2>
|
||||
<p>
|
||||
在组件中可以直接使用 CSS Variables 或 Tailwind 工具类。
|
||||
</p>
|
||||
|
||||
<CodeBlock
|
||||
title="使用示例"
|
||||
language="tsx"
|
||||
code={`// 使用 CSS Variables
|
||||
<div style={{ color: 'var(--color-text-primary)' }}>
|
||||
主要文本
|
||||
</div>
|
||||
|
||||
// 使用 Tailwind 工具类
|
||||
<div className="text-newspaper-text-primary">
|
||||
主要文本
|
||||
</div>`}
|
||||
/>
|
||||
|
||||
<h2>自定义主题</h2>
|
||||
<p>
|
||||
你可以通过覆盖 CSS Variables 来自定义主题。
|
||||
</p>
|
||||
|
||||
<CodeBlock
|
||||
title="自定义主题"
|
||||
language="css"
|
||||
code={`:root {
|
||||
/* 覆盖默认颜色 */
|
||||
--color-text-primary: #000000;
|
||||
--color-text-secondary: #1a1a1a;
|
||||
--color-background: #f9f9f9;
|
||||
|
||||
/* 自定义字体 */
|
||||
--font-family-serif: 'Merriweather', Georgia, serif;
|
||||
|
||||
/* 调整间距 */
|
||||
--spacing-gutter: 1.5rem;
|
||||
--grid-max-width: 1440px;
|
||||
}`}
|
||||
/>
|
||||
|
||||
<h2>深色模式</h2>
|
||||
<p>
|
||||
通过定义深色模式的 CSS Variables,可以轻松实现主题切换。
|
||||
</p>
|
||||
|
||||
<CodeBlock
|
||||
title="深色模式"
|
||||
language="css"
|
||||
code={`@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--color-text-primary: #f5f5f5;
|
||||
--color-text-secondary: #e0e0e0;
|
||||
--color-text-tertiary: #cccccc;
|
||||
--color-text-muted: #999999;
|
||||
--color-background: #1a1a1a;
|
||||
--color-border: #333333;
|
||||
}
|
||||
}
|
||||
|
||||
/* 或使用类名切换 */
|
||||
.dark {
|
||||
--color-text-primary: #f5f5f5;
|
||||
--color-text-secondary: #e0e0e0;
|
||||
--color-background: #1a1a1a;
|
||||
}`}
|
||||
/>
|
||||
|
||||
<h2>视觉权重配置</h2>
|
||||
<p>
|
||||
视觉权重系统的配置也可以通过主题系统进行自定义。
|
||||
</p>
|
||||
|
||||
<CodeBlock
|
||||
title="自定义视觉权重"
|
||||
language="typescript"
|
||||
code={`import { visualWeights } from '@newspaperui/theme';
|
||||
|
||||
// 查看默认配置
|
||||
console.log(visualWeights.Headline.High);
|
||||
// {
|
||||
// fontSize: '36px',
|
||||
// fontWeight: 700,
|
||||
// lineHeight: 1.1,
|
||||
// color: '#111',
|
||||
// span: [6, 8],
|
||||
// margin: '0 0 1rem 0',
|
||||
// }
|
||||
|
||||
// 自定义权重配置
|
||||
const customWeights = {
|
||||
...visualWeights,
|
||||
Headline: {
|
||||
...visualWeights.Headline,
|
||||
High: {
|
||||
...visualWeights.Headline.High,
|
||||
fontSize: '48px', // 更大的标题
|
||||
fontWeight: 800,
|
||||
},
|
||||
},
|
||||
};`}
|
||||
/>
|
||||
|
||||
<h2>响应式主题</h2>
|
||||
<p>
|
||||
可以根据屏幕尺寸调整主题变量,实现响应式设计。
|
||||
</p>
|
||||
|
||||
<CodeBlock
|
||||
title="响应式变量"
|
||||
language="css"
|
||||
code={`:root {
|
||||
--grid-columns: 24;
|
||||
--spacing-gutter: 1rem;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
:root {
|
||||
--grid-columns: 12;
|
||||
--spacing-gutter: 0.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
:root {
|
||||
--grid-columns: 6;
|
||||
--spacing-gutter: 0.5rem;
|
||||
}
|
||||
}`}
|
||||
/>
|
||||
|
||||
<h2>主题最佳实践</h2>
|
||||
<ul>
|
||||
<li>使用语义化的变量名,如 <code>--color-text-primary</code> 而不是 <code>--color-black</code></li>
|
||||
<li>保持颜色对比度符合 WCAG 无障碍标准</li>
|
||||
<li>使用相对单位(rem、em)而不是绝对单位(px)以支持用户字体大小偏好</li>
|
||||
<li>在深色模式下确保所有颜色都有对应的深色版本</li>
|
||||
<li>测试主题在不同设备和浏览器上的表现</li>
|
||||
<li>提供主题切换的用户界面</li>
|
||||
</ul>
|
||||
|
||||
<h2>导入主题</h2>
|
||||
<p>
|
||||
在你的应用中导入 NewspaperUI 主题:
|
||||
</p>
|
||||
|
||||
<CodeBlock
|
||||
title="导入主题"
|
||||
language="typescript"
|
||||
code={`// 在你的主 CSS 文件中
|
||||
import '@newspaperui/theme/variables.css';
|
||||
|
||||
// 或在 Next.js 的 _app.tsx 中
|
||||
import '@newspaperui/theme/variables.css';
|
||||
import '@newspaperui/theme/tailwind.css';`}
|
||||
/>
|
||||
|
||||
<h2>主题工具</h2>
|
||||
<p>
|
||||
NewspaperUI 提供了一些工具函数来帮助你使用主题系统:
|
||||
</p>
|
||||
|
||||
<CodeBlock
|
||||
title="主题工具"
|
||||
language="typescript"
|
||||
code={`import { getVisualWeight, applyTheme } from '@newspaperui/theme';
|
||||
|
||||
// 获取特定组件的视觉权重配置
|
||||
const headlineHighConfig = getVisualWeight('Headline', 'High');
|
||||
|
||||
// 应用主题到元素
|
||||
applyTheme(element, {
|
||||
'--color-text-primary': '#000',
|
||||
'--font-family-serif': 'Georgia',
|
||||
});`}
|
||||
/>
|
||||
|
||||
<h2>主题示例</h2>
|
||||
<p>
|
||||
以下是一些预设主题示例:
|
||||
</p>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 my-8">
|
||||
<div className="border border-gray-200 rounded-lg p-6">
|
||||
<h3 className="text-lg font-semibold mb-3">经典报纸</h3>
|
||||
<div className="space-y-2 text-sm">
|
||||
<div className="flex justify-between">
|
||||
<span>主色调</span>
|
||||
<span className="font-mono">#111111</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span>背景色</span>
|
||||
<span className="font-mono">#ffffff</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span>字体</span>
|
||||
<span className="font-mono">Georgia</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="border border-gray-200 rounded-lg p-6 bg-gray-900 text-white">
|
||||
<h3 className="text-lg font-semibold mb-3">深色模式</h3>
|
||||
<div className="space-y-2 text-sm">
|
||||
<div className="flex justify-between">
|
||||
<span>主色调</span>
|
||||
<span className="font-mono">#f5f5f5</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span>背景色</span>
|
||||
<span className="font-mono">#1a1a1a</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span>字体</span>
|
||||
<span className="font-mono">Georgia</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2>更多资源</h2>
|
||||
<ul>
|
||||
<li><a href="https://developer.mozilla.org/en-US/docs/Web/CSS/Using_CSS_custom_properties">MDN: CSS Custom Properties</a></li>
|
||||
<li><a href="https://tailwindcss.com/docs/customizing-colors">Tailwind CSS: Customizing Colors</a></li>
|
||||
<li><a href="https://www.w3.org/WAI/WCAG21/Understanding/contrast-minimum.html">WCAG: Contrast Guidelines</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
|
||||
interface CodeBlockProps {
|
||||
code: string;
|
||||
language?: string;
|
||||
title?: string;
|
||||
}
|
||||
|
||||
export function CodeBlock({ code, language = 'tsx', title }: CodeBlockProps) {
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
const handleCopy = async () => {
|
||||
await navigator.clipboard.writeText(code);
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="my-4 rounded-lg overflow-hidden border border-gray-200">
|
||||
{title && (
|
||||
<div className="bg-gray-50 px-4 py-2 border-b border-gray-200 flex justify-between items-center">
|
||||
<span className="text-sm font-medium text-gray-700">{title}</span>
|
||||
<span className="text-xs text-gray-500">{language}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="relative">
|
||||
<button
|
||||
onClick={handleCopy}
|
||||
className="absolute top-2 right-2 px-3 py-1 text-xs bg-gray-700 hover:bg-gray-600 text-white rounded"
|
||||
>
|
||||
{copied ? '已复制' : '复制'}
|
||||
</button>
|
||||
|
||||
<pre className="p-4 bg-gray-900 text-gray-100 overflow-x-auto">
|
||||
<code>{code}</code>
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
|
||||
interface DemoProps {
|
||||
title: string;
|
||||
description?: string;
|
||||
code?: string;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export function Demo({ title, description, code, children }: DemoProps) {
|
||||
const [showCode, setShowCode] = useState(false);
|
||||
|
||||
return (
|
||||
<div className="my-8 border border-gray-200 rounded-lg overflow-hidden">
|
||||
<div className="bg-gray-50 px-4 py-3 border-b border-gray-200">
|
||||
<h3 className="text-lg font-semibold text-gray-900">{title}</h3>
|
||||
{description && (
|
||||
<p className="mt-1 text-sm text-gray-600">{description}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="p-6 bg-white">
|
||||
{children}
|
||||
</div>
|
||||
|
||||
{code && (
|
||||
<>
|
||||
<div className="border-t border-gray-200 px-4 py-2 bg-gray-50">
|
||||
<button
|
||||
onClick={() => setShowCode(!showCode)}
|
||||
className="text-sm font-medium text-gray-700 hover:text-gray-900"
|
||||
>
|
||||
{showCode ? '隐藏代码' : '查看代码'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{showCode && (
|
||||
<div className="border-t border-gray-200">
|
||||
<pre className="p-4 bg-gray-900 text-gray-100 overflow-x-auto text-sm">
|
||||
<code>{code}</code>
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
interface PropDefinition {
|
||||
name: string;
|
||||
type: string;
|
||||
default?: string;
|
||||
description: string;
|
||||
required?: boolean;
|
||||
}
|
||||
|
||||
interface PropsTableProps {
|
||||
data: PropDefinition[];
|
||||
}
|
||||
|
||||
export function PropsTable({ data }: PropsTableProps) {
|
||||
return (
|
||||
<div className="my-6 overflow-x-auto">
|
||||
<table className="w-full border-collapse">
|
||||
<thead>
|
||||
<tr className="border-b-2 border-gray-300">
|
||||
<th className="text-left py-3 px-4 font-semibold text-gray-900">属性</th>
|
||||
<th className="text-left py-3 px-4 font-semibold text-gray-900">类型</th>
|
||||
<th className="text-left py-3 px-4 font-semibold text-gray-900">默认值</th>
|
||||
<th className="text-left py-3 px-4 font-semibold text-gray-900">说明</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{data.map((prop) => (
|
||||
<tr key={prop.name} className="border-b border-gray-200">
|
||||
<td className="py-3 px-4">
|
||||
<code className="text-sm bg-gray-100 px-2 py-1 rounded">
|
||||
{prop.name}
|
||||
{prop.required && <span className="text-red-500 ml-1">*</span>}
|
||||
</code>
|
||||
</td>
|
||||
<td className="py-3 px-4">
|
||||
<code className="text-sm text-blue-600">{prop.type}</code>
|
||||
</td>
|
||||
<td className="py-3 px-4">
|
||||
{prop.default ? (
|
||||
<code className="text-sm bg-gray-100 px-2 py-1 rounded">{prop.default}</code>
|
||||
) : (
|
||||
<span className="text-gray-400">-</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="py-3 px-4 text-sm text-gray-700">{prop.description}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
'use client';
|
||||
|
||||
import Link from 'next/link';
|
||||
import { usePathname } from 'next/navigation';
|
||||
|
||||
interface NavItem {
|
||||
title: string;
|
||||
href: string;
|
||||
items?: NavItem[];
|
||||
}
|
||||
|
||||
const navigation: NavItem[] = [
|
||||
{
|
||||
title: '概览',
|
||||
href: '/',
|
||||
},
|
||||
{
|
||||
title: '栅格系统',
|
||||
href: '/grid-system',
|
||||
},
|
||||
{
|
||||
title: '核心组件',
|
||||
href: '/components',
|
||||
items: [
|
||||
{ title: 'Article', href: '/components/article' },
|
||||
{ title: 'Layer', href: '/components/layer' },
|
||||
{ title: '媒体组件', href: '/components/media' },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: '文本组件',
|
||||
href: '/text',
|
||||
},
|
||||
{
|
||||
title: '主题系统',
|
||||
href: '/theme',
|
||||
},
|
||||
{
|
||||
title: '示例',
|
||||
href: '/examples',
|
||||
items: [
|
||||
{ title: '跨栏布局', href: '/examples/spanning' },
|
||||
{ title: '响应式布局', href: '/examples/responsive' },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export function Sidebar() {
|
||||
const pathname = usePathname();
|
||||
|
||||
return (
|
||||
<nav className="w-64 border-r border-gray-200 bg-white h-screen sticky top-0 overflow-y-auto">
|
||||
<div className="p-6">
|
||||
<Link href="/" className="text-xl font-bold text-gray-900">
|
||||
NewspaperUI
|
||||
</Link>
|
||||
<p className="mt-1 text-sm text-gray-600">报纸布局组件库</p>
|
||||
</div>
|
||||
|
||||
<div className="px-4 pb-6">
|
||||
{navigation.map((item) => (
|
||||
<div key={item.href} className="mb-4">
|
||||
<Link
|
||||
href={item.href}
|
||||
className={`block px-3 py-2 rounded-md text-sm font-medium ${
|
||||
pathname === item.href
|
||||
? 'bg-gray-100 text-gray-900'
|
||||
: 'text-gray-700 hover:bg-gray-50'
|
||||
}`}
|
||||
>
|
||||
{item.title}
|
||||
</Link>
|
||||
|
||||
{item.items && (
|
||||
<div className="ml-4 mt-2 space-y-1">
|
||||
{item.items.map((subItem) => (
|
||||
<Link
|
||||
key={subItem.href}
|
||||
href={subItem.href}
|
||||
className={`block px-3 py-2 rounded-md text-sm ${
|
||||
pathname === subItem.href
|
||||
? 'bg-gray-100 text-gray-900'
|
||||
: 'text-gray-600 hover:bg-gray-50'
|
||||
}`}
|
||||
>
|
||||
{subItem.title}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import type { MDXComponents } from 'mdx/types';
|
||||
|
||||
export function useMDXComponents(components: MDXComponents): MDXComponents {
|
||||
return {
|
||||
...components,
|
||||
};
|
||||
}
|
||||
Vendored
+6
@@ -0,0 +1,6 @@
|
||||
/// <reference types="next" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
/// <reference path="./.next/types/routes.d.ts" />
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||
@@ -0,0 +1,25 @@
|
||||
import createMDX from '@next/mdx';
|
||||
import rehypePrettyCode from 'rehype-pretty-code';
|
||||
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
pageExtensions: ['js', 'jsx', 'md', 'mdx', 'ts', 'tsx'],
|
||||
transpilePackages: ['@newspaperui/components', '@newspaperui/theme', '@newspaperui/utils'],
|
||||
};
|
||||
|
||||
const withMDX = createMDX({
|
||||
extension: /\.mdx?$/,
|
||||
options: {
|
||||
rehypePlugins: [
|
||||
[
|
||||
rehypePrettyCode,
|
||||
{
|
||||
theme: 'github-dark',
|
||||
keepBackground: false,
|
||||
},
|
||||
],
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
export default withMDX(nextConfig);
|
||||
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"name": "@newspaperui/docs",
|
||||
"version": "0.0.0",
|
||||
"description": "Documentation site for newspaperui",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint",
|
||||
"clean": "rm -rf .next"
|
||||
},
|
||||
"dependencies": {
|
||||
"@mdx-js/loader": "^3.1.1",
|
||||
"@mdx-js/react": "^3.1.1",
|
||||
"@newspaperui/components": "workspace:*",
|
||||
"@newspaperui/theme": "workspace:*",
|
||||
"@newspaperui/utils": "workspace:*",
|
||||
"@next/mdx": "^16.2.6",
|
||||
"@types/mdx": "^2.0.13",
|
||||
"next": "^15.1.6",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"rehype-pretty-code": "^0.14.3",
|
||||
"shiki": "^4.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.10.5",
|
||||
"@types/react": "^18.3.18",
|
||||
"@types/react-dom": "^18.3.5",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"postcss": "^8.4.49",
|
||||
"tailwindcss": "^3.4.17",
|
||||
"typescript": "^5.7.2"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,15 @@
|
||||
import type { Config } from 'tailwindcss';
|
||||
|
||||
const config: Config = {
|
||||
content: [
|
||||
'./pages/**/*.{js,ts,jsx,tsx,mdx}',
|
||||
'./components/**/*.{js,ts,jsx,tsx,mdx}',
|
||||
'./app/**/*.{js,ts,jsx,tsx,mdx}',
|
||||
],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [],
|
||||
};
|
||||
|
||||
export default config;
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"noEmit": true,
|
||||
"incremental": true,
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"paths": {
|
||||
"@/*": ["./*"]
|
||||
},
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
]
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"name": "@newspaperui/theme",
|
||||
"version": "0.0.0",
|
||||
"description": "Theme tokens and CSS variables for newspaperui",
|
||||
"type": "module",
|
||||
"main": "./dist/index.cjs",
|
||||
"module": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"import": "./dist/index.js",
|
||||
"require": "./dist/index.cjs",
|
||||
"types": "./dist/index.d.ts"
|
||||
},
|
||||
"./variables.css": "./src/variables.css",
|
||||
"./tailwind.config": "./src/tailwind.config.js"
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
"src/variables.css",
|
||||
"src/tailwind.config.js"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "vite build",
|
||||
"dev": "vite build --watch",
|
||||
"lint": "tsc --noEmit",
|
||||
"clean": "rm -rf dist"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.7.2",
|
||||
"vite": "^5.4.11",
|
||||
"vite-plugin-dts": "^4.3.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
/**
|
||||
* @newspaperui/theme
|
||||
* Theme tokens and CSS variables for newspaperui
|
||||
*/
|
||||
|
||||
// Import CSS variables
|
||||
import './variables.css';
|
||||
|
||||
// Export visual weight types and configuration
|
||||
export type {
|
||||
VisualWeight,
|
||||
ComponentType,
|
||||
VisualWeightConfig,
|
||||
} from './visual-weights';
|
||||
|
||||
export { visualWeights, resolveFontSize } from './visual-weights';
|
||||
|
||||
// Export Tailwind config for consumers
|
||||
export { default as tailwindConfig } from './tailwind.config.js';
|
||||
|
||||
export const version = '0.0.0';
|
||||
@@ -0,0 +1,103 @@
|
||||
/**
|
||||
* Tailwind CSS Configuration for NewspaperUI
|
||||
* Extends Tailwind with newspaper-specific utilities and 24-column grid
|
||||
*/
|
||||
|
||||
const config = {
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
nui: {
|
||||
text: {
|
||||
primary: 'var(--nui-color-text-primary)',
|
||||
secondary: 'var(--nui-color-text-secondary)',
|
||||
tertiary: 'var(--nui-color-text-tertiary)',
|
||||
quaternary: 'var(--nui-color-text-quaternary)',
|
||||
muted: 'var(--nui-color-text-muted)',
|
||||
},
|
||||
gray: {
|
||||
50: 'var(--nui-color-gray-50)',
|
||||
100: 'var(--nui-color-gray-100)',
|
||||
200: 'var(--nui-color-gray-200)',
|
||||
300: 'var(--nui-color-gray-300)',
|
||||
400: 'var(--nui-color-gray-400)',
|
||||
500: 'var(--nui-color-gray-500)',
|
||||
600: 'var(--nui-color-gray-600)',
|
||||
700: 'var(--nui-color-gray-700)',
|
||||
800: 'var(--nui-color-gray-800)',
|
||||
900: 'var(--nui-color-gray-900)',
|
||||
},
|
||||
accent: {
|
||||
primary: 'var(--nui-color-accent-primary)',
|
||||
secondary: 'var(--nui-color-accent-secondary)',
|
||||
tertiary: 'var(--nui-color-accent-tertiary)',
|
||||
},
|
||||
},
|
||||
},
|
||||
spacing: {
|
||||
'nui-gutter': 'var(--nui-gutter)',
|
||||
'nui-gutter-sm': 'var(--nui-gutter-sm)',
|
||||
'nui-gutter-lg': 'var(--nui-gutter-lg)',
|
||||
},
|
||||
fontFamily: {
|
||||
'nui-serif': 'var(--nui-font-serif)',
|
||||
'nui-serif-display': 'var(--nui-font-serif-display)',
|
||||
'nui-sans': 'var(--nui-font-sans)',
|
||||
'nui-sans-condensed': 'var(--nui-font-sans-condensed)',
|
||||
'nui-mono': 'var(--nui-font-mono)',
|
||||
},
|
||||
fontSize: {
|
||||
'nui-xs': 'var(--nui-font-size-xs)',
|
||||
'nui-sm': 'var(--nui-font-size-sm)',
|
||||
'nui-base': 'var(--nui-font-size-base)',
|
||||
'nui-lg': 'var(--nui-font-size-lg)',
|
||||
'nui-xl': 'var(--nui-font-size-xl)',
|
||||
'nui-2xl': 'var(--nui-font-size-2xl)',
|
||||
'nui-3xl': 'var(--nui-font-size-3xl)',
|
||||
'nui-4xl': 'var(--nui-font-size-4xl)',
|
||||
'nui-5xl': 'var(--nui-font-size-5xl)',
|
||||
'nui-6xl': 'var(--nui-font-size-6xl)',
|
||||
},
|
||||
fontWeight: {
|
||||
'nui-normal': 'var(--nui-font-weight-normal)',
|
||||
'nui-medium': 'var(--nui-font-weight-medium)',
|
||||
'nui-semibold': 'var(--nui-font-weight-semibold)',
|
||||
'nui-bold': 'var(--nui-font-weight-bold)',
|
||||
},
|
||||
lineHeight: {
|
||||
'nui-tight': 'var(--nui-line-height-tight)',
|
||||
'nui-snug': 'var(--nui-line-height-snug)',
|
||||
'nui-normal': 'var(--nui-line-height-normal)',
|
||||
'nui-relaxed': 'var(--nui-line-height-relaxed)',
|
||||
'nui-loose': 'var(--nui-line-height-loose)',
|
||||
'nui-body': 'var(--nui-line-height-body)',
|
||||
},
|
||||
maxWidth: {
|
||||
'nui-grid': 'var(--nui-grid-max-width)',
|
||||
},
|
||||
// 24-column grid utilities
|
||||
gridTemplateColumns: {
|
||||
'nui-24': 'repeat(24, minmax(0, 1fr))',
|
||||
'nui-16': 'repeat(16, minmax(0, 1fr))',
|
||||
'nui-12': 'repeat(12, minmax(0, 1fr))',
|
||||
},
|
||||
gridColumn: {
|
||||
'span-13': 'span 13 / span 13',
|
||||
'span-14': 'span 14 / span 14',
|
||||
'span-15': 'span 15 / span 15',
|
||||
'span-16': 'span 16 / span 16',
|
||||
'span-17': 'span 17 / span 17',
|
||||
'span-18': 'span 18 / span 18',
|
||||
'span-19': 'span 19 / span 19',
|
||||
'span-20': 'span 20 / span 20',
|
||||
'span-21': 'span 21 / span 21',
|
||||
'span-22': 'span 22 / span 22',
|
||||
'span-23': 'span 23 / span 23',
|
||||
'span-24': 'span 24 / span 24',
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
};
|
||||
|
||||
export default config;
|
||||
@@ -0,0 +1,127 @@
|
||||
/**
|
||||
* NewspaperUI Theme Variables
|
||||
* Global CSS variables for newspaper layout system
|
||||
*/
|
||||
|
||||
:root {
|
||||
/* ========== Color System ========== */
|
||||
/* Primary text colors */
|
||||
--nui-color-text-primary: #111111;
|
||||
--nui-color-text-secondary: #222222;
|
||||
--nui-color-text-tertiary: #333333;
|
||||
--nui-color-text-quaternary: #444444;
|
||||
--nui-color-text-muted: #555555;
|
||||
|
||||
/* Gray scale */
|
||||
--nui-color-gray-50: #fafafa;
|
||||
--nui-color-gray-100: #f5f5f5;
|
||||
--nui-color-gray-200: #e5e5e5;
|
||||
--nui-color-gray-300: #d4d4d4;
|
||||
--nui-color-gray-400: #a3a3a3;
|
||||
--nui-color-gray-500: #737373;
|
||||
--nui-color-gray-600: #525252;
|
||||
--nui-color-gray-700: #404040;
|
||||
--nui-color-gray-800: #262626;
|
||||
--nui-color-gray-900: #171717;
|
||||
|
||||
/* Accent colors */
|
||||
--nui-color-accent-primary: #0066cc;
|
||||
--nui-color-accent-secondary: #cc0000;
|
||||
--nui-color-accent-tertiary: #006600;
|
||||
|
||||
/* ========== Spacing System ========== */
|
||||
/* Gutter (column gap) */
|
||||
--nui-gutter: 1rem;
|
||||
--nui-gutter-sm: 0.75rem;
|
||||
--nui-gutter-lg: 1.5rem;
|
||||
|
||||
/* Margin */
|
||||
--nui-margin-xs: 0.25rem;
|
||||
--nui-margin-sm: 0.5rem;
|
||||
--nui-margin-md: 0.75rem;
|
||||
--nui-margin-lg: 1rem;
|
||||
--nui-margin-xl: 1.5rem;
|
||||
|
||||
/* Padding */
|
||||
--nui-padding-xs: 0.25rem;
|
||||
--nui-padding-sm: 0.5rem;
|
||||
--nui-padding-md: 0.75rem;
|
||||
--nui-padding-lg: 1rem;
|
||||
--nui-padding-xl: 1.5rem;
|
||||
|
||||
/* ========== Font System ========== */
|
||||
/* Serif fonts for headlines and body text */
|
||||
--nui-font-serif: 'Georgia', 'Times New Roman', serif;
|
||||
--nui-font-serif-display: 'Playfair Display', 'Georgia', serif;
|
||||
|
||||
/* Sans-serif fonts for UI elements */
|
||||
--nui-font-sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Helvetica Neue', Arial, sans-serif;
|
||||
--nui-font-sans-condensed: 'Arial Narrow', 'Helvetica Condensed', sans-serif;
|
||||
|
||||
/* Monospace for code */
|
||||
--nui-font-mono: 'Courier New', Courier, monospace;
|
||||
|
||||
/* ========== Grid System ========== */
|
||||
/* 24-column grid */
|
||||
--nui-grid-columns: 24;
|
||||
--nui-grid-max-width: 1440px;
|
||||
--nui-grid-container-padding: 2rem;
|
||||
|
||||
/* Responsive breakpoints */
|
||||
--nui-breakpoint-sm: 768px;
|
||||
--nui-breakpoint-md: 1024px;
|
||||
--nui-breakpoint-lg: 1440px;
|
||||
|
||||
/* ========== Typography Scale ========== */
|
||||
/* Font sizes */
|
||||
--nui-font-size-xs: 0.75rem; /* 12px */
|
||||
--nui-font-size-sm: 0.875rem; /* 14px */
|
||||
--nui-font-size-base: 1rem; /* 16px */
|
||||
--nui-font-size-lg: 1.125rem; /* 18px */
|
||||
--nui-font-size-xl: 1.25rem; /* 20px */
|
||||
--nui-font-size-2xl: 1.5rem; /* 24px */
|
||||
--nui-font-size-3xl: 1.75rem; /* 28px */
|
||||
--nui-font-size-4xl: 2rem; /* 32px */
|
||||
--nui-font-size-5xl: 2.25rem; /* 36px */
|
||||
--nui-font-size-6xl: 3rem; /* 48px */
|
||||
|
||||
/* Font weights */
|
||||
--nui-font-weight-normal: 400;
|
||||
--nui-font-weight-medium: 500;
|
||||
--nui-font-weight-semibold: 600;
|
||||
--nui-font-weight-bold: 700;
|
||||
|
||||
/* Line heights */
|
||||
--nui-line-height-tight: 1.1;
|
||||
--nui-line-height-snug: 1.2;
|
||||
--nui-line-height-normal: 1.25;
|
||||
--nui-line-height-relaxed: 1.3;
|
||||
--nui-line-height-loose: 1.4;
|
||||
--nui-line-height-body: 1.5;
|
||||
}
|
||||
|
||||
[data-theme="dark"] {
|
||||
/* Primary text colors */
|
||||
--nui-color-text-primary: #eeeeee;
|
||||
--nui-color-text-secondary: #dddddd;
|
||||
--nui-color-text-tertiary: #cccccc;
|
||||
--nui-color-text-quaternary: #bbbbbb;
|
||||
--nui-color-text-muted: #aaaaaa;
|
||||
|
||||
/* Gray scale */
|
||||
--nui-color-gray-50: #171717;
|
||||
--nui-color-gray-100: #262626;
|
||||
--nui-color-gray-200: #404040;
|
||||
--nui-color-gray-300: #525252;
|
||||
--nui-color-gray-400: #737373;
|
||||
--nui-color-gray-500: #a3a3a3;
|
||||
--nui-color-gray-600: #d4d4d4;
|
||||
--nui-color-gray-700: #e5e5e5;
|
||||
--nui-color-gray-800: #f5f5f5;
|
||||
--nui-color-gray-900: #fafafa;
|
||||
|
||||
/* Accent colors */
|
||||
--nui-color-accent-primary: #4da6ff;
|
||||
--nui-color-accent-secondary: #ff6666;
|
||||
--nui-color-accent-tertiary: #66cc66;
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
/**
|
||||
* Resolves a fontSize value to a single CSS string.
|
||||
* For range tuples [min, max], returns the lower bound (min).
|
||||
*/
|
||||
export function resolveFontSize(fontSize: string | [string, string]): string {
|
||||
return Array.isArray(fontSize) ? fontSize[0] : fontSize;
|
||||
}
|
||||
|
||||
|
||||
export type VisualWeight = 'High' | 'Medium' | 'Low' | 'Standard';
|
||||
|
||||
export type ComponentType =
|
||||
| 'Headline'
|
||||
| 'Subhead'
|
||||
| 'BodyText'
|
||||
| 'Quote'
|
||||
| 'PullQuote'
|
||||
| 'Byline'
|
||||
| 'Caption';
|
||||
|
||||
export interface VisualWeightConfig {
|
||||
fontSize: string | [string, string];
|
||||
fontWeight: number;
|
||||
lineHeight: number;
|
||||
color: string;
|
||||
span: number | [number, number];
|
||||
margin: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Visual weight configuration mapping
|
||||
* Maps component types and their visual weights to specific styling configurations
|
||||
*/
|
||||
export const visualWeights: Record<
|
||||
ComponentType,
|
||||
Partial<Record<VisualWeight, VisualWeightConfig>>
|
||||
> = {
|
||||
Headline: {
|
||||
High: {
|
||||
fontSize: ['36px', '48px'],
|
||||
fontWeight: 700,
|
||||
lineHeight: 1.1,
|
||||
color: '#111',
|
||||
span: [6, 8],
|
||||
margin: '0 0 1rem 0',
|
||||
},
|
||||
Medium: {
|
||||
fontSize: ['28px', '34px'],
|
||||
fontWeight: 600,
|
||||
lineHeight: 1.2,
|
||||
color: '#111',
|
||||
span: [4, 6],
|
||||
margin: '0 0 0.75rem 0',
|
||||
},
|
||||
Low: {
|
||||
fontSize: ['22px', '26px'],
|
||||
fontWeight: 500,
|
||||
lineHeight: 1.3,
|
||||
color: '#222',
|
||||
span: [2, 4],
|
||||
margin: '0 0 0.5rem 0',
|
||||
},
|
||||
},
|
||||
Subhead: {
|
||||
High: {
|
||||
fontSize: ['20px', '24px'],
|
||||
fontWeight: 600,
|
||||
lineHeight: 1.25,
|
||||
color: '#222',
|
||||
span: [2, 3],
|
||||
margin: '0 0 0.5rem 0',
|
||||
},
|
||||
Medium: {
|
||||
fontSize: ['16px', '18px'],
|
||||
fontWeight: 500,
|
||||
lineHeight: 1.3,
|
||||
color: '#333',
|
||||
span: [1, 2],
|
||||
margin: '0 0 0.25rem 0',
|
||||
},
|
||||
},
|
||||
BodyText: {
|
||||
High: {
|
||||
fontSize: '16px',
|
||||
fontWeight: 400,
|
||||
lineHeight: 1.5,
|
||||
color: '#333',
|
||||
span: 1,
|
||||
margin: '0 0 1rem 0',
|
||||
},
|
||||
Medium: {
|
||||
fontSize: ['14px', '15px'],
|
||||
fontWeight: 400,
|
||||
lineHeight: 1.5,
|
||||
color: '#444',
|
||||
span: 1,
|
||||
margin: '0 0 0.75rem 0',
|
||||
},
|
||||
Low: {
|
||||
fontSize: ['12px', '14px'],
|
||||
fontWeight: 400,
|
||||
lineHeight: 1.4,
|
||||
color: '#555',
|
||||
span: 1,
|
||||
margin: '0 0 0.5rem 0',
|
||||
},
|
||||
},
|
||||
Quote: {
|
||||
High: {
|
||||
fontSize: ['20px', '24px'],
|
||||
fontWeight: 500,
|
||||
lineHeight: 1.4,
|
||||
color: '#222',
|
||||
span: 2,
|
||||
margin: '0 0 0.75rem 0',
|
||||
},
|
||||
Medium: {
|
||||
fontSize: ['16px', '18px'],
|
||||
fontWeight: 400,
|
||||
lineHeight: 1.4,
|
||||
color: '#333',
|
||||
span: 1,
|
||||
margin: '0 0 0.5rem 0',
|
||||
},
|
||||
},
|
||||
PullQuote: {
|
||||
High: {
|
||||
fontSize: ['24px', '28px'],
|
||||
fontWeight: 600,
|
||||
lineHeight: 1.2,
|
||||
color: '#111',
|
||||
span: [2, 3],
|
||||
margin: '0 0 0.5rem 0',
|
||||
},
|
||||
Medium: {
|
||||
fontSize: ['18px', '20px'],
|
||||
fontWeight: 500,
|
||||
lineHeight: 1.25,
|
||||
color: '#222',
|
||||
span: [1, 2],
|
||||
margin: '0 0 0.25rem 0',
|
||||
},
|
||||
},
|
||||
Byline: {
|
||||
Standard: {
|
||||
fontSize: ['12px', '14px'],
|
||||
fontWeight: 400,
|
||||
lineHeight: 1.3,
|
||||
color: '#555',
|
||||
span: 1,
|
||||
margin: '0 0 0.25rem 0',
|
||||
},
|
||||
},
|
||||
Caption: {
|
||||
Standard: {
|
||||
fontSize: ['12px', '14px'],
|
||||
fontWeight: 400,
|
||||
lineHeight: 1.3,
|
||||
color: '#555',
|
||||
span: 1,
|
||||
margin: '0.25rem 0',
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src"
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import { resolve } from 'path';
|
||||
import dts from 'vite-plugin-dts';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
dts({
|
||||
insertTypesEntry: true,
|
||||
}),
|
||||
],
|
||||
build: {
|
||||
lib: {
|
||||
entry: resolve(__dirname, 'src/index.ts'),
|
||||
name: 'NewspaperUITheme',
|
||||
formats: ['es', 'cjs'],
|
||||
fileName: (format) => `index.${format === 'es' ? 'js' : 'cjs'}`,
|
||||
},
|
||||
rollupOptions: {
|
||||
external: [],
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"name": "@newspaperui/utils",
|
||||
"version": "0.0.0",
|
||||
"description": "Utility functions for newspaperui",
|
||||
"type": "module",
|
||||
"main": "./dist/index.cjs",
|
||||
"module": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"import": "./dist/index.js",
|
||||
"require": "./dist/index.cjs",
|
||||
"types": "./dist/index.d.ts"
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "vite build",
|
||||
"dev": "vite build --watch",
|
||||
"lint": "tsc --noEmit",
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest",
|
||||
"clean": "rm -rf dist"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.7.2",
|
||||
"vite": "^5.4.11",
|
||||
"vite-plugin-dts": "^4.3.0",
|
||||
"vitest": "^2.1.8"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { calculateSpanWidth, validateSpan, calculateGutter } from '../grid';
|
||||
|
||||
describe('grid utilities', () => {
|
||||
describe('calculateSpanWidth', () => {
|
||||
it('should calculate correct width percentage for 24-column grid', () => {
|
||||
expect(calculateSpanWidth(6, 24)).toBe('25.000%');
|
||||
expect(calculateSpanWidth(12, 24)).toBe('50.000%');
|
||||
expect(calculateSpanWidth(24, 24)).toBe('100.000%');
|
||||
expect(calculateSpanWidth(1, 24)).toBe('4.167%');
|
||||
});
|
||||
|
||||
it('should calculate correct width percentage for 12-column grid', () => {
|
||||
expect(calculateSpanWidth(6, 12)).toBe('50.000%');
|
||||
expect(calculateSpanWidth(3, 12)).toBe('25.000%');
|
||||
expect(calculateSpanWidth(12, 12)).toBe('100.000%');
|
||||
});
|
||||
|
||||
it('should use 24 columns as default', () => {
|
||||
expect(calculateSpanWidth(6)).toBe('25.000%');
|
||||
expect(calculateSpanWidth(12)).toBe('50.000%');
|
||||
});
|
||||
|
||||
it('should throw error for invalid span', () => {
|
||||
expect(() => calculateSpanWidth(0, 24)).toThrow();
|
||||
expect(() => calculateSpanWidth(25, 24)).toThrow();
|
||||
expect(() => calculateSpanWidth(-1, 24)).toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('validateSpan', () => {
|
||||
it('should return true for valid spans', () => {
|
||||
expect(validateSpan(1, 24)).toBe(true);
|
||||
expect(validateSpan(12, 24)).toBe(true);
|
||||
expect(validateSpan(24, 24)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for invalid spans', () => {
|
||||
expect(validateSpan(0, 24)).toBe(false);
|
||||
expect(validateSpan(25, 24)).toBe(false);
|
||||
expect(validateSpan(-1, 24)).toBe(false);
|
||||
expect(validateSpan(1.5, 24)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('calculateGutter', () => {
|
||||
it('should calculate gutter width correctly', () => {
|
||||
const gutter = calculateGutter(1440, 24, 0.05);
|
||||
expect(gutter).toBeGreaterThan(0);
|
||||
expect(gutter).toBeLessThan(100);
|
||||
});
|
||||
|
||||
it('should throw error for invalid inputs', () => {
|
||||
expect(() => calculateGutter(0, 24, 0.05)).toThrow();
|
||||
expect(() => calculateGutter(1440, 0, 0.05)).toThrow();
|
||||
expect(() => calculateGutter(1440, 24, -0.1)).toThrow();
|
||||
expect(() => calculateGutter(1440, 24, 1.5)).toThrow();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,62 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import {
|
||||
calculateSpanWidth,
|
||||
calculateLayout,
|
||||
getResponsiveColumns,
|
||||
adjustSpanForScreen,
|
||||
} from '../index';
|
||||
|
||||
describe('integration tests', () => {
|
||||
it('should work together for responsive layout calculation', () => {
|
||||
// Scenario: Layout 3 items on different screen sizes
|
||||
const items = [
|
||||
{ id: 'headline', span: 12 },
|
||||
{ id: 'image', span: 8 },
|
||||
{ id: 'text', span: 4 },
|
||||
];
|
||||
|
||||
// Large screen (24 columns)
|
||||
const largeScreenColumns = getResponsiveColumns(1440);
|
||||
expect(largeScreenColumns).toBe(24);
|
||||
const largeLayout = calculateLayout(items, largeScreenColumns);
|
||||
expect(largeLayout).toHaveLength(3);
|
||||
// All items fit in one row: 12 + 8 + 4 = 24
|
||||
expect(largeLayout[0].row).toBe(0);
|
||||
expect(largeLayout[1].row).toBe(0);
|
||||
expect(largeLayout[2].row).toBe(0);
|
||||
|
||||
// Medium screen (16 columns) - adjust spans
|
||||
const mediumScreenColumns = getResponsiveColumns(900);
|
||||
expect(mediumScreenColumns).toBe(16);
|
||||
const adjustedItems = items.map((item) => ({
|
||||
...item,
|
||||
span: adjustSpanForScreen(item.span, 900),
|
||||
}));
|
||||
const mediumLayout = calculateLayout(adjustedItems, mediumScreenColumns);
|
||||
expect(mediumLayout).toHaveLength(3);
|
||||
|
||||
// Small screen (12 columns) - adjust spans
|
||||
const smallScreenColumns = getResponsiveColumns(600);
|
||||
expect(smallScreenColumns).toBe(12);
|
||||
const smallAdjustedItems = items.map((item) => ({
|
||||
...item,
|
||||
span: adjustSpanForScreen(item.span, 600),
|
||||
}));
|
||||
const smallLayout = calculateLayout(smallAdjustedItems, smallScreenColumns);
|
||||
expect(smallLayout).toHaveLength(3);
|
||||
});
|
||||
|
||||
it('should calculate correct widths for newspaper layout', () => {
|
||||
// 6-column headline in 24-column grid
|
||||
const headlineWidth = calculateSpanWidth(6, 24);
|
||||
expect(headlineWidth).toBe('25.000%');
|
||||
|
||||
// 8-column image in 24-column grid
|
||||
const imageWidth = calculateSpanWidth(8, 24);
|
||||
expect(imageWidth).toBe('33.333%');
|
||||
|
||||
// Full-width article
|
||||
const fullWidth = calculateSpanWidth(24, 24);
|
||||
expect(fullWidth).toBe('100.000%');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,90 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import {
|
||||
getResponsiveColumns,
|
||||
adjustSpanForScreen,
|
||||
isBreakpoint,
|
||||
getCurrentBreakpoint,
|
||||
BREAKPOINTS,
|
||||
} from '../responsive';
|
||||
|
||||
describe('responsive utilities', () => {
|
||||
describe('getResponsiveColumns', () => {
|
||||
it('should return 12 columns for small screens', () => {
|
||||
expect(getResponsiveColumns(320)).toBe(12);
|
||||
expect(getResponsiveColumns(767)).toBe(12);
|
||||
});
|
||||
|
||||
it('should return 16 columns for medium screens', () => {
|
||||
expect(getResponsiveColumns(768)).toBe(16);
|
||||
expect(getResponsiveColumns(1023)).toBe(16);
|
||||
});
|
||||
|
||||
it('should return 24 columns for large screens', () => {
|
||||
expect(getResponsiveColumns(1024)).toBe(24);
|
||||
expect(getResponsiveColumns(1440)).toBe(24);
|
||||
expect(getResponsiveColumns(1920)).toBe(24);
|
||||
});
|
||||
});
|
||||
|
||||
describe('adjustSpanForScreen', () => {
|
||||
it('should not adjust span for large screens (24 columns)', () => {
|
||||
expect(adjustSpanForScreen(12, 1440)).toBe(12);
|
||||
expect(adjustSpanForScreen(6, 1920)).toBe(6);
|
||||
expect(adjustSpanForScreen(24, 1024)).toBe(24);
|
||||
});
|
||||
|
||||
it('should proportionally adjust span for medium screens (16 columns)', () => {
|
||||
expect(adjustSpanForScreen(12, 800)).toBe(8); // 12/24 * 16 = 8
|
||||
expect(adjustSpanForScreen(6, 900)).toBe(4); // 6/24 * 16 = 4
|
||||
expect(adjustSpanForScreen(24, 1000)).toBe(16); // 24/24 * 16 = 16
|
||||
});
|
||||
|
||||
it('should proportionally adjust span for small screens (12 columns)', () => {
|
||||
expect(adjustSpanForScreen(12, 600)).toBe(6); // 12/24 * 12 = 6
|
||||
expect(adjustSpanForScreen(6, 400)).toBe(3); // 6/24 * 12 = 3
|
||||
expect(adjustSpanForScreen(24, 700)).toBe(12); // 24/24 * 12 = 12
|
||||
});
|
||||
|
||||
it('should ensure minimum span of 1', () => {
|
||||
expect(adjustSpanForScreen(1, 600)).toBe(1);
|
||||
expect(adjustSpanForScreen(2, 600)).toBeGreaterThanOrEqual(1);
|
||||
});
|
||||
|
||||
it('should not exceed target columns', () => {
|
||||
expect(adjustSpanForScreen(24, 600)).toBeLessThanOrEqual(12);
|
||||
expect(adjustSpanForScreen(24, 800)).toBeLessThanOrEqual(16);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isBreakpoint', () => {
|
||||
it('should correctly identify breakpoints', () => {
|
||||
expect(isBreakpoint(768, 'sm')).toBe(true);
|
||||
expect(isBreakpoint(767, 'sm')).toBe(false);
|
||||
expect(isBreakpoint(1024, 'md')).toBe(true);
|
||||
expect(isBreakpoint(1023, 'md')).toBe(false);
|
||||
expect(isBreakpoint(1440, 'lg')).toBe(true);
|
||||
expect(isBreakpoint(1439, 'lg')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getCurrentBreakpoint', () => {
|
||||
it('should return correct breakpoint names', () => {
|
||||
expect(getCurrentBreakpoint(320)).toBe('xs');
|
||||
expect(getCurrentBreakpoint(767)).toBe('xs');
|
||||
expect(getCurrentBreakpoint(768)).toBe('sm');
|
||||
expect(getCurrentBreakpoint(1023)).toBe('sm');
|
||||
expect(getCurrentBreakpoint(1024)).toBe('md');
|
||||
expect(getCurrentBreakpoint(1439)).toBe('md');
|
||||
expect(getCurrentBreakpoint(1440)).toBe('lg');
|
||||
expect(getCurrentBreakpoint(1920)).toBe('lg');
|
||||
});
|
||||
});
|
||||
|
||||
describe('BREAKPOINTS', () => {
|
||||
it('should have correct breakpoint values', () => {
|
||||
expect(BREAKPOINTS.sm).toBe(768);
|
||||
expect(BREAKPOINTS.md).toBe(1024);
|
||||
expect(BREAKPOINTS.lg).toBe(1440);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,74 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { isSpanValid, calculateLayout, type LayoutItem } from '../span';
|
||||
|
||||
describe('span utilities', () => {
|
||||
describe('isSpanValid', () => {
|
||||
it('should return true for valid spans', () => {
|
||||
expect(isSpanValid(1, 24)).toBe(true);
|
||||
expect(isSpanValid(12, 24)).toBe(true);
|
||||
expect(isSpanValid(24, 24)).toBe(true);
|
||||
expect(isSpanValid(6, 12)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for invalid spans', () => {
|
||||
expect(isSpanValid(0, 24)).toBe(false);
|
||||
expect(isSpanValid(25, 24)).toBe(false);
|
||||
expect(isSpanValid(-1, 24)).toBe(false);
|
||||
expect(isSpanValid(1.5, 24)).toBe(false);
|
||||
expect(isSpanValid(13, 12)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('calculateLayout', () => {
|
||||
it('should layout items in a single row when they fit', () => {
|
||||
const items: LayoutItem[] = [
|
||||
{ id: 'a', span: 6 },
|
||||
{ id: 'b', span: 6 },
|
||||
{ id: 'c', span: 6 },
|
||||
];
|
||||
const layout = calculateLayout(items, 24);
|
||||
|
||||
expect(layout).toHaveLength(3);
|
||||
expect(layout[0]).toEqual({ item: items[0], row: 0, col: 0 });
|
||||
expect(layout[1]).toEqual({ item: items[1], row: 0, col: 6 });
|
||||
expect(layout[2]).toEqual({ item: items[2], row: 0, col: 12 });
|
||||
});
|
||||
|
||||
it('should wrap to next row when items do not fit', () => {
|
||||
const items: LayoutItem[] = [
|
||||
{ id: 'a', span: 16 },
|
||||
{ id: 'b', span: 12 },
|
||||
];
|
||||
const layout = calculateLayout(items, 24);
|
||||
|
||||
expect(layout).toHaveLength(2);
|
||||
expect(layout[0]).toEqual({ item: items[0], row: 0, col: 0 });
|
||||
expect(layout[1]).toEqual({ item: items[1], row: 1, col: 0 });
|
||||
});
|
||||
|
||||
it('should handle exact row fills', () => {
|
||||
const items: LayoutItem[] = [
|
||||
{ id: 'a', span: 12 },
|
||||
{ id: 'b', span: 12 },
|
||||
{ id: 'c', span: 24 },
|
||||
];
|
||||
const layout = calculateLayout(items, 24);
|
||||
|
||||
expect(layout).toHaveLength(3);
|
||||
expect(layout[0]).toEqual({ item: items[0], row: 0, col: 0 });
|
||||
expect(layout[1]).toEqual({ item: items[1], row: 0, col: 12 });
|
||||
expect(layout[2]).toEqual({ item: items[2], row: 1, col: 0 });
|
||||
});
|
||||
|
||||
it('should throw error for invalid section columns', () => {
|
||||
const items: LayoutItem[] = [{ id: 'a', span: 6 }];
|
||||
expect(() => calculateLayout(items, 0)).toThrow();
|
||||
expect(() => calculateLayout(items, -1)).toThrow();
|
||||
});
|
||||
|
||||
it('should throw error for invalid item span', () => {
|
||||
const items: LayoutItem[] = [{ id: 'a', span: 25 }];
|
||||
expect(() => calculateLayout(items, 24)).toThrow();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,61 @@
|
||||
/**
|
||||
* Grid calculation utilities for 24-column newspaper layout system
|
||||
*/
|
||||
|
||||
/**
|
||||
* Calculate element width percentage in a grid system
|
||||
* @param span - Number of columns the element spans
|
||||
* @param totalColumns - Total number of columns in the grid (default: 24)
|
||||
* @returns Width as a percentage string (e.g., "33.333%")
|
||||
*/
|
||||
export function calculateSpanWidth(
|
||||
span: number,
|
||||
totalColumns: number = 24
|
||||
): string {
|
||||
if (!validateSpan(span, totalColumns)) {
|
||||
throw new Error(
|
||||
`Invalid span: ${span}. Must be between 1 and ${totalColumns}`
|
||||
);
|
||||
}
|
||||
const percentage = (span / totalColumns) * 100;
|
||||
return `${percentage.toFixed(3)}%`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate if a span value is within valid range
|
||||
* @param span - Number of columns to validate
|
||||
* @param maxColumns - Maximum number of columns allowed
|
||||
* @returns True if span is valid, false otherwise
|
||||
*/
|
||||
export function validateSpan(span: number, maxColumns: number): boolean {
|
||||
return Number.isInteger(span) && span >= 1 && span <= maxColumns;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate gutter width based on container width and column count
|
||||
* @param containerWidth - Total width of the container in pixels
|
||||
* @param columns - Number of columns in the grid
|
||||
* @param gutterRatio - Ratio of gutter to column width (default: 0.05)
|
||||
* @returns Gutter width in pixels
|
||||
*/
|
||||
export function calculateGutter(
|
||||
containerWidth: number,
|
||||
columns: number,
|
||||
gutterRatio: number = 0.05
|
||||
): number {
|
||||
if (containerWidth <= 0 || columns <= 0) {
|
||||
throw new Error('Container width and columns must be positive numbers');
|
||||
}
|
||||
if (gutterRatio < 0 || gutterRatio > 1) {
|
||||
throw new Error('Gutter ratio must be between 0 and 1');
|
||||
}
|
||||
|
||||
// Calculate column width considering gutters
|
||||
// Formula: containerWidth = (columns * columnWidth) + ((columns - 1) * gutter)
|
||||
// Where gutter = columnWidth * gutterRatio
|
||||
const totalGutterRatio = (columns - 1) * gutterRatio;
|
||||
const columnWidth = containerWidth / (columns + totalGutterRatio);
|
||||
const gutterWidth = columnWidth * gutterRatio;
|
||||
|
||||
return Math.round(gutterWidth * 100) / 100;
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* @newspaperui/utils
|
||||
* Utility functions for newspaperui component library
|
||||
*/
|
||||
|
||||
// Grid utilities
|
||||
export {
|
||||
calculateSpanWidth,
|
||||
validateSpan,
|
||||
calculateGutter,
|
||||
} from './grid';
|
||||
|
||||
// Span and layout utilities
|
||||
export {
|
||||
isSpanValid,
|
||||
calculateLayout,
|
||||
type LayoutItem,
|
||||
type PositionedLayoutItem,
|
||||
} from './span';
|
||||
|
||||
// Responsive utilities
|
||||
export {
|
||||
getResponsiveColumns,
|
||||
adjustSpanForScreen,
|
||||
isBreakpoint,
|
||||
getCurrentBreakpoint,
|
||||
BREAKPOINTS,
|
||||
} from './responsive';
|
||||
|
||||
export const version = '0.0.0';
|
||||
@@ -0,0 +1,85 @@
|
||||
/**
|
||||
* Responsive utilities for newspaper layout system
|
||||
*/
|
||||
|
||||
/**
|
||||
* Breakpoint thresholds for responsive design
|
||||
*/
|
||||
export const BREAKPOINTS = {
|
||||
sm: 768,
|
||||
md: 1024,
|
||||
lg: 1440,
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* Get recommended grid column count based on screen width
|
||||
* @param screenWidth - Current screen width in pixels
|
||||
* @returns Recommended number of columns (12, 16, or 24)
|
||||
*/
|
||||
export function getResponsiveColumns(screenWidth: number): number {
|
||||
if (screenWidth < BREAKPOINTS.sm) {
|
||||
return 12; // Small screens: 12 columns
|
||||
} else if (screenWidth < BREAKPOINTS.md) {
|
||||
return 16; // Medium screens: 16 columns
|
||||
} else {
|
||||
return 24; // Large screens: 24 columns
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adjust span value for different screen sizes
|
||||
* Proportionally scales span based on available columns
|
||||
* @param span - Original span value (based on 24 columns)
|
||||
* @param screenWidth - Current screen width in pixels
|
||||
* @returns Adjusted span value for current screen size
|
||||
*/
|
||||
export function adjustSpanForScreen(
|
||||
span: number,
|
||||
screenWidth: number
|
||||
): number {
|
||||
const targetColumns = getResponsiveColumns(screenWidth);
|
||||
const baseColumns = 24;
|
||||
|
||||
// If already at 24 columns, no adjustment needed
|
||||
if (targetColumns === baseColumns) {
|
||||
return span;
|
||||
}
|
||||
|
||||
// Calculate proportional span
|
||||
const adjustedSpan = Math.round((span / baseColumns) * targetColumns);
|
||||
|
||||
// Ensure at least 1 column and not exceeding target columns
|
||||
return Math.max(1, Math.min(adjustedSpan, targetColumns));
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if current screen width matches a breakpoint
|
||||
* @param screenWidth - Current screen width in pixels
|
||||
* @param breakpoint - Breakpoint name to check
|
||||
* @returns True if screen width is at or above the breakpoint
|
||||
*/
|
||||
export function isBreakpoint(
|
||||
screenWidth: number,
|
||||
breakpoint: keyof typeof BREAKPOINTS
|
||||
): boolean {
|
||||
return screenWidth >= BREAKPOINTS[breakpoint];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current breakpoint name based on screen width
|
||||
* @param screenWidth - Current screen width in pixels
|
||||
* @returns Current breakpoint name ('sm', 'md', 'lg', or 'xs' for below sm)
|
||||
*/
|
||||
export function getCurrentBreakpoint(
|
||||
screenWidth: number
|
||||
): 'xs' | 'sm' | 'md' | 'lg' {
|
||||
if (screenWidth >= BREAKPOINTS.lg) {
|
||||
return 'lg';
|
||||
} else if (screenWidth >= BREAKPOINTS.md) {
|
||||
return 'md';
|
||||
} else if (screenWidth >= BREAKPOINTS.sm) {
|
||||
return 'sm';
|
||||
} else {
|
||||
return 'xs';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
/**
|
||||
* Span and layout utilities for newspaper column system
|
||||
*/
|
||||
|
||||
/**
|
||||
* Check if an object's span is valid within a section's column count
|
||||
* @param objectSpan - Number of columns the object spans
|
||||
* @param sectionColumns - Total number of columns in the section
|
||||
* @returns True if the span is valid, false otherwise
|
||||
*/
|
||||
export function isSpanValid(
|
||||
objectSpan: number,
|
||||
sectionColumns: number
|
||||
): boolean {
|
||||
return (
|
||||
Number.isInteger(objectSpan) &&
|
||||
Number.isInteger(sectionColumns) &&
|
||||
objectSpan >= 1 &&
|
||||
objectSpan <= sectionColumns
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Layout item interface for grid positioning
|
||||
*/
|
||||
export interface LayoutItem {
|
||||
span: number;
|
||||
id: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Positioned layout item with row and column information
|
||||
*/
|
||||
export interface PositionedLayoutItem {
|
||||
item: LayoutItem;
|
||||
row: number;
|
||||
col: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate simple flow layout for items in a section
|
||||
* Items are placed left-to-right, wrapping to new rows when needed
|
||||
* @param items - Array of items to layout
|
||||
* @param sectionColumns - Total number of columns in the section
|
||||
* @returns Array of positioned items with row and column information
|
||||
*/
|
||||
export function calculateLayout(
|
||||
items: LayoutItem[],
|
||||
sectionColumns: number
|
||||
): PositionedLayoutItem[] {
|
||||
if (sectionColumns <= 0) {
|
||||
throw new Error('Section columns must be a positive number');
|
||||
}
|
||||
|
||||
const positioned: PositionedLayoutItem[] = [];
|
||||
let currentRow = 0;
|
||||
let currentCol = 0;
|
||||
|
||||
for (const item of items) {
|
||||
// Validate item span
|
||||
if (!isSpanValid(item.span, sectionColumns)) {
|
||||
throw new Error(
|
||||
`Invalid span ${item.span} for item ${item.id}. Must be between 1 and ${sectionColumns}`
|
||||
);
|
||||
}
|
||||
|
||||
// Check if item fits in current row
|
||||
if (currentCol + item.span > sectionColumns) {
|
||||
// Move to next row
|
||||
currentRow++;
|
||||
currentCol = 0;
|
||||
}
|
||||
|
||||
// Position the item
|
||||
positioned.push({
|
||||
item,
|
||||
row: currentRow,
|
||||
col: currentCol,
|
||||
});
|
||||
|
||||
// Update current column position
|
||||
currentCol += item.span;
|
||||
|
||||
// If we've filled the row exactly, move to next row
|
||||
if (currentCol === sectionColumns) {
|
||||
currentRow++;
|
||||
currentCol = 0;
|
||||
}
|
||||
}
|
||||
|
||||
return positioned;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src"
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import { resolve } from 'path';
|
||||
import dts from 'vite-plugin-dts';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
dts({
|
||||
insertTypesEntry: true,
|
||||
}),
|
||||
],
|
||||
build: {
|
||||
lib: {
|
||||
entry: resolve(__dirname, 'src/index.ts'),
|
||||
name: 'NewspaperUIUtils',
|
||||
formats: ['es', 'cjs'],
|
||||
fileName: (format) => `index.${format === 'es' ? 'js' : 'cjs'}`,
|
||||
},
|
||||
rollupOptions: {
|
||||
external: [],
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,13 @@
|
||||
import { defineConfig } from 'vitest/config';
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
globals: true,
|
||||
environment: 'node',
|
||||
coverage: {
|
||||
provider: 'v8',
|
||||
reporter: ['text', 'json', 'html'],
|
||||
exclude: ['node_modules/', 'dist/', '**/*.test.ts'],
|
||||
},
|
||||
},
|
||||
});
|
||||
Generated
+4980
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,2 @@
|
||||
packages:
|
||||
- 'packages/*'
|
||||
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"sourceMap": true,
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
}
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"$schema": "https://turbo.build/schema.json",
|
||||
"globalDependencies": ["**/.env.*local"],
|
||||
"tasks": {
|
||||
"build": {
|
||||
"dependsOn": ["^build"],
|
||||
"outputs": ["dist/**", ".next/**", "out/**"]
|
||||
},
|
||||
"dev": {
|
||||
"cache": false,
|
||||
"persistent": true
|
||||
},
|
||||
"lint": {
|
||||
"dependsOn": ["^build"]
|
||||
},
|
||||
"test": {
|
||||
"dependsOn": ["^build"]
|
||||
},
|
||||
"clean": {
|
||||
"cache": false
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user