-
This commit is contained in:
@@ -2,142 +2,220 @@
|
||||
# AI Agent Prompt: 生产级报纸组件库开发
|
||||
|
||||
## 任务目标
|
||||
开发一个浏览器端生产级报纸布局组件库,参考 InDesign,组件少而精、职责明确、无冗余、无歧义,支持全局 24 列栅格、跨栏、视觉权重和主题系统,提供文档网站(内嵌 demo,无需 storybook)。
|
||||
开发一个浏览器端生产级报纸布局组件库,参考 InDesign 与经典严肃风(NYT / The Times)排版传统,组件少而精、职责明确、无冗余、无歧义,支持全局 24 列栅格、跨栏、视觉权重和主题系统,提供文档网站(内嵌 demo,无需 storybook)。
|
||||
|
||||
视觉基调:经典严肃风(衬线为主、暖灰系、warm off-white 背景、双线分隔、首字下沉、栏间细线、真实多栏文字流)。
|
||||
|
||||
---
|
||||
|
||||
## 1. 项目 setup 与架构
|
||||
|
||||
- **Monorepo 管理**(pnpm workspace / Turborepo)
|
||||
- **Monorepo 管理**(pnpm workspace + Turborepo)
|
||||
- Packages:
|
||||
1. `components` → Layout / Section / Article / Layer / Text / Media
|
||||
2. `theme` → 全局主题变量、字体、颜色、间距、视觉权重映射
|
||||
3. `docs` → 文档网站(shadcn stack + demo)
|
||||
4. `utils` → 栅格计算、跨栏逻辑、响应式辅助函数
|
||||
1. `components` → Layout / Section / Article / Layer / Masthead / Rule / Headline / Subhead / Kicker / BodyText / Quote / Byline / Dateline / Caption / Image / Figure / Video / PullQuote
|
||||
2. `theme` → 字体导入、CSS variables、视觉权重映射、Tailwind tokens、排版工具类
|
||||
3. `docs` → 文档网站(Next.js + Tailwind + MDX)
|
||||
4. `utils` → 栅格校验(`validateSpan` / `clampSpan`)、`cx` 类名合并
|
||||
|
||||
- **目录结构示例**:
|
||||
- **目录结构**:
|
||||
```
|
||||
|
||||
/packages
|
||||
/components
|
||||
/Layout
|
||||
/Section
|
||||
/Article
|
||||
/Layer
|
||||
/Text
|
||||
/Media
|
||||
/theme
|
||||
/docs
|
||||
/utils
|
||||
|
||||
/components
|
||||
/src
|
||||
/layout (Layout / Section / Article / Layer / Masthead / Rule)
|
||||
/text (Headline / Subhead / Kicker / BodyText / Quote / Byline / Dateline / Caption)
|
||||
/media (Image / Figure / Video / PullQuote)
|
||||
/theme
|
||||
/src
|
||||
fonts.css (Google Fonts: Cormorant Garamond / Source Serif 4 / Inter / UnifrakturMaguntia)
|
||||
variables.css (CSS variables: 字体 / 暖灰系颜色 / 间距 / 分隔线 / 强调色)
|
||||
typography.css (drop-cap / small-caps / paragraph-flow / OSF / column-rule 等工具类)
|
||||
visual-weights.ts
|
||||
tailwind.config.js
|
||||
/docs
|
||||
/utils
|
||||
```
|
||||
|
||||
- **技术栈**:
|
||||
- React 18+, TypeScript
|
||||
- TailwindCSS + shadcn 文档 stack
|
||||
- React 18+, TypeScript 5+
|
||||
- TailwindCSS 3+ + 文档站基础 stack
|
||||
- Vitest + React Testing Library
|
||||
- 构建工具:Vite + Rollup
|
||||
- 构建:Vite + Rollup(components/theme/utils);Next.js 14+(docs)
|
||||
|
||||
---
|
||||
|
||||
## 2. 全局栅格系统
|
||||
|
||||
- `<Layout>` 顶层容器:
|
||||
- maxWidth, padding, gutter, theme
|
||||
- 栅格总列数 24
|
||||
- snap-to-grid 支持
|
||||
- Section 内对象 span ≤ Section.columns
|
||||
- 响应式调整:小屏 12–16 列
|
||||
- `columns`(默认 24)、`maxWidth`、`padding`、`theme`('light' | 'dark')
|
||||
- 通过 React Context 把 columns 下传给 `<Section>`
|
||||
- `<Section>` 内部使用 `display: grid; grid-template-columns: repeat(N, 1fr); gap: var(--nui-gutter)`
|
||||
- Section 内对象通过 `grid-column: span N` 跨栏,统一布局机制
|
||||
- 响应式:通过 CSS media query / container query 调整(不再使用 JS 路径)
|
||||
|
||||
---
|
||||
|
||||
## 3. 核心布局组件
|
||||
|
||||
1. `<Section>`:
|
||||
- columns, breakable, padding/margin, priority
|
||||
- 内部对象 span ≤ Section.columns
|
||||
- `columns`(必填)、`gap`、`breakable`、`divider`('none' | 'top' | 'bottom' | 'both')
|
||||
- 内部对象 span ≤ Section.columns,超过自动 clamp 并 console.warn
|
||||
|
||||
2. `<Article>`:
|
||||
- span, priority/weight, breakable
|
||||
- 内含 Headline / Subhead / BodyText / Image / PullQuote
|
||||
- `span`、`breakable`
|
||||
- 跨栏统一通过 `grid-column: span N`
|
||||
|
||||
3. `<Layer>`:
|
||||
- position, top/left/right/bottom, zIndex
|
||||
- 用于浮动广告、拉引、浮动图片
|
||||
- `position`('absolute' | 'fixed' | 'sticky')、`top/left/right/bottom`、`zIndex`
|
||||
|
||||
4. `<Masthead>` 报头组件:
|
||||
- `title`、`kicker`、`edition`、`date`、`price`
|
||||
- `variant`:'classic'(双线居中 + Cormorant Garamond)、'blackletter'(UnifrakturMaguntia 哥特体)、'modern'(左对齐 + accent 色)
|
||||
|
||||
5. `<Rule>` 分隔线组件:
|
||||
- `variant`:'hairline' | 'double' | 'thick'
|
||||
- `orientation`:'horizontal' | 'vertical'
|
||||
- `span`(横向时占多少列,默认 `1 / -1`)
|
||||
|
||||
---
|
||||
|
||||
## 4. 内容组件
|
||||
|
||||
- 文本类:Headline / Subhead / BodyText / Quote / Byline / Caption
|
||||
- 媒体类:Image / Figure / Video / PullQuote
|
||||
- 属性:span(跨栏)、weight(High/Medium/Low)、margin/padding
|
||||
- 所有 span 基于 Section.columns
|
||||
- 文本类:`<Headline>` / `<Subhead>` / `<Kicker>` / `<BodyText>` / `<Quote>` / `<Byline>` / `<Dateline>` / `<Caption>`
|
||||
- 媒体类:`<Image>` / `<Figure>` / `<Video>` / `<PullQuote>`
|
||||
- 通用属性:`weight`、`span`(跨栏,由 Section 的 grid-column 提供)、`className`、`style`
|
||||
- `<BodyText>` 核心扩展:
|
||||
- `columns?: 1 | 2 | 3 | 4` 启用 CSS multi-column 真实多栏文字流
|
||||
- `dropCap?: boolean` 首字下沉
|
||||
- 默认开启段间无空行 + 第二段起首行缩进 1em + old-style figures + hanging punctuation
|
||||
- `<Quote>`:`variant: 'block' | 'inline'`,block 用左缩进 1.5em(不使用左 border)
|
||||
- `<PullQuote>`:上下双 hairline + Display 字体;`spanAllColumns` 在多栏 BodyText 内自动跨所有栏
|
||||
- `<Byline>` / `<Dateline>` / `<Kicker>`:使用真小帽(OpenType `font-variant-caps: small-caps`,非 `text-transform: uppercase`)
|
||||
|
||||
---
|
||||
|
||||
## 5. 视觉权重映射表(24 列)
|
||||
## 5. 视觉权重映射表(修订版)
|
||||
|
||||
| 组件 | 权重 | 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 |
|
||||
经典严肃风修订要点:
|
||||
- 字号上限放开(Headline High 48–72px,原 36–48px 太小压不住头版)
|
||||
- font-weight 普遍下调(High 700 → 600,Medium 600 → 500),避免数码黑
|
||||
- line-height 衬线上调(BodyText 1.55–1.6)
|
||||
- 颜色使用暖灰系 token(`--nui-text-primary` `#1A1A1A` 而非 `#111`)
|
||||
- 颜色 / 字体均通过 CSS variable token,不硬编码 hex
|
||||
- 新增 4 档:Masthead / Kicker / Dateline / Caption credit
|
||||
|
||||
| 组件 | 权重 | font-family | font-size | font-weight | line-height | letter-spacing | font-variant | color | span | margin |
|
||||
|---|---|---|---|---|---|---|---|---|---|---|
|
||||
| Masthead | Standard | `--font-family-masthead` | 56–96px | 700 | 1.0 | 0.02em | lining-nums | `--nui-text-primary` | 24 | 0 |
|
||||
| Headline | High | `--font-family-display` | 48–72px | 600 | 1.05 | -0.01em | lining-nums + balance | `--nui-text-primary` | 8–16 | `0 0 1rem 0` |
|
||||
| Headline | Medium | `--font-family-headline` | 32–40px | 600 | 1.1 | -0.005em | lining-nums | `--nui-text-primary` | 6–10 | `0 0 0.75rem 0` |
|
||||
| Headline | Low | `--font-family-headline` | 22–26px | 500 | 1.2 | 0 | lining-nums | `--nui-text-body` | 4–6 | `0 0 0.5rem 0` |
|
||||
| Subhead | High | `--font-family-headline` italic | 18–22px | 500 | 1.3 | 0 | oldstyle-nums | `--nui-text-secondary` | 4–8 | `0 0 0.75rem 0` |
|
||||
| Subhead | Medium | `--font-family-headline` italic | 16–18px | 400 | 1.35 | 0 | oldstyle-nums | `--nui-text-secondary` | 2–4 | `0 0 0.5rem 0` |
|
||||
| Kicker | Standard | `--font-family-meta` small-caps | 12–13px | 600 | 1.2 | 0.08em | small-caps + lining | `--nui-accent-primary` | 1–4 | `0 0 0.25rem 0` |
|
||||
| BodyText | High | `--font-family-body` | 16–17px | 400 | 1.6 | 0 | oldstyle-nums | `--nui-text-body` | 1 | `0 0 0.75rem 0` |
|
||||
| BodyText | Medium | `--font-family-body` | 15px | 400 | 1.55 | 0 | oldstyle-nums | `--nui-text-body` | 1 | `0 0 0.5rem 0` |
|
||||
| BodyText | Low | `--font-family-body` | 13–14px | 400 | 1.5 | 0 | oldstyle-nums | `--nui-text-secondary` | 1 | `0 0 0.5rem 0` |
|
||||
| Quote (block) | High | `--font-family-body` | 17–19px | 400 | 1.55 | 0 | oldstyle-nums | `--nui-text-quote` | 2 | `1rem 0 1rem 1.5em` |
|
||||
| Quote (inline) | — | `--font-family-body` italic | inherit | 400 | inherit | 0 | inherit | inherit | — | 0 |
|
||||
| PullQuote | High | `--font-family-display` | 26–32px | 600 | 1.2 | -0.005em | lining-nums + balance | `--nui-text-primary` | 2–3 | `1.5rem 0` |
|
||||
| PullQuote | Medium | `--font-family-display` | 20–24px | 500 | 1.25 | 0 | lining-nums | `--nui-text-body` | 1–2 | `1rem 0` |
|
||||
| Caption | Standard | `--font-family-body` italic | 13px | 400 | 1.4 | 0 | oldstyle-nums | `--nui-text-secondary` | 1 | `0.5rem 0 0 0` |
|
||||
| Caption credit | — | `--font-family-meta` small-caps | 11–12px | 500 | 1.4 | 0.06em | small-caps | `--nui-text-muted` | 1 | inline |
|
||||
| Byline | Standard | `--font-family-meta` small-caps | 12–13px | 500 | 1.3 | 0.06em | small-caps + lining | `--nui-text-secondary` | 1 | `0 0 0.25rem 0` |
|
||||
| Dateline | Standard | `--font-family-meta` small-caps | 12–13px | 600 | 1.3 | 0.08em | small-caps + lining | `--nui-text-primary` | 1 | inline |
|
||||
|
||||
---
|
||||
|
||||
## 6. 主题系统
|
||||
|
||||
- 全局新闻字体预设
|
||||
- 颜色变量(黑/灰/强调色)
|
||||
- 间距变量(gutter, margin, padding)
|
||||
- 权重映射 Token
|
||||
- Tailwind + CSS Variables 支持主题切换
|
||||
### 6.1 字体(Google Fonts,免费可商用)
|
||||
- `--font-family-masthead`:Cormorant Garamond(报头主字)
|
||||
- `--font-family-blackletter`:UnifrakturMaguntia(哥特报头 preset)
|
||||
- `--font-family-display` / `--font-family-headline` / `--font-family-body`:Source Serif 4(Optical Size 可变字体,覆盖头条到正文)
|
||||
- `--font-family-meta`:Inter(Byline / Dateline / Kicker / Caption credit 用真小帽)
|
||||
|
||||
### 6.2 颜色(暖灰系,避开纯黑/纯白)
|
||||
|
||||
Light(默认):
|
||||
- 页面背景:`--nui-bg-page #F7F4ED`、`--nui-bg-surface #FBF9F4`
|
||||
- 文字:`--nui-text-primary #1A1A1A`、`--nui-text-body #22201C`、`--nui-text-secondary #4A4742`、`--nui-text-muted #6E6A63`、`--nui-text-quote #2E2A24`
|
||||
- 分隔线:`--nui-rule-hairline #C9C2B2`、`--nui-rule-decorative #1A1A1A`
|
||||
- 强调色:`--nui-accent-primary #7A1F1F`(NYT 朱红)、`--nui-accent-ink-blue #1B2A4A`(The Times 蓝)、`--nui-highlight #F2E9C8`(旧报纸黄)
|
||||
|
||||
Dark(暖深棕黑,非冷黑):
|
||||
- 反向映射保持暖意,accent 色提亮 + 降饱和
|
||||
|
||||
### 6.3 间距 / 排版工具类
|
||||
- 间距 token:`--nui-gutter`、`--nui-space-{1,2,3,4,6,8}`
|
||||
- 排版工具类(typography.css):
|
||||
- `.nui-drop-cap` 首字下沉(`::first-letter` + `float: left`)
|
||||
- `.nui-small-caps` 真小帽(OpenType `smcp/c2sc`)
|
||||
- `.nui-paragraph-flow` 段间无空行 + `p + p` 首行缩进 1em
|
||||
- `.nui-osf` old-style figures、`.nui-tnum` tabular figures
|
||||
- `.nui-hanging-punctuation` 悬挂引号
|
||||
- `.nui-column-rule` 栏间 hairline
|
||||
- `.nui-avoid-break` `break-inside: avoid`
|
||||
- `.nui-span-all-columns` `column-span: all`(多栏内跨栏)
|
||||
- `.nui-masthead-rule-top` / `.nui-masthead-rule-bottom` 报头双线
|
||||
|
||||
### 6.4 主题切换
|
||||
通过 `[data-theme="dark"]` 覆盖 CSS variables,组件代码无需感知主题;提供手动切换按钮。
|
||||
|
||||
---
|
||||
|
||||
## 7. 布局规则
|
||||
|
||||
- Section 内对象 span ≤ Section.columns
|
||||
- 高权重对象可跨多栏
|
||||
- Layer 可浮动/绝对定位
|
||||
- breakable 控制分页断开
|
||||
- 响应式:小屏 12–16 列
|
||||
- Section 内对象 span ≤ Section.columns,超过自动 clamp + console.warn(不抛异常崩溃)
|
||||
- 高权重对象可跨多栏(Headline High 跨 8–16 列)
|
||||
- Layer 浮动/绝对定位,脱离栅格流
|
||||
- `breakable` 控制打印分页断开(`break-inside: auto | avoid`)
|
||||
- BodyText 多栏:`columns: 1 | 2 | 3 | 4` 启用 CSS multi-column;通过 `column-rule: 1px solid var(--nui-rule-hairline)` 渲染栏间细线;标题与 PullQuote 自动 `break-inside: avoid` 避免被切断
|
||||
- 响应式:通过 CSS media query / container query 在小屏调整列数与栏宽(不使用 JS 路径)
|
||||
|
||||
---
|
||||
|
||||
## 8. 文档网站
|
||||
|
||||
- 技术栈:shadcn stack + Next.js + Tailwind + MDX
|
||||
- Demo 内嵌组件,展示属性、span、视觉权重、跨栏效果
|
||||
- 文档章节:
|
||||
1. 概览与目标
|
||||
2. 栅格系统与 Layout/Section 说明
|
||||
3. 核心组件(Article/Image/Layer)属性说明
|
||||
4. 文本组件属性与权重映射
|
||||
5. 主题与变量使用指南
|
||||
6. 跨栏和浮动 Layer 示例
|
||||
7. 响应式布局展示
|
||||
- 技术栈:Next.js + Tailwind + MDX
|
||||
- 页面:所有 demo 用真实组件渲染,非 mockup
|
||||
- 路由结构:
|
||||
1. `/` 首页 = NYT 派完整报纸头版(Masthead + Briefs + Lead Story 含 hero image + 3 栏 BodyText + drop cap + PullQuote + Secondary + Third Story 4 栏)
|
||||
2. `/grid-system` 栅格系统与 Layout/Section/Article/Layer 说明
|
||||
3. `/components/masthead` Masthead 三种 variant 对照
|
||||
4. `/components/article` Article + Layer 跨栏与浮动 demo
|
||||
5. `/components/rule` Rule 三种 variant 对照
|
||||
6. `/components/media` Image / Figure / Video / PullQuote
|
||||
7. `/text` 文本组件全集 + 视觉权重映射表(实时渲染)+ 多栏文字流 demo + drop cap demo
|
||||
8. `/theme` CSS variables 列表 + color swatch + 暗色主题切换按钮
|
||||
9. `/examples/spanning` 跨栏布局示例
|
||||
10. `/examples/responsive` 响应式布局展示
|
||||
11. `/examples/blackletter-frontpage` The Times / FAZ 派哥特体头版 preset
|
||||
|
||||
---
|
||||
|
||||
## 9. 实施顺序
|
||||
|
||||
1. 初始化 monorepo + Layout / Section 基础组件
|
||||
2. 配置主题系统、视觉权重映射 CSS / Tailwind Token
|
||||
3. 开发 Article / Image / Layer / PullQuote / 文本组件
|
||||
4. 实现 Section 网格跨栏逻辑
|
||||
5. 测试生产级报纸布局:跨栏、浮动 Layer、breakable、响应式
|
||||
6. 构建文档网站,内嵌 demo 展示组件属性与效果
|
||||
7. 确认生产级排版效果符合视觉权重、跨栏、主题、响应式要求
|
||||
1. 初始化 monorepo(pnpm workspace + Turborepo)
|
||||
2. 实现 theme(字体导入 + 暖灰系 CSS variables + 视觉权重表 + 排版工具类 + Tailwind tokens)
|
||||
3. 实现 utils(`validateSpan` / `clampSpan` / `cx`)
|
||||
4. 实现 components 三层(layout / text / media);统一 grid-column span 跨栏机制;BodyText 接入 CSS multi-column
|
||||
5. 测试生产级头版:跨栏、多栏文字流、首字下沉、栏间 hairline、真小帽、Pull Quote 跨栏、breakable、响应式
|
||||
6. 构建文档网站,首页直接展示 NYT 派头版作为生产级标杆,章节页用真实组件 demo
|
||||
7. 验收:视觉与传统报纸排版(NYT / The Times / FAZ)一致;自动化构建/测试通过;Design Agent 复评 ≥ 9/10
|
||||
|
||||
---
|
||||
|
||||
## 10. 验收标准
|
||||
|
||||
打开首页应满足:
|
||||
- Masthead 是 Cormorant Garamond 衬线 + 双线分隔
|
||||
- 头条字号 ≥ 48px、line-height ≤ 1.15、字重 600
|
||||
- 头条下方 BodyText 真分 3 栏,栏间有 1px 暖灰 hairline
|
||||
- 第一段开头有真实 drop cap(占 2–3 行)
|
||||
- Byline / Dateline / Kicker 是 Inter 真小帽(不是 uppercase 伪小帽)
|
||||
- PullQuote 上下双 hairline、字号 26–32px
|
||||
- 配色是 warm off-white + 暖深灰,不是 `#FFFFFF` + `#000000`
|
||||
- 数字是 old-style figures(3 / 5 / 7 有下伸笔)
|
||||
- 段间无空行,第二段起首行缩进 1em
|
||||
- 切换 `[data-theme="dark"]` 后是暖深棕黑(不是冷黑)
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
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>
|
||||
);
|
||||
};
|
||||
@@ -1,37 +0,0 @@
|
||||
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>
|
||||
);
|
||||
};
|
||||
@@ -1,47 +1,39 @@
|
||||
import React, { createContext, useContext } from 'react';
|
||||
'use client';
|
||||
import React, { createContext, useContext, ReactNode, CSSProperties } from 'react';
|
||||
|
||||
export interface LayoutProps {
|
||||
maxWidth?: string;
|
||||
padding?: string;
|
||||
gutter?: number;
|
||||
columns?: number; // 默认 24
|
||||
maxWidth?: string; // 默认 '1280px'
|
||||
padding?: string; // 默认 'var(--nui-space-6)'
|
||||
theme?: 'light' | 'dark';
|
||||
columns?: number;
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
style?: CSSProperties;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
interface LayoutContextValue {
|
||||
columns: number;
|
||||
gutter: number;
|
||||
}
|
||||
|
||||
const LayoutContext = createContext<LayoutContextValue>({
|
||||
columns: 24,
|
||||
gutter: 16,
|
||||
});
|
||||
|
||||
interface LayoutContextValue { columns: number; }
|
||||
const LayoutContext = createContext<LayoutContextValue>({ columns: 24 });
|
||||
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>
|
||||
);
|
||||
};
|
||||
columns = 24, maxWidth = '1280px', padding = 'var(--nui-space-6)',
|
||||
theme, className, style, children,
|
||||
}) => (
|
||||
<LayoutContext.Provider value={{ columns }}>
|
||||
<div
|
||||
data-theme={theme}
|
||||
className={className}
|
||||
style={{
|
||||
maxWidth,
|
||||
margin: '0 auto',
|
||||
padding,
|
||||
background: 'var(--nui-bg-page)',
|
||||
color: 'var(--nui-text-body)',
|
||||
fontFamily: 'var(--font-family-body)',
|
||||
...style,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</LayoutContext.Provider>
|
||||
);
|
||||
|
||||
@@ -1,43 +1,35 @@
|
||||
import React from 'react';
|
||||
import { calculateSpanWidth } from '@newspaperui/utils';
|
||||
import { useSection } from '../Section/Section';
|
||||
import { Caption } from '../Text/Caption';
|
||||
'use client';
|
||||
import React, { CSSProperties } from 'react';
|
||||
import { clampSpan, cx } from '@newspaperui/utils';
|
||||
import { useSection } from '../layout/Section';
|
||||
import { Caption } from '../text/Caption';
|
||||
|
||||
export interface FigureProps {
|
||||
src: string;
|
||||
alt: string;
|
||||
caption?: string;
|
||||
credit?: string;
|
||||
span?: number;
|
||||
children?: React.ReactNode;
|
||||
className?: string;
|
||||
style?: CSSProperties;
|
||||
}
|
||||
|
||||
export const Figure: React.FC<FigureProps> = ({
|
||||
src,
|
||||
alt,
|
||||
caption,
|
||||
span = 1,
|
||||
children,
|
||||
src, alt, caption, credit, span, className, style,
|
||||
}) => {
|
||||
const section = useSection();
|
||||
const width = calculateSpanWidth(span, section.columns);
|
||||
|
||||
const cols = span ? clampSpan(span, section.columns) : undefined;
|
||||
return (
|
||||
<figure
|
||||
className="newspaper-figure"
|
||||
style={{ width, margin: 0 }}
|
||||
data-span={span}
|
||||
className={cx('nui-figure nui-avoid-break', className)}
|
||||
style={{
|
||||
margin: 0,
|
||||
gridColumn: cols ? `span ${cols}` : undefined,
|
||||
...style,
|
||||
}}
|
||||
>
|
||||
<img
|
||||
src={src}
|
||||
alt={alt}
|
||||
style={{
|
||||
width: '100%',
|
||||
height: 'auto',
|
||||
display: 'block',
|
||||
}}
|
||||
/>
|
||||
{caption && <Caption>{caption}</Caption>}
|
||||
{children}
|
||||
<img src={src} alt={alt} style={{ display: 'block', width: '100%', height: 'auto' }} />
|
||||
{(caption || credit) && <Caption credit={credit}>{caption}</Caption>}
|
||||
</figure>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,42 +1,31 @@
|
||||
import React from 'react';
|
||||
import { calculateSpanWidth } from '@newspaperui/utils';
|
||||
import { useSection } from '../Section/Section';
|
||||
import { Caption } from '../Text/Caption';
|
||||
'use client';
|
||||
import React, { CSSProperties } from 'react';
|
||||
import { clampSpan, cx } from '@newspaperui/utils';
|
||||
import { useSection } from '../layout/Section';
|
||||
|
||||
export interface ImageProps {
|
||||
src: string;
|
||||
alt: string;
|
||||
span?: number;
|
||||
caption?: string;
|
||||
priority?: 'High' | 'Medium' | 'Low';
|
||||
className?: string;
|
||||
style?: CSSProperties;
|
||||
}
|
||||
|
||||
export const Image: React.FC<ImageProps> = ({
|
||||
src,
|
||||
alt,
|
||||
span = 1,
|
||||
caption,
|
||||
priority = 'Medium',
|
||||
}) => {
|
||||
export const Image: React.FC<ImageProps> = ({ src, alt, span, className, style }) => {
|
||||
const section = useSection();
|
||||
const width = calculateSpanWidth(span, section.columns);
|
||||
|
||||
const cols = span ? clampSpan(span, section.columns) : undefined;
|
||||
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>
|
||||
<img
|
||||
src={src}
|
||||
alt={alt}
|
||||
className={cx('nui-image', className)}
|
||||
style={{
|
||||
display: 'block',
|
||||
width: '100%',
|
||||
height: 'auto',
|
||||
gridColumn: cols ? `span ${cols}` : undefined,
|
||||
...style,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,61 +1,70 @@
|
||||
import React from 'react';
|
||||
'use client';
|
||||
import React, { ReactNode, CSSProperties } from 'react';
|
||||
import { visualWeights, resolveFontSize } from '@newspaperui/theme';
|
||||
import { calculateSpanWidth } from '@newspaperui/utils';
|
||||
import { useSection } from '../Section/Section';
|
||||
import { clampSpan, cx } from '@newspaperui/utils';
|
||||
import { useSection } from '../layout/Section';
|
||||
|
||||
export interface PullQuoteProps {
|
||||
weight?: 'High' | 'Medium';
|
||||
span?: number;
|
||||
spanAllColumns?: boolean; // 在多栏 BodyText 内跨所有栏
|
||||
author?: string;
|
||||
children: React.ReactNode;
|
||||
align?: 'left' | 'center';
|
||||
className?: string;
|
||||
style?: CSSProperties;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export const PullQuote: React.FC<PullQuoteProps> = ({
|
||||
weight = 'High',
|
||||
span,
|
||||
author,
|
||||
children,
|
||||
weight = 'High', span, spanAllColumns = false, author, align = 'left',
|
||||
className, style, 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);
|
||||
|
||||
const config = visualWeights.PullQuote[weight]!;
|
||||
const cols = span ? clampSpan(span, section.columns) : undefined;
|
||||
return (
|
||||
<aside
|
||||
className="newspaper-pull-quote"
|
||||
className={cx(
|
||||
'nui-pullquote nui-avoid-break nui-tnum',
|
||||
spanAllColumns && 'nui-span-all-columns',
|
||||
className,
|
||||
)}
|
||||
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)',
|
||||
padding: 'var(--nui-space-4) 0',
|
||||
borderTop: '1px solid var(--nui-rule-hairline)',
|
||||
borderBottom: '1px solid var(--nui-rule-hairline)',
|
||||
textAlign: align,
|
||||
gridColumn: cols ? `span ${cols}` : undefined,
|
||||
...style,
|
||||
}}
|
||||
data-weight={weight}
|
||||
data-span={finalSpan}
|
||||
>
|
||||
<blockquote style={{ margin: 0 }}>
|
||||
<p
|
||||
style={{
|
||||
fontFamily: `var(${config.fontFamily})`,
|
||||
fontSize: resolveFontSize(config.fontSize),
|
||||
fontWeight: config.fontWeight,
|
||||
lineHeight: config.lineHeight,
|
||||
letterSpacing: config.letterSpacing,
|
||||
color: `var(${config.color})`,
|
||||
margin: 0,
|
||||
textWrap: 'balance' as CSSProperties['textWrap'],
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</blockquote>
|
||||
</p>
|
||||
{author && (
|
||||
<cite
|
||||
<footer
|
||||
className="nui-small-caps"
|
||||
style={{
|
||||
display: 'block',
|
||||
marginTop: '0.5rem',
|
||||
fontSize: '0.875em',
|
||||
fontStyle: 'normal',
|
||||
marginTop: 'var(--nui-space-2)',
|
||||
fontSize: '11px',
|
||||
color: 'var(--nui-text-muted)',
|
||||
letterSpacing: '0.08em',
|
||||
}}
|
||||
>
|
||||
— {author}
|
||||
</cite>
|
||||
</footer>
|
||||
)}
|
||||
</aside>
|
||||
);
|
||||
|
||||
@@ -1,41 +1,32 @@
|
||||
import React from 'react';
|
||||
import { calculateSpanWidth } from '@newspaperui/utils';
|
||||
import { useSection } from '../Section/Section';
|
||||
import { Caption } from '../Text/Caption';
|
||||
'use client';
|
||||
import React, { CSSProperties } from 'react';
|
||||
import { clampSpan, cx } from '@newspaperui/utils';
|
||||
import { useSection } from '../layout/Section';
|
||||
import { Caption } from '../text/Caption';
|
||||
|
||||
export interface VideoProps {
|
||||
src: string;
|
||||
poster?: string;
|
||||
span?: number;
|
||||
caption?: string;
|
||||
credit?: string;
|
||||
span?: number;
|
||||
controls?: boolean;
|
||||
className?: string;
|
||||
style?: CSSProperties;
|
||||
}
|
||||
|
||||
export const Video: React.FC<VideoProps> = ({
|
||||
src,
|
||||
poster,
|
||||
span = 1,
|
||||
caption,
|
||||
src, poster, caption, credit, span, controls = true, className, style,
|
||||
}) => {
|
||||
const section = useSection();
|
||||
const width = calculateSpanWidth(span, section.columns);
|
||||
|
||||
const cols = span ? clampSpan(span, section.columns) : undefined;
|
||||
return (
|
||||
<div
|
||||
className="newspaper-video"
|
||||
style={{ width }}
|
||||
data-span={span}
|
||||
<figure
|
||||
className={cx('nui-video nui-avoid-break', className)}
|
||||
style={{ margin: 0, gridColumn: cols ? `span ${cols}` : undefined, ...style }}
|
||||
>
|
||||
<video
|
||||
src={src}
|
||||
poster={poster}
|
||||
controls
|
||||
style={{
|
||||
width: '100%',
|
||||
height: 'auto',
|
||||
display: 'block',
|
||||
}}
|
||||
/>
|
||||
{caption && <Caption>{caption}</Caption>}
|
||||
</div>
|
||||
<video src={src} poster={poster} controls={controls} style={{ width: '100%', height: 'auto' }} />
|
||||
{(caption || credit) && <Caption credit={credit}>{caption}</Caption>}
|
||||
</figure>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
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>
|
||||
);
|
||||
};
|
||||
@@ -1,44 +1,60 @@
|
||||
import React from 'react';
|
||||
'use client';
|
||||
import React, { ReactNode, CSSProperties } from 'react';
|
||||
import { visualWeights, resolveFontSize } from '@newspaperui/theme';
|
||||
import { calculateSpanWidth } from '@newspaperui/utils';
|
||||
import { useSection } from '../Section/Section';
|
||||
import { clampSpan, cx } from '@newspaperui/utils';
|
||||
import { useSection } from '../layout/Section';
|
||||
|
||||
export interface BodyTextProps {
|
||||
weight?: 'High' | 'Medium' | 'Low';
|
||||
span?: number;
|
||||
children: React.ReactNode;
|
||||
columns?: 1 | 2 | 3 | 4; // 多栏文字流
|
||||
columnWidth?: string; // 默认 '18em'
|
||||
columnFill?: 'auto' | 'balance';
|
||||
dropCap?: boolean;
|
||||
className?: string;
|
||||
style?: CSSProperties;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export const BodyText: React.FC<BodyTextProps> = ({
|
||||
weight = 'Medium',
|
||||
span,
|
||||
children,
|
||||
weight = 'Medium', span, columns = 1, columnWidth = '18em',
|
||||
columnFill = 'auto', dropCap = false,
|
||||
className, style, 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);
|
||||
const config = visualWeights.BodyText[weight]!;
|
||||
const cols = span ? clampSpan(span, section.columns) : undefined;
|
||||
const useColumns = columns >= 2;
|
||||
|
||||
return (
|
||||
<p
|
||||
className="newspaper-body-text"
|
||||
<div
|
||||
className={cx(
|
||||
'nui-bodytext',
|
||||
'nui-paragraph-flow',
|
||||
'nui-osf',
|
||||
'nui-hanging-punctuation',
|
||||
useColumns && 'nui-column-rule',
|
||||
dropCap && 'nui-drop-cap',
|
||||
className,
|
||||
)}
|
||||
style={{
|
||||
fontFamily: `var(${config.fontFamily})`,
|
||||
fontSize: resolveFontSize(config.fontSize),
|
||||
fontWeight: config.fontWeight,
|
||||
lineHeight: config.lineHeight,
|
||||
color: config.color,
|
||||
color: `var(${config.color})`,
|
||||
margin: config.margin,
|
||||
width,
|
||||
gridColumn: cols ? `span ${cols}` : undefined,
|
||||
columnCount: useColumns ? columns : undefined,
|
||||
columnWidth: useColumns ? columnWidth : undefined,
|
||||
columnGap: useColumns ? 'var(--nui-gutter)' : undefined,
|
||||
columnFill: useColumns ? columnFill : undefined,
|
||||
...style,
|
||||
}}
|
||||
data-weight={weight}
|
||||
data-span={finalSpan}
|
||||
data-columns={columns}
|
||||
>
|
||||
{children}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,26 +1,28 @@
|
||||
import React from 'react';
|
||||
'use client';
|
||||
import React, { ReactNode, CSSProperties } from 'react';
|
||||
import { visualWeights, resolveFontSize } from '@newspaperui/theme';
|
||||
import { cx } from '@newspaperui/utils';
|
||||
|
||||
export interface BylineProps {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
style?: CSSProperties;
|
||||
children: ReactNode; // e.g. "BY ALICE SMITH"
|
||||
}
|
||||
|
||||
export const Byline: React.FC<BylineProps> = ({ children }) => {
|
||||
const config = visualWeights.Byline.Standard;
|
||||
|
||||
if (!config) {
|
||||
throw new Error('Byline configuration not found');
|
||||
}
|
||||
|
||||
export const Byline: React.FC<BylineProps> = ({ className, style, children }) => {
|
||||
const config = visualWeights.Byline.Standard!;
|
||||
return (
|
||||
<div
|
||||
className="newspaper-byline"
|
||||
className={cx('nui-byline nui-small-caps', className)}
|
||||
style={{
|
||||
fontFamily: `var(${config.fontFamily})`,
|
||||
fontSize: resolveFontSize(config.fontSize),
|
||||
fontWeight: config.fontWeight,
|
||||
lineHeight: config.lineHeight,
|
||||
color: config.color,
|
||||
letterSpacing: config.letterSpacing,
|
||||
color: `var(${config.color})`,
|
||||
margin: config.margin,
|
||||
...style,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
||||
@@ -1,29 +1,47 @@
|
||||
import React from 'react';
|
||||
'use client';
|
||||
import React, { ReactNode, CSSProperties } from 'react';
|
||||
import { visualWeights, resolveFontSize } from '@newspaperui/theme';
|
||||
import { cx } from '@newspaperui/utils';
|
||||
|
||||
export interface CaptionProps {
|
||||
children: React.ReactNode;
|
||||
credit?: string; // e.g. "Photograph by Jane Doe"
|
||||
className?: string;
|
||||
style?: CSSProperties;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export const Caption: React.FC<CaptionProps> = ({ children }) => {
|
||||
const config = visualWeights.Caption.Standard;
|
||||
|
||||
if (!config) {
|
||||
throw new Error('Caption configuration not found');
|
||||
}
|
||||
|
||||
export const Caption: React.FC<CaptionProps> = ({ credit, className, style, children }) => {
|
||||
const config = visualWeights.Caption.Standard!;
|
||||
return (
|
||||
<figcaption
|
||||
className="newspaper-caption"
|
||||
className={cx('nui-caption nui-osf', className)}
|
||||
style={{
|
||||
fontFamily: `var(${config.fontFamily})`,
|
||||
fontSize: resolveFontSize(config.fontSize),
|
||||
fontWeight: config.fontWeight,
|
||||
fontStyle: config.fontStyle,
|
||||
lineHeight: config.lineHeight,
|
||||
color: config.color,
|
||||
color: `var(${config.color})`,
|
||||
margin: config.margin,
|
||||
...style,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
{credit && (
|
||||
<span
|
||||
className="nui-small-caps"
|
||||
style={{
|
||||
display: 'inline-block',
|
||||
marginLeft: 'var(--nui-space-2)',
|
||||
fontSize: '11px',
|
||||
color: 'var(--nui-text-muted)',
|
||||
fontStyle: 'normal',
|
||||
letterSpacing: '0.06em',
|
||||
}}
|
||||
>
|
||||
{credit}
|
||||
</span>
|
||||
)}
|
||||
</figcaption>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,51 +1,49 @@
|
||||
import React from 'react';
|
||||
'use client';
|
||||
import React, { ReactNode, CSSProperties } from 'react';
|
||||
import { visualWeights, resolveFontSize } from '@newspaperui/theme';
|
||||
import { calculateSpanWidth } from '@newspaperui/utils';
|
||||
import { useSection } from '../Section/Section';
|
||||
import { clampSpan, cx } from '@newspaperui/utils';
|
||||
import { useSection } from '../layout/Section';
|
||||
|
||||
const weightToTag: Record<'High' | 'Medium' | 'Low', 'h1' | 'h2' | 'h3'> = {
|
||||
High: 'h1',
|
||||
Medium: 'h2',
|
||||
Low: 'h3',
|
||||
High: 'h1', Medium: 'h2', Low: 'h3',
|
||||
};
|
||||
|
||||
export interface HeadlineProps {
|
||||
weight?: 'High' | 'Medium' | 'Low';
|
||||
span?: number;
|
||||
as?: 'h1' | 'h2' | 'h3' | 'h4';
|
||||
children: React.ReactNode;
|
||||
as?: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6';
|
||||
align?: 'left' | 'center' | 'right';
|
||||
className?: string;
|
||||
style?: CSSProperties;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export const Headline: React.FC<HeadlineProps> = ({
|
||||
weight = 'High',
|
||||
span,
|
||||
as,
|
||||
children,
|
||||
weight = 'High', span, as, align, className, style, 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];
|
||||
const config = visualWeights.Headline[weight]!;
|
||||
const Tag = (as ?? weightToTag[weight]) as keyof JSX.IntrinsicElements;
|
||||
const cols = span ? clampSpan(span, section.columns) : undefined;
|
||||
const variantClass = config.fontVariant?.includes('lining') ? 'nui-tnum' : 'nui-osf';
|
||||
|
||||
return (
|
||||
<Tag
|
||||
className="newspaper-headline"
|
||||
className={cx('nui-headline', variantClass, className)}
|
||||
style={{
|
||||
fontFamily: `var(${config.fontFamily})`,
|
||||
fontSize: resolveFontSize(config.fontSize),
|
||||
fontWeight: config.fontWeight,
|
||||
lineHeight: config.lineHeight,
|
||||
color: config.color,
|
||||
letterSpacing: config.letterSpacing,
|
||||
color: `var(${config.color})`,
|
||||
margin: config.margin,
|
||||
width,
|
||||
textAlign: align,
|
||||
textWrap: 'balance' as CSSProperties['textWrap'],
|
||||
gridColumn: cols ? `span ${cols}` : undefined,
|
||||
...style,
|
||||
}}
|
||||
data-weight={weight}
|
||||
data-span={finalSpan}
|
||||
>
|
||||
{children}
|
||||
</Tag>
|
||||
|
||||
@@ -1,42 +1,50 @@
|
||||
import React from 'react';
|
||||
'use client';
|
||||
import React, { ReactNode, CSSProperties } from 'react';
|
||||
import { visualWeights, resolveFontSize } from '@newspaperui/theme';
|
||||
import { calculateSpanWidth } from '@newspaperui/utils';
|
||||
import { useSection } from '../Section/Section';
|
||||
import { clampSpan, cx } from '@newspaperui/utils';
|
||||
import { useSection } from '../layout/Section';
|
||||
|
||||
export interface QuoteProps {
|
||||
variant?: 'block' | 'inline';
|
||||
weight?: 'High' | 'Medium';
|
||||
span?: number;
|
||||
children: React.ReactNode;
|
||||
cite?: string;
|
||||
className?: string;
|
||||
style?: CSSProperties;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export const Quote: React.FC<QuoteProps> = ({
|
||||
weight = 'Medium',
|
||||
span,
|
||||
children,
|
||||
variant = 'block', weight = 'Medium', span, cite, className, style, children,
|
||||
}) => {
|
||||
const section = useSection();
|
||||
const config = visualWeights.Quote[weight];
|
||||
const config = visualWeights.Quote[weight]!;
|
||||
const cols = span ? clampSpan(span, section.columns) : undefined;
|
||||
|
||||
if (!config) {
|
||||
throw new Error(`Invalid weight: ${weight} for Quote`);
|
||||
if (variant === 'inline') {
|
||||
return (
|
||||
<em className={cx('nui-quote nui-quote--inline', className)} style={style}>
|
||||
{children}
|
||||
</em>
|
||||
);
|
||||
}
|
||||
|
||||
const finalSpan = span || (Array.isArray(config.span) ? config.span[0] : config.span);
|
||||
const width = calculateSpanWidth(finalSpan, section.columns);
|
||||
|
||||
return (
|
||||
<blockquote
|
||||
className="newspaper-quote"
|
||||
cite={cite}
|
||||
className={cx('nui-quote nui-quote--block nui-osf', className)}
|
||||
style={{
|
||||
fontFamily: `var(${config.fontFamily})`,
|
||||
fontSize: resolveFontSize(config.fontSize),
|
||||
fontWeight: config.fontWeight,
|
||||
lineHeight: config.lineHeight,
|
||||
color: config.color,
|
||||
color: `var(${config.color})`,
|
||||
margin: config.margin,
|
||||
width,
|
||||
gridColumn: cols ? `span ${cols}` : undefined,
|
||||
borderLeft: 'none',
|
||||
...style,
|
||||
}}
|
||||
data-weight={weight}
|
||||
data-span={finalSpan}
|
||||
>
|
||||
{children}
|
||||
</blockquote>
|
||||
|
||||
@@ -1,44 +1,42 @@
|
||||
import React from 'react';
|
||||
'use client';
|
||||
import React, { ReactNode, CSSProperties } from 'react';
|
||||
import { visualWeights, resolveFontSize } from '@newspaperui/theme';
|
||||
import { calculateSpanWidth } from '@newspaperui/utils';
|
||||
import { useSection } from '../Section/Section';
|
||||
import { clampSpan, cx } from '@newspaperui/utils';
|
||||
import { useSection } from '../layout/Section';
|
||||
|
||||
export interface SubheadProps {
|
||||
weight?: 'High' | 'Medium';
|
||||
span?: number;
|
||||
children: React.ReactNode;
|
||||
as?: 'h2' | 'h3' | 'h4' | 'p';
|
||||
className?: string;
|
||||
style?: CSSProperties;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export const Subhead: React.FC<SubheadProps> = ({
|
||||
weight = 'Medium',
|
||||
span,
|
||||
children,
|
||||
weight = 'Medium', span, as = 'p', className, style, 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);
|
||||
|
||||
const config = visualWeights.Subhead[weight]!;
|
||||
const Tag = as as keyof JSX.IntrinsicElements;
|
||||
const cols = span ? clampSpan(span, section.columns) : undefined;
|
||||
return (
|
||||
<h2
|
||||
className="newspaper-subhead"
|
||||
<Tag
|
||||
className={cx('nui-subhead nui-osf', className)}
|
||||
style={{
|
||||
fontFamily: `var(${config.fontFamily})`,
|
||||
fontSize: resolveFontSize(config.fontSize),
|
||||
fontWeight: config.fontWeight,
|
||||
fontStyle: config.fontStyle,
|
||||
lineHeight: config.lineHeight,
|
||||
color: config.color,
|
||||
color: `var(${config.color})`,
|
||||
margin: config.margin,
|
||||
width,
|
||||
gridColumn: cols ? `span ${cols}` : undefined,
|
||||
...style,
|
||||
}}
|
||||
data-weight={weight}
|
||||
data-span={finalSpan}
|
||||
>
|
||||
{children}
|
||||
</h2>
|
||||
</Tag>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,51 +1,46 @@
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { render } from '@testing-library/react';
|
||||
import { Layout } from '../Layout/Layout';
|
||||
import { Section } from '../Section/Section';
|
||||
import { Article } from '../Article/Article';
|
||||
import { Layout } from '../layout/Layout';
|
||||
import { Section } from '../layout/Section';
|
||||
import { Article } from '../layout/Article';
|
||||
|
||||
describe('Article Component', () => {
|
||||
it('renders children correctly', () => {
|
||||
describe('Article', () => {
|
||||
it('applies grid-column span N when span is provided', () => {
|
||||
const { container } = render(
|
||||
<Layout>
|
||||
<Section columns={12}>
|
||||
<Article span={6}>
|
||||
<div>Article Content</div>
|
||||
</Article>
|
||||
<Article span={6}>x</Article>
|
||||
</Section>
|
||||
</Layout>
|
||||
</Layout>,
|
||||
);
|
||||
expect(container.textContent).toContain('Article Content');
|
||||
const article = container.querySelector('article') as HTMLElement;
|
||||
expect(article.style.gridColumn).toBe('span 6');
|
||||
expect(article.getAttribute('data-span')).toBe('6');
|
||||
});
|
||||
|
||||
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', () => {
|
||||
it('defaults to filling section.columns when span is omitted', () => {
|
||||
const { container } = render(
|
||||
<Layout>
|
||||
<Section columns={12}>
|
||||
<Article span={6}>
|
||||
<div>Half Width</div>
|
||||
</Article>
|
||||
<Section columns={8}>
|
||||
<Article>x</Article>
|
||||
</Section>
|
||||
</Layout>
|
||||
</Layout>,
|
||||
);
|
||||
const article = container.querySelector('.newspaper-article');
|
||||
expect(article).toHaveStyle({ gridColumn: 'span 6' });
|
||||
const article = container.querySelector('article') as HTMLElement;
|
||||
expect(article.style.gridColumn).toBe('span 8');
|
||||
expect(article.getAttribute('data-span')).toBe('8');
|
||||
});
|
||||
|
||||
it('clamps span when exceeding section.columns', () => {
|
||||
const { container } = render(
|
||||
<Layout>
|
||||
<Section columns={6}>
|
||||
<Article span={20}>x</Article>
|
||||
</Section>
|
||||
</Layout>,
|
||||
);
|
||||
const article = container.querySelector('article') as HTMLElement;
|
||||
expect(article.style.gridColumn).toBe('span 6');
|
||||
expect(article.getAttribute('data-span')).toBe('6');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,36 +1,39 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { Layout } from '../Layout/Layout';
|
||||
import { Section } from '../Section/Section';
|
||||
import { Layout, useLayout } from '../layout/Layout';
|
||||
|
||||
describe('Layout Component', () => {
|
||||
it('renders children correctly', () => {
|
||||
const ColumnsProbe = () => {
|
||||
const { columns } = useLayout();
|
||||
return <span data-testid="cols">{columns}</span>;
|
||||
};
|
||||
|
||||
describe('Layout', () => {
|
||||
it('renders children', () => {
|
||||
render(
|
||||
<Layout>
|
||||
<div>Test Content</div>
|
||||
</Layout>
|
||||
<p>hello</p>
|
||||
</Layout>,
|
||||
);
|
||||
expect(screen.getByText('Test Content')).toBeInTheDocument();
|
||||
expect(screen.getByText('hello')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('applies custom maxWidth and padding', () => {
|
||||
it('provides default columns context of 24', () => {
|
||||
render(
|
||||
<Layout>
|
||||
<ColumnsProbe />
|
||||
</Layout>,
|
||||
);
|
||||
expect(screen.getByTestId('cols').textContent).toBe('24');
|
||||
});
|
||||
|
||||
it('applies maxWidth and padding', () => {
|
||||
const { container } = render(
|
||||
<Layout maxWidth="1200px" padding="2rem">
|
||||
<div>Content</div>
|
||||
</Layout>
|
||||
<Layout maxWidth="900px" padding="16px">
|
||||
<p>x</p>
|
||||
</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();
|
||||
const root = container.firstChild as HTMLElement;
|
||||
expect(root.style.maxWidth).toBe('900px');
|
||||
expect(root.style.padding).toBe('16px');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,46 +1,49 @@
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { render } from '@testing-library/react';
|
||||
import { Layout } from '../Layout/Layout';
|
||||
import { Section } from '../Section/Section';
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { Layout } from '../layout/Layout';
|
||||
import { Section, useSection } from '../layout/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');
|
||||
});
|
||||
const ColsProbe = () => {
|
||||
const { columns } = useSection();
|
||||
return <span data-testid="section-cols">{columns}</span>;
|
||||
};
|
||||
|
||||
it('clamps columns exceeding layout columns and warns', () => {
|
||||
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
||||
describe('Section', () => {
|
||||
it('applies grid-template-columns repeat', () => {
|
||||
const { container } = render(
|
||||
<Layout columns={24}>
|
||||
<Section columns={30}>
|
||||
<div>Clamped</div>
|
||||
<Section columns={12}>
|
||||
<p>x</p>
|
||||
</Section>
|
||||
</Layout>
|
||||
</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();
|
||||
const section = container.querySelector('section') as HTMLElement;
|
||||
expect(section.style.display).toBe('grid');
|
||||
expect(section.style.gridTemplateColumns).toBe('repeat(12, 1fr)');
|
||||
expect(section.getAttribute('data-columns')).toBe('12');
|
||||
});
|
||||
|
||||
it('applies custom padding and margin', () => {
|
||||
it('clamps columns when exceeding layout.columns', () => {
|
||||
render(
|
||||
<Layout columns={12}>
|
||||
<Section columns={24}>
|
||||
<ColsProbe />
|
||||
</Section>
|
||||
</Layout>,
|
||||
);
|
||||
expect(screen.getByTestId('section-cols').textContent).toBe('12');
|
||||
});
|
||||
|
||||
it('applies top and bottom borders when divider="both"', () => {
|
||||
const { container } = render(
|
||||
<Layout>
|
||||
<Section columns={12} padding="1rem" margin="2rem">
|
||||
<div>Content</div>
|
||||
<Section columns={12} divider="both">
|
||||
<p>x</p>
|
||||
</Section>
|
||||
</Layout>
|
||||
</Layout>,
|
||||
);
|
||||
const section = container.querySelector('.newspaper-section');
|
||||
expect(section).toHaveStyle({ padding: '1rem', margin: '2rem' });
|
||||
const section = container.querySelector('section') as HTMLElement;
|
||||
expect(section.style.borderTop).toContain('1px');
|
||||
expect(section.style.borderBottom).toContain('1px');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,51 +1,43 @@
|
||||
/**
|
||||
* @newspaperui/components
|
||||
* React components for newspaperui
|
||||
*/
|
||||
import '@newspaperui/theme';
|
||||
|
||||
// Layout components
|
||||
export { Layout, useLayout } from './Layout/Layout';
|
||||
export type { LayoutProps } from './Layout/Layout';
|
||||
// layout
|
||||
export { Layout, useLayout } from './layout/Layout';
|
||||
export type { LayoutProps } from './layout/Layout';
|
||||
export { Section, useSection } from './layout/Section';
|
||||
export type { SectionProps } from './layout/Section';
|
||||
export { Article } from './layout/Article';
|
||||
export type { ArticleProps } from './layout/Article';
|
||||
export { Layer } from './layout/Layer';
|
||||
export type { LayerProps } from './layout/Layer';
|
||||
export { Masthead } from './layout/Masthead';
|
||||
export type { MastheadProps } from './layout/Masthead';
|
||||
export { Rule } from './layout/Rule';
|
||||
export type { RuleProps } from './layout/Rule';
|
||||
|
||||
export { Section, useSection } from './Section/Section';
|
||||
export type { SectionProps } from './Section/Section';
|
||||
// text
|
||||
export { Headline } from './text/Headline';
|
||||
export type { HeadlineProps } from './text/Headline';
|
||||
export { Subhead } from './text/Subhead';
|
||||
export type { SubheadProps } from './text/Subhead';
|
||||
export { Kicker } from './text/Kicker';
|
||||
export type { KickerProps } from './text/Kicker';
|
||||
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 { Dateline } from './text/Dateline';
|
||||
export type { DatelineProps } from './text/Dateline';
|
||||
export { Caption } from './text/Caption';
|
||||
export type { CaptionProps } from './text/Caption';
|
||||
|
||||
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';
|
||||
// media
|
||||
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';
|
||||
|
||||
@@ -1,192 +0,0 @@
|
||||
'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>
|
||||
);
|
||||
}
|
||||
@@ -1,199 +0,0 @@
|
||||
'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>
|
||||
);
|
||||
}
|
||||
@@ -1,271 +0,0 @@
|
||||
'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>
|
||||
);
|
||||
}
|
||||
@@ -1,251 +0,0 @@
|
||||
'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>
|
||||
);
|
||||
}
|
||||
@@ -1,267 +0,0 @@
|
||||
'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>
|
||||
);
|
||||
}
|
||||
@@ -1,35 +1,17 @@
|
||||
@import '@newspaperui/theme/variables.css';
|
||||
@import "@newspaperui/theme/dist/style.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;
|
||||
}
|
||||
html, body {
|
||||
background: var(--nui-bg-page);
|
||||
color: var(--nui-text-body);
|
||||
font-family: var(--font-family-body);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
text-rendering: optimizeLegibility;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; }
|
||||
|
||||
@@ -1,209 +0,0 @@
|
||||
'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>
|
||||
);
|
||||
}
|
||||
@@ -1,29 +1,15 @@
|
||||
import type { Metadata } from 'next';
|
||||
import { Sidebar } from '@/components/Sidebar';
|
||||
import './globals.css';
|
||||
import type { Metadata } from 'next';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'NewspaperUI - Documentation',
|
||||
description: 'A newspaper-style UI component library',
|
||||
title: 'NewspaperUI — Production Newspaper Components',
|
||||
description: '生产级报纸布局组件库,参考 InDesign 与经典严肃风排版传统,24 列栅格、跨栏、视觉权重和主题系统',
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
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 lang="zh">
|
||||
<body>{children}</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
|
||||
+190
-84
@@ -1,102 +1,208 @@
|
||||
'use client';
|
||||
|
||||
import { Demo } from '@/components/Demo';
|
||||
import { Layout, Section, Article } from '@newspaperui/components';
|
||||
import { Headline, BodyText } from '@newspaperui/components';
|
||||
import {
|
||||
Layout, Section, Article, Masthead, Rule,
|
||||
Headline, Subhead, Kicker, BodyText, Byline, Dateline,
|
||||
Figure, PullQuote,
|
||||
} from '@newspaperui/components';
|
||||
|
||||
export default function HomePage() {
|
||||
export default function FrontPage() {
|
||||
return (
|
||||
<div className="prose prose-lg max-w-none">
|
||||
<h1>NewspaperUI</h1>
|
||||
<p className="text-xl text-gray-600">
|
||||
生产级报纸布局组件库,参考 InDesign 设计理念,支持 24 列栅格、跨栏、视觉权重和主题系统。
|
||||
</p>
|
||||
<Layout columns={24} maxWidth="1200px" padding="2rem 1.5rem">
|
||||
<Masthead
|
||||
variant="classic"
|
||||
kicker="Late City Edition"
|
||||
title="The Daily Chronicle"
|
||||
edition="Vol. CXLIX · No. 51,895"
|
||||
date="Tuesday, May 19, 2026"
|
||||
price="$4.00"
|
||||
/>
|
||||
|
||||
<h2>设计理念</h2>
|
||||
<p>
|
||||
NewspaperUI 借鉴专业排版软件 InDesign 的设计思想,为 Web 应用提供报纸风格的布局能力。
|
||||
组件设计遵循少而精、职责明确、无冗余、无歧义的原则。
|
||||
</p>
|
||||
<Section columns={24} divider="bottom" gap="2rem" style={{ marginTop: '2rem' }}>
|
||||
<Article span={5} style={{ borderRight: '1px solid var(--nui-rule-hairline)', paddingRight: '1.5rem' }}>
|
||||
<Kicker>Inside Today</Kicker>
|
||||
<Headline weight="Low" as="h3" style={{ marginTop: 0 }}>
|
||||
Senate Approves Climate Resolution After Months of Debate
|
||||
</Headline>
|
||||
<BodyText weight="Low">
|
||||
<p>The unanimous vote concludes a contentious legislative session marked by partisan disputes
|
||||
and last-minute amendments. Page A6.</p>
|
||||
</BodyText>
|
||||
|
||||
<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>
|
||||
<Rule variant="hairline" style={{ margin: '1rem 0' }} />
|
||||
|
||||
<h2>快速开始</h2>
|
||||
<Headline weight="Low" as="h3">Tech Sector Gains as Inflation Eases</Headline>
|
||||
<BodyText weight="Low">
|
||||
<p>Major indices climbed for a fifth consecutive session as new data showed price growth
|
||||
slowing across consumer goods. Business B1.</p>
|
||||
</BodyText>
|
||||
|
||||
<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>
|
||||
<Rule variant="hairline" style={{ margin: '1rem 0' }} />
|
||||
|
||||
<h3>基础使用</h3>
|
||||
<Demo
|
||||
title="简单的报纸布局"
|
||||
description="使用 Layout 和 Section 创建基础的栅格布局"
|
||||
code={`import { Layout, Section, Article, Headline, BodyText } from '@newspaperui/components';
|
||||
<Headline weight="Low" as="h3">Drought Conditions Worsen Across the Plains</Headline>
|
||||
<BodyText weight="Low">
|
||||
<p>Officials in seven states have requested federal disaster relief as reservoir levels reach
|
||||
historic lows. National A12.</p>
|
||||
</BodyText>
|
||||
|
||||
function MyPage() {
|
||||
return (
|
||||
<Layout columns={24}>
|
||||
<Section columns={8}>
|
||||
<Article span={8}>
|
||||
<Headline weight="High">主要新闻标题</Headline>
|
||||
<BodyText>这是一篇重要的新闻内容...</BodyText>
|
||||
<Rule variant="hairline" style={{ margin: '1rem 0' }} />
|
||||
|
||||
<Headline weight="Low" as="h3">New Exhibit Opens at the Metropolitan</Headline>
|
||||
<BodyText weight="Low">
|
||||
<p>A retrospective of mid-century textile design draws record opening crowds. Arts C3.</p>
|
||||
</BodyText>
|
||||
</Article>
|
||||
|
||||
<Article span={14}>
|
||||
<div style={{ textAlign: 'center' }}><Kicker>Capitol · Breaking</Kicker></div>
|
||||
<Headline weight="High" align="center">
|
||||
Historic Accord Reshapes Continental Trade After Marathon Session
|
||||
</Headline>
|
||||
<Subhead weight="High" style={{ textAlign: 'center', marginTop: 0 }}>
|
||||
Negotiators emerge with sweeping framework on tariffs, labor, and emissions; ratification expected within weeks
|
||||
</Subhead>
|
||||
<div style={{ display: 'flex', justifyContent: 'center', gap: '1rem', margin: '0.5rem 0 1rem', alignItems: 'baseline' }}>
|
||||
<Byline>By Eleanor Whitcombe and Marcus Reyes</Byline>
|
||||
<span style={{ color: 'var(--nui-text-muted)' }}>·</span>
|
||||
<span style={{ fontFamily: 'var(--font-family-meta)', fontSize: '12px', color: 'var(--nui-text-muted)' }}>5 min read</span>
|
||||
</div>
|
||||
|
||||
<Figure
|
||||
src="https://images.unsplash.com/photo-1572949645841-094f3a9c4c94?auto=format&fit=crop&w=1200&q=80"
|
||||
alt="Diplomats applaud after the final draft was approved"
|
||||
caption="Negotiators applaud after the final draft was approved Monday evening at the Continental Conference Center."
|
||||
credit="Photograph by Jane Doe / Pool"
|
||||
/>
|
||||
|
||||
<BodyText weight="High" columns={3} dropCap style={{ marginTop: '1.5rem' }}>
|
||||
<p><Dateline>Brussels —</Dateline> After eleven consecutive days of negotiation that several
|
||||
participants described as the most demanding in a generation, delegates from twenty-three nations
|
||||
announced on Monday a sweeping framework to reorganize commerce across the continent. The accord,
|
||||
which still requires ratification by member parliaments, would harmonize tariff schedules, set
|
||||
common labor standards, and bind signatories to a shared emissions pathway through 2040.</p>
|
||||
|
||||
<p>Officials briefed on the talks said the breakthrough came shortly before midnight, when a
|
||||
dispute over agricultural subsidies was resolved with a side letter granting transitional relief
|
||||
to producers in five smaller economies. The chief negotiator, Margarethe Lindqvist, called the
|
||||
outcome “a long argument that finally became a conversation.”</p>
|
||||
|
||||
<p>The framework’s most consequential provisions target heavy industry. Cement, steel, and
|
||||
chemical producers would face a graduated carbon levy beginning in 2028, with revenues recycled
|
||||
into a continental investment fund for low-carbon manufacturing. Industry associations expressed
|
||||
cautious support, while environmental groups praised the levy’s binding architecture but warned
|
||||
that the timeline gives polluters too much room to delay.</p>
|
||||
|
||||
<p>Markets reacted with measured optimism. The continental composite index closed up 1.2 percent,
|
||||
led by capital-goods makers expected to benefit from infrastructure investment. The currency
|
||||
strengthened against the dollar by 0.7 percent. Bond yields, which had climbed throughout the
|
||||
negotiations on fiscal-stability concerns, retreated to levels seen before the talks began.</p>
|
||||
|
||||
<p>Domestic political reaction was mixed. The accord’s labor provisions, which establish minimum
|
||||
standards for paid leave and collective bargaining, drew immediate praise from union federations
|
||||
and equally immediate concern from chambers of commerce. The chairman of the Federation of
|
||||
Industries warned that small firms would struggle with compliance costs absent transitional support.</p>
|
||||
|
||||
<p>Parliamentary leaders in three capitals signaled that ratification could occur before the
|
||||
summer recess. Two governments, however, indicated that they would seek public referenda before
|
||||
committing, a process likely to extend into the autumn. Analysts at the Centre for Trade Studies
|
||||
estimated that full implementation, even on the most expedited timeline, would require at least
|
||||
eighteen months.</p>
|
||||
|
||||
<p>For ordinary travelers and consumers, the immediate effects will be modest. Border procedures
|
||||
and product standards remain governed by existing arrangements pending ratification. The longer
|
||||
arc is what matters: a continent of historically fractious neighbors agreeing on a single set of
|
||||
rules for the most consequential decade in living memory.</p>
|
||||
</BodyText>
|
||||
|
||||
<PullQuote weight="High" author="Margarethe Lindqvist, Chief Negotiator" align="center" style={{ margin: '2rem 0' }}>
|
||||
A long argument that finally became a conversation.
|
||||
</PullQuote>
|
||||
|
||||
<BodyText weight="High" columns={2} style={{ marginTop: '1rem' }}>
|
||||
<p>The accord’s signing ceremony, originally scheduled for last Friday, was delayed three times
|
||||
as drafters reconciled competing texts on dispute resolution. The final compromise establishes
|
||||
an arbitration panel of nine jurists, three appointed by each of the bloc’s three regional
|
||||
groupings, with binding authority over commercial disputes exceeding twenty million units.</p>
|
||||
|
||||
<p>Critics on the populist right denounced the framework as an erosion of national sovereignty,
|
||||
while critics on the left argued that the labor floor was set too low to meaningfully protect
|
||||
workers in tighter regulatory regimes. Both camps signaled that ratification battles would be
|
||||
fierce, particularly in legislatures with narrow majorities.</p>
|
||||
</BodyText>
|
||||
</Article>
|
||||
|
||||
<Article span={5} style={{ borderLeft: '1px solid var(--nui-rule-hairline)', paddingLeft: '1.5rem' }}>
|
||||
<Kicker>Foreign Desk</Kicker>
|
||||
<Headline weight="Medium" as="h2">
|
||||
Coastal Nations Pledge Joint Action on Maritime Pollution
|
||||
</Headline>
|
||||
<Subhead weight="Medium">
|
||||
Pact follows years of stalled regional talks and a cascade of recent shipping accidents.
|
||||
</Subhead>
|
||||
<Byline>By Tomás Almeida</Byline>
|
||||
|
||||
<BodyText weight="Medium" style={{ marginTop: '0.75rem' }}>
|
||||
<p><Dateline>Lisbon —</Dateline> Eleven coastal nations announced a binding compact to coordinate
|
||||
cleanup operations and harmonize liability rules for vessels exceeding fifty thousand tons. The
|
||||
agreement establishes a shared rapid-response fund and creates a regional inspectorate empowered
|
||||
to detain non-compliant ships in any signatory port.</p>
|
||||
|
||||
<p>Maritime industry groups received the news with caution. A spokesperson for the Continental
|
||||
Shipping Council acknowledged that “stronger common rules are overdue” but warned that
|
||||
implementation costs could fall disproportionately on smaller operators.</p>
|
||||
|
||||
<p>The compact takes effect on January 1, pending technical annexes. Environmental observers
|
||||
described the pact as the most consequential maritime accord in a decade.</p>
|
||||
</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>
|
||||
<Section columns={24} divider="top" gap="2rem" style={{ marginTop: '2rem', paddingTop: '2rem' }}>
|
||||
<Article span={24}>
|
||||
<Kicker>National · Investigation</Kicker>
|
||||
<Headline weight="Medium" as="h2">
|
||||
Records Reveal Years of Overlooked Warnings at Aging Reservoirs
|
||||
</Headline>
|
||||
<Subhead weight="Medium">
|
||||
Internal inspection memoranda, obtained through public records requests, suggest that
|
||||
structural concerns flagged repeatedly by field engineers were not escalated to senior staff.
|
||||
</Subhead>
|
||||
<Byline style={{ marginBottom: '1rem' }}>By Ravi Nair, Anita Kowalski, and Charles Weston</Byline>
|
||||
|
||||
<BodyText weight="High" columns={4}>
|
||||
<p><Dateline>Sacramento —</Dateline> A six-month review of more than four thousand pages of
|
||||
inspection records, interviews with twenty-three current and former engineers, and reconstructions
|
||||
of three near-failure incidents reveals a pattern of unheeded warnings about the structural
|
||||
integrity of mid-twentieth-century earthen dams across the western states.</p>
|
||||
|
||||
<p>The records show that field engineers documented concerns about seepage, erosion, and spillway
|
||||
capacity in repeated annual assessments dating back at least fifteen years. In several instances,
|
||||
those concerns were rated “moderate” in the field reports but downgraded to “low” by the time they
|
||||
reached senior officials. The pattern was particularly pronounced at three facilities serving
|
||||
regions of more than two million residents.</p>
|
||||
|
||||
<p>Officials at the Department of Water Resources, asked to review excerpts of the records, said
|
||||
in a written statement that “every reservoir under our oversight has been deemed safe for current
|
||||
operations” but did not specifically address the discrepancies between field and final ratings.
|
||||
The agency declined to make senior staff available for interviews.</p>
|
||||
|
||||
<p>The findings come amid renewed scrutiny of aging infrastructure following the partial collapse
|
||||
of an earthen embankment in March that displaced more than fifteen hundred residents. Federal
|
||||
inspectors who responded to that incident found the proximate cause to be precisely the type of
|
||||
seepage concern that field engineers had flagged in three of the past four annual assessments.</p>
|
||||
|
||||
<p>The investigative review found that of forty-seven reservoirs surveyed, sixteen had at least
|
||||
one instance in which a “moderate” or “high” field rating was downgraded before reaching senior
|
||||
management. In nine cases, the downgrades persisted for three or more consecutive years. None of
|
||||
the affected facilities have publicly disclosed the discrepancies.</p>
|
||||
|
||||
<p>Engineering professional associations have, in recent years, called for an independent review
|
||||
of inspection workflows in the western states. A spokesperson for the Society of Hydraulic
|
||||
Engineers said the Society was “deeply concerned” by the patterns described and would convene a
|
||||
working group to examine reform options.</p>
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,334 +0,0 @@
|
||||
'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>
|
||||
);
|
||||
}
|
||||
@@ -1,310 +0,0 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -1,96 +1,134 @@
|
||||
'use client';
|
||||
|
||||
import Link from 'next/link';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import { cx } from '@newspaperui/utils';
|
||||
|
||||
interface NavItem {
|
||||
title: string;
|
||||
label: string;
|
||||
href: string;
|
||||
items?: NavItem[];
|
||||
children?: NavItem[];
|
||||
}
|
||||
|
||||
const navigation: NavItem[] = [
|
||||
const nav: NavItem[] = [
|
||||
{ label: '概览 / 头版', href: '/' },
|
||||
{ label: '栅格系统', href: '/grid-system' },
|
||||
{
|
||||
title: '概览',
|
||||
href: '/',
|
||||
},
|
||||
{
|
||||
title: '栅格系统',
|
||||
href: '/grid-system',
|
||||
},
|
||||
{
|
||||
title: '核心组件',
|
||||
href: '/components',
|
||||
items: [
|
||||
{ title: 'Article', href: '/components/article' },
|
||||
{ title: 'Layer', href: '/components/layer' },
|
||||
{ title: '媒体组件', href: '/components/media' },
|
||||
label: '布局组件',
|
||||
href: '/components/article',
|
||||
children: [
|
||||
{ label: 'Masthead', href: '/components/masthead' },
|
||||
{ label: 'Article + Layer', href: '/components/article' },
|
||||
{ label: 'Rule 分隔线', href: '/components/rule' },
|
||||
],
|
||||
},
|
||||
{ label: '文本组件', href: '/text' },
|
||||
{ label: '媒体组件', href: '/components/media' },
|
||||
{ label: '主题与颜色', href: '/theme' },
|
||||
{
|
||||
title: '文本组件',
|
||||
href: '/text',
|
||||
},
|
||||
{
|
||||
title: '主题系统',
|
||||
href: '/theme',
|
||||
},
|
||||
{
|
||||
title: '示例',
|
||||
href: '/examples',
|
||||
items: [
|
||||
{ title: '跨栏布局', href: '/examples/spanning' },
|
||||
{ title: '响应式布局', href: '/examples/responsive' },
|
||||
label: '示例',
|
||||
href: '/examples/spanning',
|
||||
children: [
|
||||
{ label: '跨栏布局', href: '/examples/spanning' },
|
||||
{ label: '响应式', href: '/examples/responsive' },
|
||||
{ label: 'Blackletter 头版', href: '/examples/blackletter-frontpage' },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
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>
|
||||
<aside
|
||||
style={{
|
||||
width: '240px',
|
||||
flexShrink: 0,
|
||||
padding: '2rem 1rem',
|
||||
borderRight: '1px solid var(--nui-rule-hairline)',
|
||||
background: 'var(--nui-bg-surface)',
|
||||
fontFamily: 'var(--font-family-meta)',
|
||||
fontSize: '14px',
|
||||
position: 'sticky',
|
||||
top: 0,
|
||||
alignSelf: 'flex-start',
|
||||
maxHeight: '100vh',
|
||||
overflowY: 'auto',
|
||||
}}
|
||||
>
|
||||
<Link
|
||||
href="/"
|
||||
style={{
|
||||
display: 'block',
|
||||
fontFamily: 'var(--font-family-masthead)',
|
||||
fontSize: '22px',
|
||||
fontWeight: 700,
|
||||
color: 'var(--nui-text-primary)',
|
||||
textDecoration: 'none',
|
||||
marginBottom: '0.25rem',
|
||||
letterSpacing: '0.02em',
|
||||
}}
|
||||
>
|
||||
NewspaperUI
|
||||
</Link>
|
||||
<div
|
||||
className="nui-small-caps"
|
||||
style={{
|
||||
fontSize: '11px',
|
||||
color: 'var(--nui-text-muted)',
|
||||
marginBottom: '2rem',
|
||||
letterSpacing: '0.08em',
|
||||
}}
|
||||
>
|
||||
Production Newspaper Components
|
||||
</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>
|
||||
<nav>
|
||||
<ul style={{ listStyle: 'none', padding: 0, margin: 0 }}>
|
||||
{nav.map((item) => (
|
||||
<li key={item.href} style={{ marginBottom: '0.75rem' }}>
|
||||
<Link
|
||||
href={item.href}
|
||||
className={cx(pathname === item.href && 'active')}
|
||||
style={{
|
||||
display: 'block',
|
||||
padding: '0.25rem 0',
|
||||
color:
|
||||
pathname === item.href
|
||||
? 'var(--nui-accent-primary)'
|
||||
: 'var(--nui-text-secondary)',
|
||||
textDecoration: 'none',
|
||||
fontWeight: pathname === item.href ? 600 : 400,
|
||||
}}
|
||||
>
|
||||
{item.label}
|
||||
</Link>
|
||||
{item.children && (
|
||||
<ul style={{ listStyle: 'none', padding: '0 0 0 1rem', margin: '0.25rem 0 0' }}>
|
||||
{item.children.map((child) => (
|
||||
<li key={child.href} style={{ marginBottom: '0.25rem' }}>
|
||||
<Link
|
||||
href={child.href}
|
||||
style={{
|
||||
display: 'block',
|
||||
padding: '0.15rem 0',
|
||||
fontSize: '13px',
|
||||
color:
|
||||
pathname === child.href
|
||||
? 'var(--nui-accent-primary)'
|
||||
: 'var(--nui-text-muted)',
|
||||
textDecoration: 'none',
|
||||
fontWeight: pathname === child.href ? 600 : 400,
|
||||
}}
|
||||
>
|
||||
{child.label}
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</nav>
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
"require": "./dist/index.cjs",
|
||||
"types": "./dist/index.d.ts"
|
||||
},
|
||||
"./dist/style.css": "./dist/style.css",
|
||||
"./variables.css": "./src/variables.css",
|
||||
"./tailwind.config": "./src/tailwind.config.js"
|
||||
},
|
||||
|
||||
@@ -1,21 +1,6 @@
|
||||
/**
|
||||
* @newspaperui/theme
|
||||
* Theme tokens and CSS variables for newspaperui
|
||||
*/
|
||||
|
||||
// Import CSS variables
|
||||
import './fonts.css';
|
||||
import './variables.css';
|
||||
import './typography.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';
|
||||
export { visualWeights, resolveFontSize, resolveSpan } from './visual-weights';
|
||||
export type { VisualWeight, ComponentType, VisualWeightConfig } from './visual-weights';
|
||||
|
||||
@@ -1,103 +1,31 @@
|
||||
/**
|
||||
* Tailwind CSS Configuration for NewspaperUI
|
||||
* Extends Tailwind with newspaper-specific utilities and 24-column grid
|
||||
*/
|
||||
|
||||
const config = {
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
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)',
|
||||
masthead: ['var(--font-family-masthead)'],
|
||||
blackletter: ['var(--font-family-blackletter)'],
|
||||
display: ['var(--font-family-display)'],
|
||||
headline: ['var(--font-family-headline)'],
|
||||
body: ['var(--font-family-body)'],
|
||||
meta: ['var(--font-family-meta)'],
|
||||
},
|
||||
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',
|
||||
colors: {
|
||||
page: 'var(--nui-bg-page)',
|
||||
surface: 'var(--nui-bg-surface)',
|
||||
primary: 'var(--nui-text-primary)',
|
||||
body: 'var(--nui-text-body)',
|
||||
secondary: 'var(--nui-text-secondary)',
|
||||
muted: 'var(--nui-text-muted)',
|
||||
quote: 'var(--nui-text-quote)',
|
||||
hairline: 'var(--nui-rule-hairline)',
|
||||
decorative: 'var(--nui-rule-decorative)',
|
||||
accent: {
|
||||
primary: 'var(--nui-accent-primary)',
|
||||
'ink-blue': 'var(--nui-accent-ink-blue)',
|
||||
},
|
||||
highlight: 'var(--nui-highlight)',
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
};
|
||||
|
||||
export default config;
|
||||
|
||||
@@ -1,127 +1,55 @@
|
||||
/**
|
||||
* 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;
|
||||
/* font families */
|
||||
--font-family-masthead: "Cormorant Garamond", "Playfair Display", Georgia, serif;
|
||||
--font-family-blackletter: "UnifrakturMaguntia", "Cormorant Garamond", serif;
|
||||
--font-family-display: "Source Serif 4", Georgia, "Times New Roman", serif;
|
||||
--font-family-headline: "Source Serif 4", Georgia, "Times New Roman", serif;
|
||||
--font-family-body: "Source Serif 4", Georgia, "Times New Roman", serif;
|
||||
--font-family-meta: "Inter", system-ui, sans-serif;
|
||||
|
||||
/* 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;
|
||||
/* page background */
|
||||
--nui-bg-page: #F7F4ED;
|
||||
--nui-bg-surface: #FBF9F4;
|
||||
|
||||
/* Accent colors */
|
||||
--nui-color-accent-primary: #0066cc;
|
||||
--nui-color-accent-secondary: #cc0000;
|
||||
--nui-color-accent-tertiary: #006600;
|
||||
/* text */
|
||||
--nui-text-primary: #1A1A1A;
|
||||
--nui-text-body: #22201C;
|
||||
--nui-text-secondary: #4A4742;
|
||||
--nui-text-muted: #6E6A63;
|
||||
--nui-text-quote: #2E2A24;
|
||||
|
||||
/* ========== Spacing System ========== */
|
||||
/* Gutter (column gap) */
|
||||
--nui-gutter: 1rem;
|
||||
--nui-gutter-sm: 0.75rem;
|
||||
--nui-gutter-lg: 1.5rem;
|
||||
/* rules */
|
||||
--nui-rule-hairline: #C9C2B2;
|
||||
--nui-rule-decorative: #1A1A1A;
|
||||
|
||||
/* 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;
|
||||
/* accents */
|
||||
--nui-accent-primary: #7A1F1F;
|
||||
--nui-accent-ink-blue: #1B2A4A;
|
||||
--nui-highlight: #F2E9C8;
|
||||
|
||||
/* 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;
|
||||
/* spacing */
|
||||
--nui-gutter: 1.5rem;
|
||||
--nui-space-1: 0.25rem;
|
||||
--nui-space-2: 0.5rem;
|
||||
--nui-space-3: 0.75rem;
|
||||
--nui-space-4: 1rem;
|
||||
--nui-space-6: 1.5rem;
|
||||
--nui-space-8: 2rem;
|
||||
}
|
||||
|
||||
[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;
|
||||
--nui-bg-page: #14110D;
|
||||
--nui-bg-surface: #1C1812;
|
||||
--nui-text-primary: #EDE6D6;
|
||||
--nui-text-body: #DCD4C2;
|
||||
--nui-text-secondary: #A89F8C;
|
||||
--nui-text-muted: #857C6A;
|
||||
--nui-text-quote: #DCD4C2;
|
||||
--nui-rule-hairline: #3A352C;
|
||||
--nui-rule-decorative: #EDE6D6;
|
||||
--nui-accent-primary: #C97A6E;
|
||||
--nui-accent-ink-blue: #7E94C2;
|
||||
--nui-highlight: #3A2F18;
|
||||
}
|
||||
|
||||
html, body { background: var(--nui-bg-page); color: var(--nui-text-body); }
|
||||
|
||||
@@ -1,164 +1,247 @@
|
||||
/**
|
||||
* Resolves a fontSize value to a single CSS string.
|
||||
* For range tuples [min, max], returns the lower bound (min).
|
||||
* Visual weight mapping for newspaper components.
|
||||
* Reference: design.md §5 + classic-style design audit revision.
|
||||
*
|
||||
* fontFamily / color use CSS variable NAMES (e.g. '--font-family-headline').
|
||||
* Components do `var(${config.fontFamily})` at usage site.
|
||||
*/
|
||||
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 =
|
||||
| 'Masthead'
|
||||
| 'Headline'
|
||||
| 'Subhead'
|
||||
| 'Kicker'
|
||||
| 'BodyText'
|
||||
| 'Quote'
|
||||
| 'PullQuote'
|
||||
| 'Byline'
|
||||
| 'Dateline'
|
||||
| 'Caption';
|
||||
|
||||
export interface VisualWeightConfig {
|
||||
fontFamily: string; // CSS variable name, e.g. '--font-family-headline'
|
||||
fontSize: string | [string, string];
|
||||
fontWeight: number;
|
||||
lineHeight: number;
|
||||
color: string;
|
||||
letterSpacing?: string;
|
||||
fontStyle?: 'normal' | 'italic';
|
||||
fontVariant?: string; // 'small-caps' | 'oldstyle-nums proportional-nums' …
|
||||
color: string; // CSS variable name, e.g. '--nui-text-primary'
|
||||
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>>
|
||||
> = {
|
||||
export const visualWeights: Record<ComponentType, Partial<Record<VisualWeight, VisualWeightConfig>>> = {
|
||||
Masthead: {
|
||||
Standard: {
|
||||
fontFamily: '--font-family-masthead',
|
||||
fontSize: ['56px', '96px'],
|
||||
fontWeight: 700,
|
||||
lineHeight: 1.0,
|
||||
letterSpacing: '0.02em',
|
||||
fontVariant: 'lining-nums',
|
||||
color: '--nui-text-primary',
|
||||
span: 24,
|
||||
margin: '0',
|
||||
},
|
||||
},
|
||||
Headline: {
|
||||
High: {
|
||||
fontSize: ['36px', '48px'],
|
||||
fontWeight: 700,
|
||||
lineHeight: 1.1,
|
||||
color: '#111',
|
||||
span: [6, 8],
|
||||
fontFamily: '--font-family-display',
|
||||
fontSize: ['48px', '72px'],
|
||||
fontWeight: 600,
|
||||
lineHeight: 1.05,
|
||||
letterSpacing: '-0.01em',
|
||||
fontVariant: 'lining-nums',
|
||||
color: '--nui-text-primary',
|
||||
span: [8, 16],
|
||||
margin: '0 0 1rem 0',
|
||||
},
|
||||
Medium: {
|
||||
fontSize: ['28px', '34px'],
|
||||
fontFamily: '--font-family-headline',
|
||||
fontSize: ['32px', '40px'],
|
||||
fontWeight: 600,
|
||||
lineHeight: 1.2,
|
||||
color: '#111',
|
||||
span: [4, 6],
|
||||
lineHeight: 1.1,
|
||||
letterSpacing: '-0.005em',
|
||||
fontVariant: 'lining-nums',
|
||||
color: '--nui-text-primary',
|
||||
span: [6, 10],
|
||||
margin: '0 0 0.75rem 0',
|
||||
},
|
||||
Low: {
|
||||
fontFamily: '--font-family-headline',
|
||||
fontSize: ['22px', '26px'],
|
||||
fontWeight: 500,
|
||||
lineHeight: 1.3,
|
||||
color: '#222',
|
||||
span: [2, 4],
|
||||
lineHeight: 1.2,
|
||||
fontVariant: 'lining-nums',
|
||||
color: '--nui-text-body',
|
||||
span: [4, 6],
|
||||
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',
|
||||
fontFamily: '--font-family-headline',
|
||||
fontSize: ['18px', '22px'],
|
||||
fontWeight: 500,
|
||||
fontStyle: 'italic',
|
||||
lineHeight: 1.3,
|
||||
fontVariant: 'oldstyle-nums',
|
||||
color: '--nui-text-secondary',
|
||||
span: [4, 8],
|
||||
margin: '0 0 0.75rem 0',
|
||||
},
|
||||
Medium: {
|
||||
fontFamily: '--font-family-headline',
|
||||
fontSize: ['16px', '18px'],
|
||||
fontWeight: 500,
|
||||
lineHeight: 1.3,
|
||||
color: '#333',
|
||||
span: [1, 2],
|
||||
fontWeight: 400,
|
||||
fontStyle: 'italic',
|
||||
lineHeight: 1.35,
|
||||
fontVariant: 'oldstyle-nums',
|
||||
color: '--nui-text-secondary',
|
||||
span: [2, 4],
|
||||
margin: '0 0 0.5rem 0',
|
||||
},
|
||||
},
|
||||
Kicker: {
|
||||
Standard: {
|
||||
fontFamily: '--font-family-meta',
|
||||
fontSize: ['12px', '13px'],
|
||||
fontWeight: 600,
|
||||
lineHeight: 1.2,
|
||||
letterSpacing: '0.08em',
|
||||
fontVariant: 'small-caps',
|
||||
color: '--nui-accent-primary',
|
||||
span: [1, 4],
|
||||
margin: '0 0 0.25rem 0',
|
||||
},
|
||||
},
|
||||
BodyText: {
|
||||
High: {
|
||||
fontSize: '16px',
|
||||
fontFamily: '--font-family-body',
|
||||
fontSize: ['16px', '17px'],
|
||||
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',
|
||||
lineHeight: 1.6,
|
||||
fontVariant: 'oldstyle-nums',
|
||||
color: '--nui-text-body',
|
||||
span: 1,
|
||||
margin: '0 0 0.75rem 0',
|
||||
},
|
||||
Low: {
|
||||
fontSize: ['12px', '14px'],
|
||||
Medium: {
|
||||
fontFamily: '--font-family-body',
|
||||
fontSize: '15px',
|
||||
fontWeight: 400,
|
||||
lineHeight: 1.4,
|
||||
color: '#555',
|
||||
lineHeight: 1.55,
|
||||
fontVariant: 'oldstyle-nums',
|
||||
color: '--nui-text-body',
|
||||
span: 1,
|
||||
margin: '0 0 0.5rem 0',
|
||||
},
|
||||
Low: {
|
||||
fontFamily: '--font-family-body',
|
||||
fontSize: ['13px', '14px'],
|
||||
fontWeight: 400,
|
||||
lineHeight: 1.5,
|
||||
fontVariant: 'oldstyle-nums',
|
||||
color: '--nui-text-secondary',
|
||||
span: 1,
|
||||
margin: '0 0 0.5rem 0',
|
||||
},
|
||||
},
|
||||
Quote: {
|
||||
High: {
|
||||
fontSize: ['20px', '24px'],
|
||||
fontWeight: 500,
|
||||
lineHeight: 1.4,
|
||||
color: '#222',
|
||||
fontFamily: '--font-family-body',
|
||||
fontSize: ['17px', '19px'],
|
||||
fontWeight: 400,
|
||||
lineHeight: 1.55,
|
||||
fontVariant: 'oldstyle-nums',
|
||||
color: '--nui-text-quote',
|
||||
span: 2,
|
||||
margin: '0 0 0.75rem 0',
|
||||
margin: '1rem 0 1rem 1.5em',
|
||||
},
|
||||
Medium: {
|
||||
fontSize: ['16px', '18px'],
|
||||
fontFamily: '--font-family-body',
|
||||
fontSize: ['15px', '17px'],
|
||||
fontWeight: 400,
|
||||
lineHeight: 1.4,
|
||||
color: '#333',
|
||||
lineHeight: 1.5,
|
||||
fontVariant: 'oldstyle-nums',
|
||||
color: '--nui-text-quote',
|
||||
span: 1,
|
||||
margin: '0 0 0.5rem 0',
|
||||
margin: '0.75rem 0',
|
||||
},
|
||||
},
|
||||
PullQuote: {
|
||||
High: {
|
||||
fontSize: ['24px', '28px'],
|
||||
fontFamily: '--font-family-display',
|
||||
fontSize: ['26px', '32px'],
|
||||
fontWeight: 600,
|
||||
lineHeight: 1.2,
|
||||
color: '#111',
|
||||
letterSpacing: '-0.005em',
|
||||
fontVariant: 'lining-nums',
|
||||
color: '--nui-text-primary',
|
||||
span: [2, 3],
|
||||
margin: '0 0 0.5rem 0',
|
||||
margin: '1.5rem 0',
|
||||
},
|
||||
Medium: {
|
||||
fontSize: ['18px', '20px'],
|
||||
fontFamily: '--font-family-display',
|
||||
fontSize: ['20px', '24px'],
|
||||
fontWeight: 500,
|
||||
lineHeight: 1.25,
|
||||
color: '#222',
|
||||
fontVariant: 'lining-nums',
|
||||
color: '--nui-text-body',
|
||||
span: [1, 2],
|
||||
margin: '0 0 0.25rem 0',
|
||||
margin: '1rem 0',
|
||||
},
|
||||
},
|
||||
Byline: {
|
||||
Standard: {
|
||||
fontSize: ['12px', '14px'],
|
||||
fontWeight: 400,
|
||||
fontFamily: '--font-family-meta',
|
||||
fontSize: ['12px', '13px'],
|
||||
fontWeight: 500,
|
||||
lineHeight: 1.3,
|
||||
color: '#555',
|
||||
letterSpacing: '0.06em',
|
||||
fontVariant: 'small-caps',
|
||||
color: '--nui-text-secondary',
|
||||
span: 1,
|
||||
margin: '0 0 0.25rem 0',
|
||||
},
|
||||
},
|
||||
Dateline: {
|
||||
Standard: {
|
||||
fontFamily: '--font-family-meta',
|
||||
fontSize: ['12px', '13px'],
|
||||
fontWeight: 600,
|
||||
lineHeight: 1.3,
|
||||
letterSpacing: '0.08em',
|
||||
fontVariant: 'small-caps',
|
||||
color: '--nui-text-primary',
|
||||
span: 1,
|
||||
margin: '0',
|
||||
},
|
||||
},
|
||||
Caption: {
|
||||
Standard: {
|
||||
fontSize: ['12px', '14px'],
|
||||
fontFamily: '--font-family-body',
|
||||
fontSize: '13px',
|
||||
fontWeight: 400,
|
||||
lineHeight: 1.3,
|
||||
color: '#555',
|
||||
fontStyle: 'italic',
|
||||
lineHeight: 1.4,
|
||||
fontVariant: 'oldstyle-nums',
|
||||
color: '--nui-text-secondary',
|
||||
span: 1,
|
||||
margin: '0.25rem 0',
|
||||
margin: '0.5rem 0 0 0',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
/** Resolve fontSize: tuple → first value (lower bound used by default). */
|
||||
export function resolveFontSize(value: string | [string, string]): string {
|
||||
return Array.isArray(value) ? value[0] : value;
|
||||
}
|
||||
|
||||
/** Resolve span: tuple → first value (lower bound). */
|
||||
export function resolveSpan(value: number | [number, number]): number {
|
||||
return Array.isArray(value) ? value[0] : value;
|
||||
}
|
||||
|
||||
@@ -1,60 +1,45 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { calculateSpanWidth, validateSpan, calculateGutter } from '../grid';
|
||||
import { validateSpan, clampSpan } 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('returns true for valid integers in [1, max]', () => {
|
||||
expect(validateSpan(1, 24)).toBe(true);
|
||||
expect(validateSpan(12, 24)).toBe(true);
|
||||
expect(validateSpan(24, 24)).toBe(true);
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
it('returns false for out-of-range values', () => {
|
||||
expect(validateSpan(0, 24)).toBe(false);
|
||||
expect(validateSpan(-1, 24)).toBe(false);
|
||||
expect(validateSpan(25, 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();
|
||||
});
|
||||
it('returns false for non-integers', () => {
|
||||
expect(validateSpan(1.5, 24)).toBe(false);
|
||||
expect(validateSpan(NaN, 24)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('clampSpan', () => {
|
||||
it('returns the value when in range', () => {
|
||||
expect(clampSpan(8, 24)).toBe(8);
|
||||
});
|
||||
|
||||
it('clamps to max when over', () => {
|
||||
expect(clampSpan(30, 24)).toBe(24);
|
||||
});
|
||||
|
||||
it('clamps to 1 when under', () => {
|
||||
expect(clampSpan(0, 24)).toBe(1);
|
||||
expect(clampSpan(-5, 24)).toBe(1);
|
||||
});
|
||||
|
||||
it('floors fractional input', () => {
|
||||
expect(clampSpan(3.9, 24)).toBe(3);
|
||||
});
|
||||
|
||||
it('returns 1 for non-finite input', () => {
|
||||
expect(clampSpan(NaN, 24)).toBe(1);
|
||||
expect(clampSpan(Infinity, 24)).toBe(24);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
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%');
|
||||
});
|
||||
});
|
||||
@@ -1,90 +0,0 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,74 +0,0 @@
|
||||
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();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,61 +1,12 @@
|
||||
/**
|
||||
* 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 span is within [1, max]. */
|
||||
export function validateSpan(span: number, max: number): boolean {
|
||||
return Number.isInteger(span) && span >= 1 && span <= max;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
/** Clamp span into [1, max]. Used by Section/Article to recover from invalid input. */
|
||||
export function clampSpan(span: number, max: number): number {
|
||||
if (Number.isNaN(span)) return 1;
|
||||
if (span < 1) return 1;
|
||||
if (span > max) return max;
|
||||
return Math.floor(span);
|
||||
}
|
||||
|
||||
@@ -1,30 +1,2 @@
|
||||
/**
|
||||
* @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';
|
||||
export { validateSpan, clampSpan } from './grid';
|
||||
export { cx } from './cx';
|
||||
|
||||
@@ -1,85 +0,0 @@
|
||||
/**
|
||||
* 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';
|
||||
}
|
||||
}
|
||||
@@ -1,92 +0,0 @@
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
Reference in New Issue
Block a user