refact: update ui

This commit is contained in:
2026-03-25 14:06:37 +08:00
parent 276160d769
commit d13cb64567
28 changed files with 2451 additions and 250 deletions

View File

@@ -0,0 +1,177 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS

View File

@@ -0,0 +1,42 @@
---
name: frontend-design
description: Create distinctive, production-grade frontend interfaces with high design quality. Use this skill when the user asks to build web components, pages, artifacts, posters, or applications (examples include websites, landing pages, dashboards, React components, HTML/CSS layouts, or when styling/beautifying any web UI). Generates creative, polished code and UI design that avoids generic AI aesthetics.
license: Complete terms in LICENSE.txt
---
This skill guides creation of distinctive, production-grade frontend interfaces that avoid generic "AI slop" aesthetics. Implement real working code with exceptional attention to aesthetic details and creative choices.
The user provides frontend requirements: a component, page, application, or interface to build. They may include context about the purpose, audience, or technical constraints.
## Design Thinking
Before coding, understand the context and commit to a BOLD aesthetic direction:
- **Purpose**: What problem does this interface solve? Who uses it?
- **Tone**: Pick an extreme: brutally minimal, maximalist chaos, retro-futuristic, organic/natural, luxury/refined, playful/toy-like, editorial/magazine, brutalist/raw, art deco/geometric, soft/pastel, industrial/utilitarian, etc. There are so many flavors to choose from. Use these for inspiration but design one that is true to the aesthetic direction.
- **Constraints**: Technical requirements (framework, performance, accessibility).
- **Differentiation**: What makes this UNFORGETTABLE? What's the one thing someone will remember?
**CRITICAL**: Choose a clear conceptual direction and execute it with precision. Bold maximalism and refined minimalism both work - the key is intentionality, not intensity.
Then implement working code (HTML/CSS/JS, React, Vue, etc.) that is:
- Production-grade and functional
- Visually striking and memorable
- Cohesive with a clear aesthetic point-of-view
- Meticulously refined in every detail
## Frontend Aesthetics Guidelines
Focus on:
- **Typography**: Choose fonts that are beautiful, unique, and interesting. Avoid generic fonts like Arial and Inter; opt instead for distinctive choices that elevate the frontend's aesthetics; unexpected, characterful font choices. Pair a distinctive display font with a refined body font.
- **Color & Theme**: Commit to a cohesive aesthetic. Use CSS variables for consistency. Dominant colors with sharp accents outperform timid, evenly-distributed palettes.
- **Motion**: Use animations for effects and micro-interactions. Prioritize CSS-only solutions for HTML. Use Motion library for React when available. Focus on high-impact moments: one well-orchestrated page load with staggered reveals (animation-delay) creates more delight than scattered micro-interactions. Use scroll-triggering and hover states that surprise.
- **Spatial Composition**: Unexpected layouts. Asymmetry. Overlap. Diagonal flow. Grid-breaking elements. Generous negative space OR controlled density.
- **Backgrounds & Visual Details**: Create atmosphere and depth rather than defaulting to solid colors. Add contextual effects and textures that match the overall aesthetic. Apply creative forms like gradient meshes, noise textures, geometric patterns, layered transparencies, dramatic shadows, decorative borders, custom cursors, and grain overlays.
NEVER use generic AI-generated aesthetics like overused font families (Inter, Roboto, Arial, system fonts), cliched color schemes (particularly purple gradients on white backgrounds), predictable layouts and component patterns, and cookie-cutter design that lacks context-specific character.
Interpret creatively and make unexpected choices that feel genuinely designed for the context. No design should be the same. Vary between light and dark themes, different fonts, different aesthetics. NEVER converge on common choices (Space Grotesk, for example) across generations.
**IMPORTANT**: Match implementation complexity to the aesthetic vision. Maximalist designs need elaborate code with extensive animations and effects. Minimalist or refined designs need restraint, precision, and careful attention to spacing, typography, and subtle details. Elegance comes from executing the vision well.
Remember: Claude is capable of extraordinary creative work. Don't hold back, show what can truly be created when thinking outside the box and committing fully to a distinctive vision.

View File

@@ -0,0 +1,57 @@
---
title: "LaTeX vs MathML: Which Format Should You Use?"
description: A practical comparison of LaTeX and MathML for students and researchers
slug: latex-vs-mathml
date: 2026-03-15
tags: [guide, formats]
---
# LaTeX vs MathML: Which Format Should You Use?
TexPixel can export your recognized formulas in both LaTeX and MathML. But which one should you choose? Here's a quick guide.
## LaTeX — The Academic Standard
LaTeX is the most widely used format for typesetting math in academic papers, theses, and textbooks.
**Best for:**
- Writing papers in Overleaf, TeXmaker, or any LaTeX editor
- Pasting into Markdown documents (with KaTeX or MathJax rendering)
- Sharing formulas in forums like Stack Exchange or Reddit
**Example:**
```latex
\int_{0}^{\infty} e^{-x^2} dx = \frac{\sqrt{\pi}}{2}
```
## MathML — The Web Standard
MathML is an XML-based format designed for displaying math in web browsers and screen readers.
**Best for:**
- Embedding formulas in HTML web pages
- Accessibility — screen readers can interpret MathML
- Word documents (DOCX uses MathML internally)
**Example:**
```xml
<math>
<msubsup><mo>&int;</mo><mn>0</mn><mi>&infin;</mi></msubsup>
<msup><mi>e</mi><mrow><mo>-</mo><msup><mi>x</mi><mn>2</mn></msup></mrow></msup>
<mi>d</mi><mi>x</mi>
</math>
```
## Quick Decision Guide
| Scenario | Use |
|----------|-----|
| Writing a paper | LaTeX |
| Homework in Google Docs / Word | MathML (via DOCX export) |
| Posting on a blog or website | LaTeX (with MathJax) |
| Accessibility-focused content | MathML |
| Sharing on social media | Image export |
## The TexPixel Advantage
You don't have to choose upfront. TexPixel recognizes your formula once and lets you export in any format — switch freely between LaTeX, MathML, Markdown, Word, and image.

View File

@@ -0,0 +1,45 @@
---
title: 5 Tips for Better Handwriting Recognition
description: Get the most accurate results from TexPixel when scanning handwritten formulas
slug: handwriting-tips
date: 2026-03-20
tags: [tutorial, tips]
---
# 5 Tips for Better Handwriting Recognition
Getting clean, accurate LaTeX from handwritten math doesn't require perfect penmanship. But a few simple habits can dramatically improve your results.
## 1. Use Dark Ink on Light Paper
High contrast is the single biggest factor in recognition accuracy. A dark pen (black or dark blue) on white or light paper gives TexPixel the clearest signal. Pencil works too, but press firmly.
## 2. Give Symbols Room to Breathe
Cramped formulas are harder for both humans and AI to read. Leave clear gaps between:
- Fraction bars and the expressions above/below them
- Subscripts and superscripts and their base symbols
- Parentheses and the terms they enclose
## 3. Be Deliberate with Similar Characters
Some characters are notoriously ambiguous in handwriting:
- **1, l, |** — make your ones with a serif or flag
- **0, O, o** — zeros should be narrower and more oval
- **x, ×** — use a clear multiplication dot (·) when you mean "times"
- **u, v** — round bottom vs. pointed bottom
## 4. Keep Your Camera Steady
If you're photographing notes with a phone:
- Hold the phone parallel to the paper (not at an angle)
- Make sure the lighting is even — no harsh shadows across the formula
- Get close enough that the formula fills most of the frame
## 5. One Formula Per Upload
TexPixel works best when each image contains a single formula or a closely related set of expressions. If you have a page full of equations, crop them individually for best results.
---
With these habits, you'll see noticeably better accuracy — often 95%+ even for complex handwritten expressions.

View File

@@ -0,0 +1,48 @@
---
title: "LaTeX 和 MathML你应该用哪种格式"
description: 面向学生和研究人员的 LaTeX 与 MathML 实用对比
slug: latex-vs-mathml
date: 2026-03-15
tags: [指南, 格式]
---
# LaTeX 和 MathML你应该用哪种格式
TexPixel 可以将识别出的公式导出为 LaTeX 和 MathML 两种格式。那么你应该选择哪种?这里有一份快速指南。
## LaTeX — 学术标准
LaTeX 是学术论文、学位论文和教材中排版数学公式最广泛使用的格式。
**适用场景:**
- 在 Overleaf、TeXmaker 或任何 LaTeX 编辑器中写论文
- 粘贴到 Markdown 文档中(配合 KaTeX 或 MathJax 渲染)
- 在 Stack Exchange 或 Reddit 等论坛分享公式
**示例:**
```latex
\int_{0}^{\infty} e^{-x^2} dx = \frac{\sqrt{\pi}}{2}
```
## MathML — Web 标准
MathML 是一种基于 XML 的格式,专为在浏览器和屏幕阅读器中显示数学内容而设计。
**适用场景:**
- 在 HTML 网页中嵌入公式
- 无障碍访问 — 屏幕阅读器可以解读 MathML
- Word 文档DOCX 内部使用 MathML
## 快速决策指南
| 场景 | 推荐格式 |
|------|---------|
| 写论文 | LaTeX |
| 在 Google Docs / Word 中做作业 | MathML通过 DOCX 导出) |
| 发布博客或网站 | LaTeX配合 MathJax |
| 注重无障碍访问 | MathML |
| 社交媒体分享 | 图片导出 |
## TexPixel 的优势
你不需要提前选择。TexPixel 识别一次公式后,可以导出为任意格式——在 LaTeX、MathML、Markdown、Word 和图片之间自由切换。

View File

@@ -0,0 +1,45 @@
---
title: 提升手写公式识别准确率的 5 个技巧
description: 使用 TexPixel 扫描手写公式时,如何获得最准确的识别结果
slug: handwriting-tips
date: 2026-03-20
tags: [教程, 技巧]
---
# 提升手写公式识别准确率的 5 个技巧
从手写数学公式获得干净、准确的 LaTeX 并不需要完美的书写。但一些简单的习惯可以显著提升识别结果。
## 1. 使用深色墨水和浅色纸张
高对比度是影响识别准确率的最大因素。在白纸或浅色纸上使用深色笔(黑色或深蓝色)能给 TexPixel 最清晰的信号。铅笔也可以,但要用力书写。
## 2. 给符号留出空间
拥挤的公式无论对人还是 AI 都更难辨认。请在以下位置留出清晰的间隔:
- 分数线与上下表达式之间
- 上标、下标与基础符号之间
- 括号与其包含的项之间
## 3. 注意易混淆字符
一些字符在手写中特别容易混淆:
- **1, l, |** — 写数字 1 时加上衬线
- **0, O, o** — 零应更窄更椭圆
- **x, ×** — 表示"乘"时使用乘号点(·)
- **u, v** — 圆底 vs 尖底
## 4. 保持拍摄稳定
如果你用手机拍摄笔记:
- 手机保持与纸面平行(不要倾斜)
- 确保光线均匀,公式上没有阴影
- 靠近拍摄,让公式占据画面的大部分
## 5. 每次上传一个公式
TexPixel 在每张图片只包含一个公式或一组紧密相关的表达式时效果最好。如果你有一整页方程,建议逐个裁剪后分别上传。
---
养成这些习惯后,你会发现识别准确率明显提升——即使是复杂的手写表达式也能达到 95% 以上。

62
content/docs/en/faq.md Normal file
View File

@@ -0,0 +1,62 @@
---
title: FAQ
description: Frequently asked questions about TexPixel
slug: faq
date: 2026-03-25
tags: [reference]
order: 3
---
# Frequently Asked Questions
## General
### Is TexPixel free?
Yes! You can use TexPixel for free with up to 3 uploads per day. No sign-up or credit card is required. For unlimited uploads and additional features, check our Pro plan.
### Do I need to create an account?
No. You can start using TexPixel immediately as a guest. Creating an account (free) lets you sync history across devices.
### What languages are supported?
TexPixel recognizes mathematical formulas regardless of the surrounding language. The user interface is available in English and Chinese.
## Recognition
### How accurate is the recognition?
TexPixel achieves 90-98% accuracy depending on image quality and formula complexity. Clean, high-contrast images of typed formulas typically achieve the highest accuracy.
### Can it recognize handwritten formulas?
Yes. TexPixel handles both printed and handwritten formulas. For best results with handwriting, use dark ink on white paper and keep characters well-spaced.
### What about complex multi-line equations?
TexPixel can recognize multi-line equations, equation arrays, and systems of equations. Each line is captured and formatted correctly in the output.
### Does it support matrices and tables?
Yes. Matrices, determinants, and tabular expressions are supported and output as proper LaTeX `\begin{matrix}` or `\begin{pmatrix}` environments.
## Export & Integration
### Can I use the output in Overleaf?
Absolutely. Copy the LaTeX output and paste it directly into your Overleaf project. It works immediately.
### How do I use the output in Word?
Use the DOCX export option. This creates a Word file with properly formatted equations that you can edit normally in Microsoft Word or Google Docs.
## Privacy & Data
### Are my uploaded images stored?
Uploaded images are processed for recognition and temporarily cached. They are automatically deleted after processing. We do not use your images for training.
### Is my data encrypted?
Yes. All data is transmitted over HTTPS. Uploaded files and results are handled securely.

View File

@@ -0,0 +1,50 @@
---
title: Supported Formats
description: Input and output formats supported by TexPixel
slug: supported-formats
date: 2026-03-25
tags: [reference]
order: 2
---
# Supported Formats
## Input Formats
TexPixel accepts the following file types for formula recognition:
### Images
- **JPEG / JPG** — Photos from cameras or phones
- **PNG** — Screenshots, scanned images, or digital drawings
### Documents
- **PDF** — Single or multi-page documents. All pages are processed and formulas are extracted from each page.
**File size limit:** 10 MB per file.
**Resolution tips:** For best results, use images at least 300 DPI. Phone photos work well when the formula fills most of the frame.
## Output Formats
After recognition, you can export results in multiple formats:
### Code Formats
- **LaTeX** — Standard math typesetting syntax, compatible with Overleaf, TeXmaker, and Markdown editors
- **MathML** — XML-based format for web pages and screen readers
### Document Formats
- **Markdown** — Ready to paste into Markdown files with inline math notation
- **Word (DOCX)** — Editable Word document with properly formatted equations
### Image Formats
- **PNG** — High-resolution rendered formula image
## Format Comparison
| Format | Best For | Editable | Web-Ready |
|--------|----------|----------|-----------|
| LaTeX | Papers, Markdown | Yes | With MathJax |
| MathML | Websites, Accessibility | Yes | Native |
| Markdown | Notes, Docs | Yes | With renderer |
| Word | Assignments | Yes | No |
| PNG | Sharing | No | Yes |

62
content/docs/zh/faq.md Normal file
View File

@@ -0,0 +1,62 @@
---
title: 常见问题
description: 关于 TexPixel 的常见问题解答
slug: faq
date: 2026-03-25
tags: [参考]
order: 3
---
# 常见问题
## 基本信息
### TexPixel 是免费的吗?
是的!你可以每天免费使用 3 次上传。无需注册或绑定信用卡。如需无限上传和更多功能,请查看我们的专业版。
### 需要创建账号吗?
不需要。你可以立即以访客身份使用 TexPixel。创建免费账号后可以跨设备同步历史记录。
### 支持哪些语言?
TexPixel 可以识别任何语言环境中的数学公式。用户界面支持中文和英文。
## 识别相关
### 识别准确率如何?
根据图片质量和公式复杂度TexPixel 的准确率在 90-98% 之间。清晰、高对比度的印刷体公式通常能达到最高准确率。
### 能识别手写公式吗?
可以。TexPixel 支持印刷体和手写公式。手写公式建议使用深色墨水在白纸上书写,并保持字符间距适当。
### 支持复杂的多行方程吗?
TexPixel 可以识别多行方程、方程组和方程数组。每一行都会被准确捕获并正确格式化输出。
### 支持矩阵和表格吗?
支持。矩阵、行列式和表格表达式都能识别,并输出为正确的 LaTeX `\begin{matrix}``\begin{pmatrix}` 环境。
## 导出与集成
### 输出可以在 Overleaf 中使用吗?
当然可以。复制 LaTeX 输出,直接粘贴到你的 Overleaf 项目中即可使用。
### 如何在 Word 中使用?
使用 DOCX 导出选项。这会创建一个包含格式正确的公式的 Word 文件,你可以在 Microsoft Word 或 Google Docs 中正常编辑。
## 隐私与数据
### 上传的图片会被存储吗?
上传的图片仅用于识别处理并临时缓存,处理完成后会自动删除。我们不会使用你的图片进行训练。
### 数据是加密的吗?
是的。所有数据通过 HTTPS 传输。上传的文件和结果都经过安全处理。

View File

@@ -0,0 +1,50 @@
---
title: 支持的格式
description: TexPixel 支持的输入和输出格式
slug: supported-formats
date: 2026-03-25
tags: [参考]
order: 2
---
# 支持的格式
## 输入格式
TexPixel 接受以下文件类型进行公式识别:
### 图片
- **JPEG / JPG** — 相机或手机拍摄的照片
- **PNG** — 截图、扫描图片或数字绘图
### 文档
- **PDF** — 单页或多页文档。所有页面都会被处理,每一页中的公式都会被提取。
**文件大小限制:** 每个文件最大 10 MB。
**分辨率建议:** 为获得最佳效果,建议使用至少 300 DPI 的图片。手机拍照时让公式占据画面的大部分即可。
## 输出格式
识别完成后,你可以将结果导出为多种格式:
### 代码格式
- **LaTeX** — 标准数学排版语法,兼容 Overleaf、TeXmaker 和 Markdown 编辑器
- **MathML** — 基于 XML 的格式,适用于网页和屏幕阅读器
### 文档格式
- **Markdown** — 可直接粘贴到 Markdown 文件中,包含内联数学符号
- **Word (DOCX)** — 可编辑的 Word 文档,公式格式完整
### 图片格式
- **PNG** — 高分辨率渲染的公式图片
## 格式对比
| 格式 | 最适合 | 可编辑 | 网页就绪 |
|------|--------|--------|---------|
| LaTeX | 论文、Markdown | 是 | 需 MathJax |
| MathML | 网站、无障碍 | 是 | 原生支持 |
| Markdown | 笔记、文档 | 是 | 需渲染器 |
| Word | 作业 | 是 | 否 |
| PNG | 分享 | 否 | 是 |

View File

@@ -0,0 +1,974 @@
# Website Restructure Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Restructure the single-page OCR app into a multi-section marketing website with dedicated workspace, docs, and blog pages.
**Architecture:** SPA with react-router-dom layout routes. MarketingLayout (Navbar + Footer) wraps Home/Docs/Blog. AppLayout wraps the OCR workspace at `/app`. Markdown content compiled at build time via a Vite plugin. SEO handled by react-helmet-async + vite-plugin-prerender.
**Tech Stack:** React 18, react-router-dom, Tailwind CSS, react-helmet-async, vite-plugin-prerender, remark/rehype (existing), gray-matter
---
## File Structure
```
src/
├── components/
│ ├── home/
│ │ ├── HeroSection.tsx — Hero with OCR demo + CTA
│ │ ├── FeaturesSection.tsx — Feature cards grid
│ │ ├── HowItWorksSection.tsx — 3-step flow
│ │ ├── PricingSection.tsx — Price cards
│ │ └── ContactSection.tsx — Contact info + form
│ ├── layout/
│ │ ├── MarketingNavbar.tsx — Full site nav
│ │ ├── AppNavbar.tsx — Workspace nav (from Navbar.tsx)
│ │ ├── Footer.tsx — Site footer
│ │ ├── MarketingLayout.tsx — MarketingNavbar + Outlet + Footer
│ │ └── AppLayout.tsx — AppNavbar + Outlet
│ └── seo/
│ └── SEOHead.tsx — react-helmet-async wrapper
├── pages/
│ ├── HomePage.tsx
│ ├── WorkspacePage.tsx — migrated from App.tsx
│ ├── DocsListPage.tsx
│ ├── DocDetailPage.tsx
│ ├── BlogListPage.tsx
│ └── BlogDetailPage.tsx
├── lib/
│ └── content.ts — Load markdown manifests
├── routes/
│ └── AppRouter.tsx — Updated with layout routes
content/
├── docs/
│ ├── en/getting-started.md
│ └── zh/getting-started.md
└── blog/
├── en/2026-03-25-introducing-texpixel.md
└── zh/2026-03-25-introducing-texpixel.md
scripts/
└── build-content.ts — Compile markdown to JSON
```
---
### Task 1: Install dependencies and setup
**Files:**
- Modify: `package.json`
- [ ] **Step 1: Install react-helmet-async and gray-matter**
```bash
npm install react-helmet-async gray-matter
```
- [ ] **Step 2: Wrap app with HelmetProvider**
In `src/main.tsx`, add `HelmetProvider` wrapping:
```tsx
import { HelmetProvider } from 'react-helmet-async';
// Wrap inside StrictMode:
<HelmetProvider>
<BrowserRouter>
<AuthProvider>
<LanguageProvider>
<AppRouter />
</LanguageProvider>
</AuthProvider>
</BrowserRouter>
</HelmetProvider>
```
- [ ] **Step 3: Commit**
```bash
git add package.json package-lock.json src/main.tsx
git commit -m "feat: install react-helmet-async and gray-matter, add HelmetProvider"
```
---
### Task 2: Create SEOHead component
**Files:**
- Create: `src/components/seo/SEOHead.tsx`
- [ ] **Step 1: Create SEOHead component**
```tsx
import { Helmet } from 'react-helmet-async';
interface SEOHeadProps {
title: string;
description: string;
path: string;
type?: 'website' | 'article';
image?: string;
publishedTime?: string;
noindex?: boolean;
}
const BASE_URL = 'https://texpixel.com';
export default function SEOHead({
title,
description,
path,
type = 'website',
image = 'https://cdn.texpixel.com/public/og-cover.png',
publishedTime,
noindex = false,
}: SEOHeadProps) {
const url = `${BASE_URL}${path}`;
const fullTitle = path === '/' ? title : `${title} | TexPixel`;
return (
<Helmet>
<title>{fullTitle}</title>
<meta name="description" content={description} />
<link rel="canonical" href={url} />
{noindex && <meta name="robots" content="noindex, nofollow" />}
{/* Open Graph */}
<meta property="og:title" content={fullTitle} />
<meta property="og:description" content={description} />
<meta property="og:url" content={url} />
<meta property="og:type" content={type} />
<meta property="og:image" content={image} />
<meta property="og:site_name" content="TexPixel" />
{publishedTime && <meta property="article:published_time" content={publishedTime} />}
{/* Twitter */}
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content={fullTitle} />
<meta name="twitter:description" content={description} />
<meta name="twitter:image" content={image} />
</Helmet>
);
}
```
- [ ] **Step 2: Commit**
```bash
git add src/components/seo/SEOHead.tsx
git commit -m "feat: add SEOHead component with react-helmet-async"
```
---
### Task 3: Create layout components
**Files:**
- Create: `src/components/layout/MarketingNavbar.tsx`
- Create: `src/components/layout/AppNavbar.tsx`
- Create: `src/components/layout/Footer.tsx`
- Create: `src/components/layout/MarketingLayout.tsx`
- Create: `src/components/layout/AppLayout.tsx`
- [ ] **Step 1: Create MarketingNavbar**
Full-width navbar with logo, nav links (Home, Docs, Blog), anchor links (Pricing, Contact on home page), language switcher, and CTA button to `/app`. Use `useLocation` to show anchor links only when on `/`. Responsive with mobile hamburger menu.
```tsx
// src/components/layout/MarketingNavbar.tsx
import { useState } from 'react';
import { Link, useLocation } from 'react-router-dom';
import { Languages, ChevronDown, Check, Menu, X } from 'lucide-react';
import { useLanguage } from '../../contexts/LanguageContext';
export default function MarketingNavbar() {
const { language, setLanguage, t } = useLanguage();
const location = useLocation();
const [showLangMenu, setShowLangMenu] = useState(false);
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
const isHome = location.pathname === '/';
const navLinks = [
{ to: '/', label: t.marketing?.nav?.home ?? 'Home' },
{ to: '/docs', label: t.marketing?.nav?.docs ?? 'Docs' },
{ to: '/blog', label: t.marketing?.nav?.blog ?? 'Blog' },
];
const anchorLinks = isHome
? [
{ href: '#pricing', label: t.marketing?.nav?.pricing ?? 'Pricing' },
{ href: '#contact', label: t.marketing?.nav?.contact ?? 'Contact' },
]
: [];
return (
<nav className="h-16 bg-white border-b border-gray-200 flex items-center justify-between px-6 flex-shrink-0 z-[60] relative">
{/* Logo */}
<Link to="/" className="flex items-center gap-2">
<img src="/texpixel-app-icon.svg" alt="TexPixel" className="w-8 h-8" />
<span className="text-xl font-bold text-gray-900 tracking-tight">TexPixel</span>
</Link>
{/* Desktop Nav */}
<div className="hidden md:flex items-center gap-6">
{navLinks.map((link) => (
<Link
key={link.to}
to={link.to}
className={`text-sm font-medium transition-colors ${
location.pathname === link.to ? 'text-blue-600' : 'text-gray-700 hover:text-gray-900'
}`}
>
{link.label}
</Link>
))}
{anchorLinks.map((link) => (
<a
key={link.href}
href={link.href}
className="text-sm font-medium text-gray-700 hover:text-gray-900 transition-colors"
>
{link.label}
</a>
))}
</div>
{/* Right actions */}
<div className="flex items-center gap-3">
{/* Language Switcher */}
<div className="relative">
<button
onClick={() => setShowLangMenu(!showLangMenu)}
className="flex items-center gap-2 px-3 py-2 hover:bg-gray-100 rounded-lg text-gray-700 text-sm font-medium transition-colors"
>
<Languages size={18} />
<span className="hidden sm:inline">{language === 'en' ? 'EN' : '中文'}</span>
<ChevronDown size={14} className={`transition-transform duration-200 ${showLangMenu ? 'rotate-180' : ''}`} />
</button>
{showLangMenu && (
<div className="absolute right-0 top-full mt-2 w-32 bg-white rounded-xl shadow-lg border border-gray-200 py-1 z-50">
{(['en', 'zh'] as const).map((lang) => (
<button
key={lang}
onClick={() => { setLanguage(lang); setShowLangMenu(false); }}
className={`w-full flex items-center justify-between px-4 py-2 text-sm transition-colors hover:bg-gray-50 ${language === lang ? 'text-blue-600 font-medium' : 'text-gray-700'}`}
>
{lang === 'en' ? 'English' : '简体中文'}
{language === lang && <Check size={14} />}
</button>
))}
</div>
)}
</div>
{/* CTA */}
<Link
to="/app"
className="hidden sm:inline-flex items-center px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white text-sm font-medium rounded-lg transition-colors"
>
{t.marketing?.nav?.launchApp ?? 'Launch App'}
</Link>
{/* Mobile menu toggle */}
<button className="md:hidden p-2" onClick={() => setMobileMenuOpen(!mobileMenuOpen)}>
{mobileMenuOpen ? <X size={20} /> : <Menu size={20} />}
</button>
</div>
{/* Mobile menu */}
{mobileMenuOpen && (
<div className="absolute top-16 left-0 right-0 bg-white border-b border-gray-200 shadow-lg md:hidden z-50 py-4 px-6 flex flex-col gap-3">
{navLinks.map((link) => (
<Link key={link.to} to={link.to} className="text-sm font-medium text-gray-700 py-2" onClick={() => setMobileMenuOpen(false)}>
{link.label}
</Link>
))}
{anchorLinks.map((link) => (
<a key={link.href} href={link.href} className="text-sm font-medium text-gray-700 py-2" onClick={() => setMobileMenuOpen(false)}>
{link.label}
</a>
))}
<Link to="/app" className="text-sm font-medium text-blue-600 py-2" onClick={() => setMobileMenuOpen(false)}>
{t.marketing?.nav?.launchApp ?? 'Launch App'}
</Link>
</div>
)}
</nav>
);
}
```
- [ ] **Step 2: Create AppNavbar**
Simplified version of current `Navbar.tsx` for the workspace. Keep language switcher, reward, contact, guide, help — remove marketing nav links. Add a "Back to Home" link.
Copy current `src/components/Navbar.tsx` content into `src/components/layout/AppNavbar.tsx`. Add a `Link` to `/` (home icon or "TexPixel" logo links to `/`). Keep all existing functionality (reward modal, contact dropdown, language switcher, guide button).
- [ ] **Step 3: Create Footer**
```tsx
// src/components/layout/Footer.tsx
import { Link } from 'react-router-dom';
import { useLanguage } from '../../contexts/LanguageContext';
export default function Footer() {
const { t } = useLanguage();
return (
<footer className="bg-gray-900 text-gray-400 py-12 px-6">
<div className="max-w-6xl mx-auto grid grid-cols-1 md:grid-cols-4 gap-8">
{/* Brand */}
<div>
<div className="flex items-center gap-2 mb-4">
<img src="/texpixel-app-icon.svg" alt="TexPixel" className="w-6 h-6" />
<span className="text-white font-bold">TexPixel</span>
</div>
<p className="text-sm">{t.marketing?.footer?.tagline ?? 'AI-powered math formula recognition'}</p>
</div>
{/* Product */}
<div>
<h4 className="text-white font-semibold mb-3 text-sm">{t.marketing?.footer?.product ?? 'Product'}</h4>
<div className="flex flex-col gap-2 text-sm">
<Link to="/app" className="hover:text-white transition-colors">{t.marketing?.nav?.launchApp ?? 'Launch App'}</Link>
<a href="/#pricing" className="hover:text-white transition-colors">{t.marketing?.nav?.pricing ?? 'Pricing'}</a>
</div>
</div>
{/* Resources */}
<div>
<h4 className="text-white font-semibold mb-3 text-sm">{t.marketing?.footer?.resources ?? 'Resources'}</h4>
<div className="flex flex-col gap-2 text-sm">
<Link to="/docs" className="hover:text-white transition-colors">{t.marketing?.nav?.docs ?? 'Docs'}</Link>
<Link to="/blog" className="hover:text-white transition-colors">{t.marketing?.nav?.blog ?? 'Blog'}</Link>
</div>
</div>
{/* Contact */}
<div>
<h4 className="text-white font-semibold mb-3 text-sm">{t.marketing?.footer?.contactTitle ?? 'Contact'}</h4>
<div className="flex flex-col gap-2 text-sm">
<a href="mailto:yogecoder@gmail.com" className="hover:text-white transition-colors">yogecoder@gmail.com</a>
</div>
</div>
</div>
<div className="max-w-6xl mx-auto mt-8 pt-8 border-t border-gray-800 text-sm text-center">
&copy; {new Date().getFullYear()} TexPixel. All rights reserved.
</div>
</footer>
);
}
```
- [ ] **Step 4: Create MarketingLayout and AppLayout**
```tsx
// src/components/layout/MarketingLayout.tsx
import { Outlet } from 'react-router-dom';
import MarketingNavbar from './MarketingNavbar';
import Footer from './Footer';
export default function MarketingLayout() {
return (
<div className="min-h-screen flex flex-col bg-white">
<MarketingNavbar />
<main className="flex-1">
<Outlet />
</main>
<Footer />
</div>
);
}
```
```tsx
// src/components/layout/AppLayout.tsx
import { Outlet } from 'react-router-dom';
import AppNavbar from './AppNavbar';
export default function AppLayout() {
return (
<div className="h-screen flex flex-col bg-gray-50 font-sans text-gray-900 overflow-hidden">
<AppNavbar />
<div className="flex-1 flex overflow-hidden">
<Outlet />
</div>
</div>
);
}
```
- [ ] **Step 5: Commit**
```bash
git add src/components/layout/
git commit -m "feat: add layout components (MarketingNavbar, AppNavbar, Footer, layouts)"
```
---
### Task 4: Add marketing translations
**Files:**
- Modify: `src/lib/translations.ts`
- [ ] **Step 1: Add marketing section to translations**
Add `marketing` key to both `en` and `zh` objects in `translations.ts`:
```typescript
marketing: {
nav: {
home: 'Home', // zh: '首页'
docs: 'Docs', // zh: '文档'
blog: 'Blog', // zh: '博客'
pricing: 'Pricing', // zh: '价格'
contact: 'Contact', // zh: '联系我们'
launchApp: 'Launch App', // zh: '启动应用'
},
hero: {
title: 'Convert Math Formulas to LaTeX in Seconds',
// zh: '数学公式秒级转换为 LaTeX'
subtitle: 'AI-powered OCR for handwritten and printed mathematical formulas. Get LaTeX, MathML, and Markdown output instantly.',
// zh: 'AI 驱动的手写和印刷体数学公式识别,即时输出 LaTeX、MathML 和 Markdown。'
cta: 'Try It Free', // zh: '免费试用'
ctaSecondary: 'Learn More', // zh: '了解更多'
},
features: {
title: 'Features', // zh: '功能特性'
subtitle: 'Everything you need for formula recognition',
// zh: '公式识别所需的一切'
items: [
{ title: 'Handwriting Recognition', description: 'Accurately recognize handwritten math formulas from photos or scans' },
{ title: 'Multi-Format Output', description: 'Export to LaTeX, MathML, Markdown, Word, and more' },
{ title: 'PDF Support', description: 'Upload PDF documents and extract formulas automatically' },
{ title: 'Batch Processing', description: 'Process multiple files at once for maximum efficiency' },
{ title: 'High Accuracy', description: 'Powered by advanced AI models for industry-leading accuracy' },
{ title: 'Free to Start', description: 'Get started with free uploads, no credit card required' },
],
// zh versions of items array
},
howItWorks: {
title: 'How It Works', // zh: '使用流程'
steps: [
{ title: 'Upload', description: 'Upload an image or PDF containing math formulas' },
{ title: 'Recognize', description: 'Our AI analyzes and recognizes the formulas' },
{ title: 'Export', description: 'Copy or export results in your preferred format' },
],
},
pricing: {
title: 'Pricing', // zh: '价格方案'
subtitle: 'Choose the plan that fits your needs',
// zh: '选择适合您的方案'
plans: [
{ name: 'Free', price: '$0', period: '/month', features: ['3 uploads/day', 'LaTeX & Markdown output', 'Community support'], cta: 'Get Started' },
{ name: 'Pro', price: '$9.9', period: '/month', features: ['Unlimited uploads', 'All export formats', 'Priority processing', 'API access'], cta: 'Coming Soon', popular: true },
{ name: 'Enterprise', price: 'Custom', period: '', features: ['Custom volume', 'Dedicated support', 'SLA guarantee', 'On-premise option'], cta: 'Contact Us' },
],
},
contact: {
title: 'Contact Us', // zh: '联系我们'
subtitle: 'Get in touch with our team',
// zh: '与我们的团队取得联系'
nameLabel: 'Name', // zh: '姓名'
emailLabel: 'Email', // zh: '邮箱'
messageLabel: 'Message', // zh: '留言'
send: 'Send Message', // zh: '发送消息'
sending: 'Sending...', // zh: '发送中...'
sent: 'Message sent!', // zh: '消息已发送!'
qqGroup: 'QQ Group', // zh: 'QQ 群'
},
footer: {
tagline: 'AI-powered math formula recognition',
// zh: 'AI 驱动的数学公式识别'
product: 'Product', // zh: '产品'
resources: 'Resources', // zh: '资源'
contactTitle: 'Contact', // zh: '联系方式'
},
},
```
- [ ] **Step 2: Commit**
```bash
git add src/lib/translations.ts
git commit -m "feat: add marketing translations for en and zh"
```
---
### Task 5: Create Home page sections
**Files:**
- Create: `src/components/home/HeroSection.tsx`
- Create: `src/components/home/FeaturesSection.tsx`
- Create: `src/components/home/HowItWorksSection.tsx`
- Create: `src/components/home/PricingSection.tsx`
- Create: `src/components/home/ContactSection.tsx`
- Create: `src/pages/HomePage.tsx`
- [ ] **Step 1: Create HeroSection**
Hero with product tagline, a mini drag-and-drop demo area (visual only, clicking it navigates to `/app`), and CTA buttons. Use Tailwind for gradient backgrounds and animations.
Key elements:
- Large heading from `t.marketing.hero.title`
- Subtitle from `t.marketing.hero.subtitle`
- Primary CTA button → links to `/app`
- Secondary CTA button → scrolls to `#features`
- A decorative mock preview showing a formula being converted (static image or CSS illustration)
- [ ] **Step 2: Create FeaturesSection**
6-card grid from `t.marketing.features.items`. Each card has an icon (from lucide-react), title, and description. Use icons: `PenTool`, `FileOutput`, `FileText`, `Layers`, `Zap`, `Gift`.
- [ ] **Step 3: Create HowItWorksSection**
3-step horizontal flow with numbered circles, title, description. Steps from `t.marketing.howItWorks.steps`. Use icons: `Upload`, `Cpu`, `Download`.
- [ ] **Step 4: Create PricingSection**
3-column card layout from `t.marketing.pricing.plans`. Middle card (Pro) has `popular: true` → highlighted border/badge. CTA buttons: Free → link to `/app`, Pro → disabled "Coming Soon", Enterprise → link to `#contact`.
Section has `id="pricing"` for anchor navigation.
- [ ] **Step 5: Create ContactSection**
Two-column layout. Left: contact info (email, QQ group). Right: form with name, email, message fields + submit button. Form initially just shows a success toast on submit (no backend). `id="contact"` for anchor nav.
- [ ] **Step 6: Create HomePage**
```tsx
// src/pages/HomePage.tsx
import SEOHead from '../components/seo/SEOHead';
import HeroSection from '../components/home/HeroSection';
import FeaturesSection from '../components/home/FeaturesSection';
import HowItWorksSection from '../components/home/HowItWorksSection';
import PricingSection from '../components/home/PricingSection';
import ContactSection from '../components/home/ContactSection';
import { useLanguage } from '../contexts/LanguageContext';
export default function HomePage() {
const { t } = useLanguage();
return (
<>
<SEOHead
title="TexPixel - AI Math Formula Recognition | LaTeX, MathML OCR Tool"
description={t.marketing.hero.subtitle}
path="/"
/>
<HeroSection />
<FeaturesSection />
<HowItWorksSection />
<PricingSection />
<ContactSection />
</>
);
}
```
- [ ] **Step 7: Commit**
```bash
git add src/components/home/ src/pages/HomePage.tsx
git commit -m "feat: add Home page with Hero, Features, HowItWorks, Pricing, Contact sections"
```
---
### Task 6: Migrate App.tsx to WorkspacePage
**Files:**
- Create: `src/pages/WorkspacePage.tsx`
- Modify: `src/App.tsx` (will become thin redirect or removed)
- [ ] **Step 1: Create WorkspacePage**
Move all logic from `App.tsx` into `WorkspacePage.tsx`. Remove the outer `<div className="h-screen flex flex-col ...">` and `<Navbar />` wrappers since `AppLayout` provides those. Keep the inner flex container with LeftSidebar, FilePreview, ResultPanel, modals, and loading overlay.
The component should render:
```tsx
<>
<SEOHead title="Workspace" description="..." path="/app" noindex />
{/* Left Sidebar */}
<div ref={sidebarRef} ...>
<LeftSidebar ... />
{/* Resize Handle */}
</div>
{/* Middle: FilePreview */}
<div className="flex-1 ..."><FilePreview ... /></div>
{/* Right: ResultPanel */}
<div className="flex-1 ..."><ResultPanel ... /></div>
{/* Modals */}
{showUploadModal && <UploadModal ... />}
{showAuthModal && <AuthModal ... />}
<UserGuide ... />
{loading && <div>...</div>}
</>
```
Note: `AppLayout` already provides `<div className="h-screen flex flex-col ...">` and `<AppNavbar />` and `<div className="flex-1 flex overflow-hidden">`, so WorkspacePage renders directly inside that flex container.
- [ ] **Step 2: Update App.tsx**
Replace `App.tsx` with a simple redirect to maintain backward compatibility if anything imports it:
```tsx
import { Navigate } from 'react-router-dom';
export default function App() {
return <Navigate to="/" replace />;
}
```
- [ ] **Step 3: Verify build**
```bash
npm run typecheck
```
- [ ] **Step 4: Commit**
```bash
git add src/pages/WorkspacePage.tsx src/App.tsx
git commit -m "feat: migrate App.tsx logic to WorkspacePage"
```
---
### Task 7: Update AppRouter with layout routes
**Files:**
- Modify: `src/routes/AppRouter.tsx`
- [ ] **Step 1: Update AppRouter**
```tsx
import { lazy, Suspense } from 'react';
import { Routes, Route } from 'react-router-dom';
import MarketingLayout from '../components/layout/MarketingLayout';
import AppLayout from '../components/layout/AppLayout';
import AuthCallbackPage from '../pages/AuthCallbackPage';
const HomePage = lazy(() => import('../pages/HomePage'));
const WorkspacePage = lazy(() => import('../pages/WorkspacePage'));
const DocsListPage = lazy(() => import('../pages/DocsListPage'));
const DocDetailPage = lazy(() => import('../pages/DocDetailPage'));
const BlogListPage = lazy(() => import('../pages/BlogListPage'));
const BlogDetailPage = lazy(() => import('../pages/BlogDetailPage'));
function LoadingFallback() {
return (
<div className="min-h-screen flex items-center justify-center">
<div className="w-12 h-12 border-4 border-blue-600 border-t-transparent rounded-full animate-spin" />
</div>
);
}
export default function AppRouter() {
return (
<Suspense fallback={<LoadingFallback />}>
<Routes>
<Route element={<MarketingLayout />}>
<Route path="/" element={<HomePage />} />
<Route path="/docs" element={<DocsListPage />} />
<Route path="/docs/:slug" element={<DocDetailPage />} />
<Route path="/blog" element={<BlogListPage />} />
<Route path="/blog/:slug" element={<BlogDetailPage />} />
</Route>
<Route element={<AppLayout />}>
<Route path="/app" element={<WorkspacePage />} />
</Route>
<Route path="/auth/google/callback" element={<AuthCallbackPage />} />
</Routes>
</Suspense>
);
}
```
- [ ] **Step 2: Commit**
```bash
git add src/routes/AppRouter.tsx
git commit -m "feat: update AppRouter with layout routes and lazy loading"
```
---
### Task 8: Create placeholder Docs and Blog pages
**Files:**
- Create: `src/pages/DocsListPage.tsx`
- Create: `src/pages/DocDetailPage.tsx`
- Create: `src/pages/BlogListPage.tsx`
- Create: `src/pages/BlogDetailPage.tsx`
- [ ] **Step 1: Create DocsListPage**
List page showing available docs. For now, hardcode a few placeholder entries. Each entry links to `/docs/:slug`. Include SEOHead.
```tsx
import { Link } from 'react-router-dom';
import SEOHead from '../components/seo/SEOHead';
import { useLanguage } from '../contexts/LanguageContext';
const docs = [
{ slug: 'getting-started', title: 'Getting Started', titleZh: '快速开始', description: 'Learn how to use TexPixel', descriptionZh: '了解如何使用 TexPixel' },
];
export default function DocsListPage() {
const { language } = useLanguage();
return (
<>
<SEOHead title="Documentation" description="TexPixel documentation and guides" path="/docs" />
<div className="max-w-4xl mx-auto py-16 px-6">
<h1 className="text-3xl font-bold text-gray-900 mb-8">{language === 'en' ? 'Documentation' : '文档'}</h1>
<div className="space-y-4">
{docs.map((doc) => (
<Link key={doc.slug} to={`/docs/${doc.slug}`} className="block p-6 bg-gray-50 rounded-lg hover:bg-gray-100 transition-colors">
<h2 className="text-lg font-semibold text-gray-900">{language === 'en' ? doc.title : doc.titleZh}</h2>
<p className="text-gray-600 mt-1 text-sm">{language === 'en' ? doc.description : doc.descriptionZh}</p>
</Link>
))}
</div>
</div>
</>
);
}
```
- [ ] **Step 2: Create DocDetailPage**
Placeholder that reads `:slug` from params and shows a coming-soon message.
```tsx
import { useParams } from 'react-router-dom';
import SEOHead from '../components/seo/SEOHead';
export default function DocDetailPage() {
const { slug } = useParams<{ slug: string }>();
return (
<>
<SEOHead title={slug ?? 'Doc'} description={`Documentation: ${slug}`} path={`/docs/${slug}`} />
<div className="max-w-4xl mx-auto py-16 px-6">
<h1 className="text-3xl font-bold text-gray-900 mb-4">{slug}</h1>
<p className="text-gray-600">Content coming soon.</p>
</div>
</>
);
}
```
- [ ] **Step 3: Create BlogListPage and BlogDetailPage**
Same pattern as docs. BlogListPage shows placeholder blog entries with date and title. BlogDetailPage reads `:slug` param.
- [ ] **Step 4: Commit**
```bash
git add src/pages/DocsListPage.tsx src/pages/DocDetailPage.tsx src/pages/BlogListPage.tsx src/pages/BlogDetailPage.tsx
git commit -m "feat: add placeholder Docs and Blog pages"
```
---
### Task 9: Build content pipeline (Markdown → JSON)
**Files:**
- Create: `content/docs/en/getting-started.md`
- Create: `content/docs/zh/getting-started.md`
- Create: `content/blog/en/2026-03-25-introducing-texpixel.md`
- Create: `content/blog/zh/2026-03-25-introducing-texpixel.md`
- Create: `scripts/build-content.ts`
- Create: `src/lib/content.ts`
- Modify: `package.json` (add build:content script)
- [ ] **Step 1: Create sample markdown files**
Each file has frontmatter (title, description, slug, date, tags, order) and body content.
- [ ] **Step 2: Create build-content script**
Node script that:
1. Scans `content/docs/{en,zh}/` and `content/blog/{en,zh}/`
2. Parses frontmatter with `gray-matter`
3. Compiles markdown body with `remark` + `rehype` → HTML string
4. Outputs `public/content/docs-manifest.json` and `public/content/blog-manifest.json`
5. Outputs individual `public/content/docs/{lang}/{slug}.json` and `public/content/blog/{lang}/{slug}.json`
Manifest format:
```json
{
"en": [{ "slug": "getting-started", "title": "...", "description": "...", "date": "...", "tags": [], "order": 1 }],
"zh": [...]
}
```
Individual file format:
```json
{ "meta": { ... frontmatter ... }, "html": "<p>compiled html</p>" }
```
- [ ] **Step 3: Create content loader utility**
```tsx
// src/lib/content.ts
import type { Language } from './translations';
export interface ContentMeta {
slug: string;
title: string;
description: string;
date: string;
tags: string[];
order?: number;
cover?: string;
}
export interface ContentManifest {
en: ContentMeta[];
zh: ContentMeta[];
}
export interface ContentItem {
meta: ContentMeta;
html: string;
}
const BASE = '/content';
export async function loadManifest(type: 'docs' | 'blog'): Promise<ContentManifest> {
const res = await fetch(`${BASE}/${type}-manifest.json`);
return res.json();
}
export async function loadContent(type: 'docs' | 'blog', lang: Language, slug: string): Promise<ContentItem> {
const res = await fetch(`${BASE}/${type}/${lang}/${slug}.json`);
return res.json();
}
```
- [ ] **Step 4: Add npm script**
In `package.json`, add:
```json
"build:content": "npx tsx scripts/build-content.ts",
"build": "npm run build:content && vite build"
```
- [ ] **Step 5: Commit**
```bash
git add content/ scripts/build-content.ts src/lib/content.ts package.json
git commit -m "feat: add markdown content pipeline with build script"
```
---
### Task 10: Wire Docs/Blog pages to content pipeline
**Files:**
- Modify: `src/pages/DocsListPage.tsx`
- Modify: `src/pages/DocDetailPage.tsx`
- Modify: `src/pages/BlogListPage.tsx`
- Modify: `src/pages/BlogDetailPage.tsx`
- [ ] **Step 1: Update DocsListPage to load from manifest**
Use `useEffect` + `loadManifest('docs')` to fetch doc list. Render based on current language.
- [ ] **Step 2: Update DocDetailPage to load content**
Use `useEffect` + `loadContent('docs', language, slug)` to fetch and render HTML. Use `dangerouslySetInnerHTML` for the compiled HTML (safe since we control the source markdown). Apply Tailwind typography classes (`prose`).
- [ ] **Step 3: Update Blog pages similarly**
Same pattern. BlogListPage shows date + cover image. BlogDetailPage renders article with `type="article"` in SEOHead.
- [ ] **Step 4: Run build:content and test**
```bash
npm run build:content && npm run dev
```
Visit `/docs`, `/docs/getting-started`, `/blog`, `/blog/introducing-texpixel`.
- [ ] **Step 5: Commit**
```bash
git add src/pages/
git commit -m "feat: wire Docs and Blog pages to markdown content pipeline"
```
---
### Task 11: Update sitemap and SEO infrastructure
**Files:**
- Modify: `public/sitemap.xml`
- Modify: `public/robots.txt`
- Modify: `index.html`
- [ ] **Step 1: Update sitemap.xml**
Add all new routes: `/`, `/app`, `/docs`, `/docs/getting-started`, `/blog`, `/blog/introducing-texpixel`. Set appropriate `changefreq` and `priority`.
- [ ] **Step 2: Update robots.txt**
Add `Disallow: /app` to prevent indexing of the workspace.
- [ ] **Step 3: Clean up index.html**
Since SEOHead now manages per-page meta tags via react-helmet-async, simplify `index.html` to only keep the base defaults. Remove the inline language detection script (LanguageContext handles this).
- [ ] **Step 4: Commit**
```bash
git add public/sitemap.xml public/robots.txt index.html
git commit -m "feat: update sitemap, robots.txt, and index.html for new routes"
```
---
### Task 12: Final verification
- [ ] **Step 1: Type check**
```bash
npm run typecheck
```
- [ ] **Step 2: Lint**
```bash
npm run lint
```
- [ ] **Step 3: Build**
```bash
npm run build
```
- [ ] **Step 4: Test all routes in dev**
```bash
npm run dev
```
Visit: `/`, `/app`, `/docs`, `/docs/getting-started`, `/blog`, `/blog/introducing-texpixel`
Verify:
- Home page shows all sections, anchor links work
- `/app` workspace functions as before
- Docs/Blog pages load content
- Language switching works across all pages
- Mobile responsive nav works
- [ ] **Step 5: Commit any fixes and final commit**
```bash
git add -A
git commit -m "feat: complete website restructure with marketing pages, docs, and blog"
```

View File

@@ -0,0 +1,75 @@
# Website Restructure Design
## Overview
Restructure the single-page OCR app into a multi-section website with Home, Docs, Blog, Pricing, Contact modules while maintaining the core OCR tool functionality in a dedicated workspace page.
## Routes
| Route | Page | SEO | Layout |
|-------|------|-----|--------|
| `/` | HomePage (Hero + Features + HowItWorks + Pricing + Contact) | Prerender | MarketingLayout |
| `/app` | WorkspacePage (current 3-panel OCR layout) | noindex | AppLayout |
| `/docs` | DocsListPage | Prerender | MarketingLayout |
| `/docs/:slug` | DocDetailPage | Prerender | MarketingLayout |
| `/blog` | BlogListPage | Prerender | MarketingLayout |
| `/blog/:slug` | BlogDetailPage | Prerender | MarketingLayout |
| `/auth/google/callback` | AuthCallbackPage | noindex | None |
## Home Page Sections
1. **Hero** — Product tagline + mini upload demo + CTA to `/app`
2. **Features** — Feature cards grid (formula recognition, multi-format export, etc.)
3. **How it works** — 3-step process
4. **Pricing** — Static price cards (Free / Pro / Enterprise), payment integration later
5. **Contact** — Contact info display + form submission
## SEO Strategy
- **Prerendering**: `vite-plugin-prerender` for static HTML generation of marketing pages
- **Meta tags**: `react-helmet-async` for per-page title, description, OG/Twitter cards
- **Structured data**: JSON-LD (SoftwareApplication, Article, TechArticle)
- **Sitemap**: Auto-generated at build time from routes + markdown content
- **i18n SEO**: `hreflang` tags for zh/en
- `/app` route gets `noindex`
## Markdown Content System
```
content/
├── docs/{en,zh}/*.md
└── blog/{en,zh}/*.md
```
- Frontmatter: title, description, slug, date, tags, order, cover
- Build script scans content/, parses frontmatter, generates manifest JSON
- Body compiled via remark + rehype (reusing existing KaTeX pipeline)
## Component Architecture
### Layouts
- `MarketingLayout` — MarketingNavbar + Outlet + Footer
- `AppLayout` — AppNavbar + Outlet
### New Pages
- `src/pages/HomePage.tsx`
- `src/pages/WorkspacePage.tsx` (migrated from App.tsx)
- `src/pages/DocsListPage.tsx`
- `src/pages/DocDetailPage.tsx`
- `src/pages/BlogListPage.tsx`
- `src/pages/BlogDetailPage.tsx`
### Home Sections
- `src/components/home/HeroSection.tsx`
- `src/components/home/FeaturesSection.tsx`
- `src/components/home/HowItWorksSection.tsx`
- `src/components/home/PricingSection.tsx`
- `src/components/home/ContactSection.tsx`
### Shared Layout Components
- `src/components/layout/MarketingNavbar.tsx`
- `src/components/layout/AppNavbar.tsx` (evolved from current Navbar.tsx)
- `src/components/layout/Footer.tsx`
## Migration
- `App.tsx` core logic → `WorkspacePage.tsx`
- `Navbar.tsx` → split into `MarketingNavbar` + `AppNavbar`
- All existing components (LeftSidebar, FilePreview, ResultPanel, etc.) unchanged, referenced by WorkspacePage
## Lazy Loading
All page components use `React.lazy()` + `Suspense` to keep initial bundle small.

View File

@@ -9,6 +9,11 @@
<link rel="manifest" href="/site.webmanifest" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<!-- Fonts: Plus Jakarta Sans (headings) + DM Sans (body) -->
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@500;600;700;800&family=DM+Sans:ital,wght@0,400;0,500;0,600;1,400&display=swap" rel="stylesheet" />
<!-- hreflang: same URL serves both languages (SPA), point both to canonical -->
<link rel="canonical" href="https://texpixel.com/" />
<link rel="alternate" hreflang="zh-CN" href="https://texpixel.com/" />

10
skills-lock.json Normal file
View File

@@ -0,0 +1,10 @@
{
"version": 1,
"skills": {
"frontend-design": {
"source": "anthropics/skills",
"sourceType": "github",
"computedHash": "063a0e6448123cd359ad0044cc46b0e490cc7964d45ef4bb9fd842bd2ffbca67"
}
}
}

View File

@@ -1,5 +1,5 @@
import { useState } from 'react';
import { Mail, Users, Send } from 'lucide-react';
import { Mail, Users, Send, ArrowUpRight } from 'lucide-react';
import { useLanguage } from '../../contexts/LanguageContext';
export default function ContactSection() {
@@ -10,7 +10,6 @@ export default function ContactSection() {
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
setStatus('sending');
// Simulate send — replace with actual API call later
setTimeout(() => {
setStatus('sent');
setTimeout(() => setStatus('idle'), 3000);
@@ -19,75 +18,77 @@ export default function ContactSection() {
};
return (
<section id="contact" className="py-20 bg-gray-50">
<div className="max-w-6xl mx-auto px-6">
<section id="contact" className="section-padding relative" style={{ backgroundColor: 'var(--color-bg)' }}>
<div className="max-w-6xl mx-auto">
<div className="text-center mb-16">
<h2 className="text-3xl font-bold text-gray-900 mb-4">{c.title}</h2>
<p className="text-gray-600 text-lg">{c.subtitle}</p>
<h2 className="font-display text-3xl lg:text-4xl font-bold text-ink mb-4 tracking-tight">{c.title}</h2>
<p className="text-ink-secondary text-lg">{c.subtitle}</p>
</div>
<div className="grid md:grid-cols-2 gap-12 max-w-4xl mx-auto">
{/* Contact info */}
<div className="space-y-6">
<div className="flex items-start gap-4">
<div className="w-10 h-10 bg-blue-100 rounded-lg flex items-center justify-center flex-shrink-0">
<Mail size={20} className="text-blue-600" />
<div className="grid md:grid-cols-5 gap-10 max-w-4xl mx-auto">
{/* Contact info — 2 cols */}
<div className="md:col-span-2 space-y-5">
<a
href="mailto:yogecoder@gmail.com"
className="card p-5 flex items-start gap-4 group hover:border-coral-200 block"
>
<div className="w-10 h-10 bg-coral-50 rounded-xl flex items-center justify-center flex-shrink-0 group-hover:scale-110 transition-transform">
<Mail size={18} className="text-coral-500" />
</div>
<div>
<div className="text-sm text-gray-500 mb-1">{t.common.email}</div>
<a href="mailto:yogecoder@gmail.com" className="text-gray-900 font-medium hover:text-blue-600 transition-colors">
yogecoder@gmail.com
</a>
<div className="flex-1 min-w-0">
<div className="text-xs text-ink-muted mb-1 font-medium">{t.common.email}</div>
<div className="text-sm font-medium text-ink truncate">yogecoder@gmail.com</div>
</div>
</div>
<ArrowUpRight size={14} className="text-ink-muted group-hover:text-coral-500 transition-colors mt-1" />
</a>
<div className="flex items-start gap-4">
<div className="w-10 h-10 bg-green-100 rounded-lg flex items-center justify-center flex-shrink-0">
<Users size={20} className="text-green-600" />
<div className="card p-5 flex items-start gap-4">
<div className="w-10 h-10 bg-sage-50 rounded-xl flex items-center justify-center flex-shrink-0">
<Users size={18} className="text-sage-600" />
</div>
<div>
<div className="text-sm text-gray-500 mb-1">{c.qqGroup}</div>
<span className="text-gray-900 font-medium">1018282100</span>
<div className="text-xs text-ink-muted mb-1 font-medium">{c.qqGroup}</div>
<span className="text-sm font-medium text-ink font-mono">1018282100</span>
</div>
</div>
</div>
{/* Contact form */}
<form onSubmit={handleSubmit} className="space-y-4">
{/* Contact form — 3 cols */}
<form onSubmit={handleSubmit} className="md:col-span-3 space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">{c.nameLabel}</label>
<label className="block text-sm font-medium text-ink mb-1.5">{c.nameLabel}</label>
<input
type="text"
required
className="w-full px-4 py-2.5 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent outline-none transition-shadow text-sm"
className="w-full px-4 py-2.5 bg-white border border-cream-300 rounded-xl focus:ring-2 focus:ring-coral-200 focus:border-coral-400 outline-none transition-all text-sm"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">{c.emailLabel}</label>
<label className="block text-sm font-medium text-ink mb-1.5">{c.emailLabel}</label>
<input
type="email"
required
className="w-full px-4 py-2.5 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent outline-none transition-shadow text-sm"
className="w-full px-4 py-2.5 bg-white border border-cream-300 rounded-xl focus:ring-2 focus:ring-coral-200 focus:border-coral-400 outline-none transition-all text-sm"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">{c.messageLabel}</label>
<label className="block text-sm font-medium text-ink mb-1.5">{c.messageLabel}</label>
<textarea
required
rows={4}
className="w-full px-4 py-2.5 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent outline-none transition-shadow text-sm resize-none"
className="w-full px-4 py-2.5 bg-white border border-cream-300 rounded-xl focus:ring-2 focus:ring-coral-200 focus:border-coral-400 outline-none transition-all text-sm resize-none"
/>
</div>
<button
type="submit"
disabled={status === 'sending'}
className={`w-full py-3 rounded-lg text-sm font-semibold flex items-center justify-center gap-2 transition-colors ${
className={`w-full py-3 rounded-xl text-sm font-semibold flex items-center justify-center gap-2 transition-all duration-200 ${
status === 'sent'
? 'bg-green-600 text-white'
: 'bg-blue-600 hover:bg-blue-700 text-white'
? 'bg-sage-500 text-white'
: 'btn-primary justify-center'
} disabled:opacity-60`}
>
<Send size={16} />
<Send size={15} />
{status === 'sending' ? c.sending : status === 'sent' ? c.sent : c.send}
</button>
</form>

View File

@@ -2,6 +2,14 @@ import { PenTool, FileOutput, FileText, Layers, Zap, Gift } from 'lucide-react';
import { useLanguage } from '../../contexts/LanguageContext';
const iconMap = [PenTool, FileOutput, FileText, Layers, Zap, Gift];
const accentColors = [
{ bg: 'bg-coral-50', icon: 'text-coral-500', border: 'hover:border-coral-200' },
{ bg: 'bg-sage-50', icon: 'text-sage-600', border: 'hover:border-sage-200' },
{ bg: 'bg-amber-50', icon: 'text-amber-600', border: 'hover:border-amber-200' },
{ bg: 'bg-violet-50', icon: 'text-violet-500', border: 'hover:border-violet-200' },
{ bg: 'bg-coral-50', icon: 'text-coral-500', border: 'hover:border-coral-200' },
{ bg: 'bg-sage-50', icon: 'text-sage-600', border: 'hover:border-sage-200' },
];
export default function FeaturesSection() {
const { t } = useLanguage();
@@ -17,23 +25,31 @@ export default function FeaturesSection() {
];
return (
<section id="features" className="py-20 bg-white">
<div className="max-w-6xl mx-auto px-6">
<div className="text-center mb-16">
<h2 className="text-3xl font-bold text-gray-900 mb-4">{f.title}</h2>
<p className="text-gray-600 text-lg">{f.subtitle}</p>
<section id="features" className="section-padding bg-white relative">
{/* Subtle top border accent */}
<div className="absolute top-0 left-1/2 -translate-x-1/2 w-24 h-0.5 bg-gradient-to-r from-transparent via-coral-300 to-transparent" />
<div className="max-w-6xl mx-auto">
<div className="max-w-2xl mb-16">
<h2 className="font-display text-3xl lg:text-4xl font-bold text-ink mb-4 tracking-tight">{f.title}</h2>
<p className="text-ink-secondary text-lg leading-relaxed">{f.subtitle}</p>
</div>
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-8">
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-5">
{items.map((item, i) => {
const Icon = iconMap[i];
const color = accentColors[i];
return (
<div key={i} className="p-6 rounded-xl border border-gray-100 hover:border-blue-100 hover:shadow-lg transition-all group">
<div className="w-12 h-12 bg-blue-50 group-hover:bg-blue-100 rounded-lg flex items-center justify-center mb-4 transition-colors">
<Icon size={24} className="text-blue-600" />
<div
key={i}
className={`card p-6 ${color.border} group`}
style={{ opacity: 0, animationDelay: `${i * 80}ms` }}
>
<div className={`w-11 h-11 ${color.bg} rounded-xl flex items-center justify-center mb-4 transition-transform duration-200 group-hover:scale-110`}>
<Icon size={20} className={color.icon} />
</div>
<h3 className="text-lg font-semibold text-gray-900 mb-2">{item.title}</h3>
<p className="text-gray-600 text-sm leading-relaxed">{item.description}</p>
<h3 className="font-display text-base font-semibold text-ink mb-2">{item.title}</h3>
<p className="text-ink-secondary text-sm leading-relaxed">{item.description}</p>
</div>
);
})}

View File

@@ -1,71 +1,121 @@
import { Link } from 'react-router-dom';
import { ArrowRight, Upload } from 'lucide-react';
import { ArrowRight } from 'lucide-react';
import { useLanguage } from '../../contexts/LanguageContext';
export default function HeroSection() {
const { t } = useLanguage();
const { t, language } = useLanguage();
return (
<section className="relative overflow-hidden bg-gradient-to-br from-blue-50 via-white to-indigo-50 py-20 lg:py-32">
<div className="max-w-6xl mx-auto px-6">
<div className="grid lg:grid-cols-2 gap-12 items-center">
{/* Left: Text */}
<div>
<h1 className="text-4xl lg:text-5xl font-bold text-gray-900 leading-tight mb-6">
<section className="relative overflow-hidden min-h-[90vh] flex items-center">
{/* Warm gradient background */}
<div className="absolute inset-0 bg-gradient-to-br from-cream-100 via-cream-50 to-coral-50/30" />
{/* Decorative elements */}
<div className="absolute top-20 right-[15%] w-72 h-72 bg-coral-200/20 rounded-full blur-3xl" />
<div className="absolute bottom-20 left-[10%] w-56 h-56 bg-sage-200/20 rounded-full blur-3xl" />
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[600px] h-[600px] bg-gradient-radial from-coral-100/10 to-transparent rounded-full" />
{/* Grid pattern */}
<div
className="absolute inset-0 opacity-[0.03]"
style={{
backgroundImage: `linear-gradient(var(--color-ink) 1px, transparent 1px), linear-gradient(90deg, var(--color-ink) 1px, transparent 1px)`,
backgroundSize: '60px 60px',
}}
/>
<div className="relative max-w-6xl mx-auto px-6 py-20 lg:py-28">
<div className="grid lg:grid-cols-12 gap-12 lg:gap-8 items-center">
{/* Left: Text — takes 7 cols for asymmetry */}
<div className="lg:col-span-7 animate-fade-up">
{/* Badge */}
<div className="inline-flex items-center gap-2 px-3 py-1.5 bg-white/80 backdrop-blur-sm border border-cream-300 rounded-full text-xs font-medium text-ink-secondary mb-8">
<span className="w-1.5 h-1.5 rounded-full bg-sage-500 animate-pulse" />
{language === 'zh' ? '免费使用 · 无需注册' : 'Free to use · No sign-up required'}
</div>
<h1 className="font-display text-4xl sm:text-5xl lg:text-[3.5rem] font-extrabold text-ink leading-[1.1] mb-6 tracking-tight">
{t.marketing.hero.title}
</h1>
<p className="text-lg text-gray-600 mb-8 leading-relaxed">
<p className="text-lg text-ink-secondary leading-relaxed mb-10 max-w-xl">
{t.marketing.hero.subtitle}
</p>
<div className="flex flex-wrap gap-4">
<Link
to="/app"
className="inline-flex items-center gap-2 px-6 py-3 bg-blue-600 hover:bg-blue-700 text-white font-semibold rounded-lg transition-colors shadow-lg shadow-blue-600/25"
className="btn-primary px-7 py-3.5 text-base"
>
{t.marketing.hero.cta}
<ArrowRight size={18} />
</Link>
<a
href="#features"
className="inline-flex items-center gap-2 px-6 py-3 bg-white hover:bg-gray-50 text-gray-700 font-semibold rounded-lg transition-colors border border-gray-200"
className="btn-secondary px-7 py-3.5 text-base"
>
{t.marketing.hero.ctaSecondary}
</a>
</div>
{/* Social proof hint */}
<div className="mt-10 flex items-center gap-3 text-sm text-ink-muted">
<div className="flex -space-x-2">
{['bg-coral-300', 'bg-sage-300', 'bg-amber-300', 'bg-violet-300'].map((bg, i) => (
<div key={i} className={`w-7 h-7 rounded-full ${bg} border-2 border-cream-50`} />
))}
</div>
<span>{language === 'zh' ? '已有 1,000+ 学生在使用' : 'Used by 1,000+ students'}</span>
</div>
</div>
{/* Right: Demo card */}
<div className="relative">
{/* Right: Interactive demo card — 5 cols */}
<div className="lg:col-span-5 animate-fade-up delay-200" style={{ opacity: 0 }}>
<Link
to="/app"
className="block bg-white rounded-2xl shadow-2xl border border-gray-200 p-8 hover:shadow-3xl transition-shadow group"
className="block group"
>
<div className="border-2 border-dashed border-gray-300 group-hover:border-blue-400 rounded-xl p-10 flex flex-col items-center justify-center transition-colors">
<div className="w-16 h-16 bg-blue-100 rounded-full flex items-center justify-center mb-4 group-hover:bg-blue-200 transition-colors">
<Upload size={28} className="text-blue-600" />
<div className="card p-1 hover:border-coral-200">
{/* Mock window chrome */}
<div className="flex items-center gap-1.5 px-4 py-3 border-b border-cream-300/50">
<div className="w-2.5 h-2.5 rounded-full bg-coral-300" />
<div className="w-2.5 h-2.5 rounded-full bg-amber-300" />
<div className="w-2.5 h-2.5 rounded-full bg-sage-300" />
<span className="ml-3 text-[10px] font-mono text-ink-muted">texpixel.com/app</span>
</div>
<p className="text-gray-500 text-sm text-center">
{t.sidebar.uploadInstruction}
</p>
</div>
{/* Mock result preview */}
<div className="mt-6 bg-gray-50 rounded-lg p-4">
<div className="flex items-center gap-2 mb-3">
<div className="w-2 h-2 rounded-full bg-green-500" />
<span className="text-xs text-gray-500 font-mono">LaTeX Output</span>
<div className="p-6">
{/* Upload zone */}
<div className="border-2 border-dashed border-cream-300 group-hover:border-coral-300 rounded-xl p-8 flex flex-col items-center justify-center transition-colors duration-300 bg-cream-50/50">
<div className="w-14 h-14 bg-coral-50 group-hover:bg-coral-100 rounded-2xl flex items-center justify-center mb-3 transition-all duration-300 group-hover:scale-110">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="text-coral-500">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
<polyline points="17 8 12 3 7 8" />
<line x1="12" y1="3" x2="12" y2="15" />
</svg>
</div>
<p className="text-ink-muted text-xs text-center">
{t.sidebar.uploadInstruction}
</p>
</div>
{/* Mock result */}
<div className="mt-4 rounded-lg p-4 bg-ink/[0.03]">
<div className="flex items-center gap-2 mb-3">
<div className="w-1.5 h-1.5 rounded-full bg-sage-500" />
<span className="text-[10px] text-ink-muted font-mono tracking-wider uppercase">LaTeX Output</span>
</div>
<code className="text-sm text-ink font-mono block leading-relaxed">
{'\\frac{-b \\pm \\sqrt{b^2 - 4ac}}{2a}'}
</code>
</div>
</div>
<code className="text-sm text-gray-700 font-mono block">
{'\\frac{-b \\pm \\sqrt{b^2 - 4ac}}{2a}'}
</code>
</div>
</Link>
</div>
</div>
</div>
{/* Background decoration */}
<div className="absolute top-0 right-0 w-96 h-96 bg-blue-200/20 rounded-full blur-3xl -translate-y-1/2 translate-x-1/2" />
<div className="absolute bottom-0 left-0 w-72 h-72 bg-indigo-200/20 rounded-full blur-3xl translate-y-1/2 -translate-x-1/2" />
</section>
);
}

View File

@@ -2,6 +2,11 @@ import { Upload, Cpu, Download } from 'lucide-react';
import { useLanguage } from '../../contexts/LanguageContext';
const icons = [Upload, Cpu, Download];
const stepColors = [
{ num: 'text-coral-500', ring: 'ring-coral-100', bg: 'bg-coral-50' },
{ num: 'text-sage-600', ring: 'ring-sage-100', bg: 'bg-sage-50' },
{ num: 'text-amber-600', ring: 'ring-amber-100', bg: 'bg-amber-50' },
];
export default function HowItWorksSection() {
const { t } = useLanguage();
@@ -14,23 +19,33 @@ export default function HowItWorksSection() {
];
return (
<section className="py-20 bg-gray-50">
<div className="max-w-6xl mx-auto px-6">
<section className="section-padding relative" style={{ backgroundColor: 'var(--color-bg)' }}>
<div className="max-w-6xl mx-auto">
<div className="text-center mb-16">
<h2 className="text-3xl font-bold text-gray-900 mb-4">{h.title}</h2>
<h2 className="font-display text-3xl lg:text-4xl font-bold text-ink mb-4 tracking-tight">{h.title}</h2>
</div>
<div className="grid md:grid-cols-3 gap-8">
<div className="grid md:grid-cols-3 gap-8 lg:gap-12 relative">
{/* Connecting line (desktop only) */}
<div className="hidden md:block absolute top-12 left-[20%] right-[20%] h-px border-t-2 border-dashed border-cream-300" />
{steps.map((step, i) => {
const Icon = icons[i];
const color = stepColors[i];
return (
<div key={i} className="text-center">
<div className="w-16 h-16 bg-blue-600 text-white rounded-full flex items-center justify-center mx-auto mb-6 shadow-lg shadow-blue-600/25">
<Icon size={28} />
<div key={i} className="relative text-center group">
{/* Step number badge */}
<div className="relative inline-flex flex-col items-center mb-6">
<div className={`w-24 h-24 ${color.bg} rounded-3xl flex items-center justify-center ring-4 ${color.ring} ring-offset-4 ring-offset-[var(--color-bg)] transition-transform duration-300 group-hover:scale-105 relative z-10`}>
<Icon size={32} className={color.num} strokeWidth={1.5} />
</div>
<span className={`absolute -top-3 -right-3 w-8 h-8 rounded-full bg-white shadow-md flex items-center justify-center font-display font-bold text-sm ${color.num} z-20`}>
{String(i + 1).padStart(2, '0')}
</span>
</div>
<div className="text-sm font-bold text-blue-600 mb-2">0{i + 1}</div>
<h3 className="text-xl font-semibold text-gray-900 mb-3">{step.title}</h3>
<p className="text-gray-600 text-sm leading-relaxed">{step.description}</p>
<h3 className="font-display text-xl font-semibold text-ink mb-3">{step.title}</h3>
<p className="text-ink-secondary text-sm leading-relaxed max-w-xs mx-auto">{step.description}</p>
</div>
);
})}

View File

@@ -16,6 +16,7 @@ export default function PricingSection() {
href: '/app',
disabled: false,
popular: false,
accent: 'sage',
},
{
name: p.pro,
@@ -26,6 +27,7 @@ export default function PricingSection() {
href: '#',
disabled: true,
popular: true,
accent: 'coral',
},
{
name: p.enterprise,
@@ -36,43 +38,44 @@ export default function PricingSection() {
href: '#contact',
disabled: false,
popular: false,
accent: 'ink',
},
];
return (
<section id="pricing" className="py-20 bg-white">
<div className="max-w-6xl mx-auto px-6">
<section id="pricing" className="section-padding bg-white">
<div className="max-w-6xl mx-auto">
<div className="text-center mb-16">
<h2 className="text-3xl font-bold text-gray-900 mb-4">{p.title}</h2>
<p className="text-gray-600 text-lg">{p.subtitle}</p>
<h2 className="font-display text-3xl lg:text-4xl font-bold text-ink mb-4 tracking-tight">{p.title}</h2>
<p className="text-ink-secondary text-lg">{p.subtitle}</p>
</div>
<div className="grid md:grid-cols-3 gap-8 max-w-5xl mx-auto">
<div className="grid md:grid-cols-3 gap-6 max-w-5xl mx-auto">
{plans.map((plan, i) => (
<div
key={i}
className={`rounded-2xl p-8 flex flex-col ${
className={`relative rounded-2xl p-7 flex flex-col transition-all duration-200 ${
plan.popular
? 'border-2 border-blue-600 shadow-xl relative'
: 'border border-gray-200'
? 'border-2 border-coral-400 shadow-xl shadow-coral-500/10 scale-[1.02]'
: 'card'
}`}
>
{plan.popular && (
<div className="absolute -top-3 left-1/2 -translate-x-1/2 bg-blue-600 text-white text-xs font-semibold px-3 py-1 rounded-full">
<div className="absolute -top-3.5 left-1/2 -translate-x-1/2 bg-gradient-to-r from-coral-500 to-coral-400 text-white text-xs font-display font-semibold px-4 py-1 rounded-full shadow-md">
{p.popular}
</div>
)}
<h3 className="text-xl font-bold text-gray-900 mb-2">{plan.name}</h3>
<h3 className="font-display text-lg font-bold text-ink mb-1">{plan.name}</h3>
<div className="mb-6">
<span className="text-4xl font-bold text-gray-900">{plan.price}</span>
{plan.period && <span className="text-gray-500 text-sm">{plan.period}</span>}
<span className="font-display text-4xl font-extrabold text-ink">{plan.price}</span>
{plan.period && <span className="text-ink-muted text-sm ml-1">{plan.period}</span>}
</div>
<ul className="space-y-3 mb-8 flex-1">
{plan.features.map((feature, j) => (
<li key={j} className="flex items-start gap-2 text-sm text-gray-600">
<Check size={16} className="text-green-500 mt-0.5 flex-shrink-0" />
<li key={j} className="flex items-start gap-2.5 text-sm text-ink-secondary">
<Check size={15} className="text-sage-500 mt-0.5 flex-shrink-0" />
{feature}
</li>
))}
@@ -81,17 +84,17 @@ export default function PricingSection() {
{plan.disabled ? (
<button
disabled
className="w-full py-3 rounded-lg text-sm font-semibold bg-gray-100 text-gray-400 cursor-not-allowed"
className="w-full py-3 rounded-xl text-sm font-semibold bg-cream-200 text-ink-muted cursor-not-allowed"
>
{plan.cta}
</button>
) : plan.href.startsWith('/') ? (
<Link
to={plan.href}
className={`w-full py-3 rounded-lg text-sm font-semibold text-center block transition-colors ${
className={`w-full py-3 rounded-xl text-sm font-semibold text-center block transition-all duration-200 ${
plan.popular
? 'bg-blue-600 hover:bg-blue-700 text-white'
: 'bg-gray-900 hover:bg-gray-800 text-white'
? 'btn-primary justify-center'
: 'bg-ink hover:bg-ink/90 text-white shadow-sm'
}`}
>
{plan.cta}
@@ -99,7 +102,7 @@ export default function PricingSection() {
) : (
<a
href={plan.href}
className="w-full py-3 rounded-lg text-sm font-semibold text-center block bg-gray-900 hover:bg-gray-800 text-white transition-colors"
className="w-full py-3 rounded-xl text-sm font-semibold text-center block bg-ink hover:bg-ink/90 text-white transition-colors shadow-sm"
>
{plan.cta}
</a>

View File

@@ -33,48 +33,49 @@ export default function AppNavbar() {
}, []);
return (
<div className="h-16 bg-white border-b border-gray-200 flex items-center justify-between px-6 flex-shrink-0 z-[60] relative">
<div className="h-14 bg-white border-b border-cream-300 flex items-center justify-between px-5 flex-shrink-0 z-[60] relative">
{/* Left: Logo + Home link */}
<div className="flex items-center gap-4">
<Link to="/" className="flex items-center gap-2">
<img src="/texpixel-app-icon.svg" alt="TexPixel" className="w-8 h-8" />
<span className="text-xl font-bold text-gray-900 tracking-tight">TexPixel</span>
<div className="flex items-center gap-3">
<Link to="/" className="flex items-center gap-2 group">
<img src="/texpixel-app-icon.svg" alt="TexPixel" className="w-7 h-7 transition-transform group-hover:scale-110 duration-200" />
<span className="text-lg font-display font-bold text-ink tracking-tight">TexPixel</span>
</Link>
<div className="w-px h-5 bg-cream-300" />
<Link
to="/"
className="flex items-center gap-1 px-2 py-1 text-gray-500 hover:text-gray-700 text-xs transition-colors"
className="flex items-center gap-1.5 px-2 py-1 text-ink-muted hover:text-ink-secondary text-xs font-medium transition-colors rounded-md hover:bg-cream-200/60"
>
<Home size={14} />
<Home size={13} />
<span className="hidden sm:inline">{t.marketing.nav.home}</span>
</Link>
</div>
{/* Right: Actions */}
<div className="flex items-center gap-3">
<div className="flex items-center gap-2">
{/* Language Switcher */}
<div className="relative" ref={langMenuRef}>
<button
onClick={() => setShowLangMenu(!showLangMenu)}
className="flex items-center gap-2 px-3 py-2 hover:bg-gray-100 rounded-lg text-gray-700 text-sm font-medium transition-colors"
className="flex items-center gap-1.5 px-2.5 py-1.5 hover:bg-cream-200/60 rounded-lg text-ink-secondary text-sm font-medium transition-colors"
title="Switch Language"
>
<Languages size={18} />
<span className="hidden sm:inline">{language === 'en' ? 'English' : '简体中文'}</span>
<ChevronDown size={14} className={`transition-transform duration-200 ${showLangMenu ? 'rotate-180' : ''}`} />
<Languages size={16} />
<span className="hidden sm:inline text-xs">{language === 'en' ? 'EN' : '中文'}</span>
<ChevronDown size={12} className={`transition-transform duration-200 ${showLangMenu ? 'rotate-180' : ''}`} />
</button>
{showLangMenu && (
<div className="absolute right-0 top-full mt-2 w-32 bg-white rounded-xl shadow-lg border border-gray-200 py-1 z-50 animate-in fade-in slide-in-from-top-2 duration-200">
<div className="absolute right-0 top-full mt-2 w-36 bg-white rounded-xl shadow-lg border border-cream-300 py-1.5 z-50">
<button
onClick={() => { setLanguage('en'); setShowLangMenu(false); }}
className={`w-full flex items-center justify-between px-4 py-2 text-sm transition-colors hover:bg-gray-50 ${language === 'en' ? 'text-blue-600 font-medium' : 'text-gray-700'}`}
className={`w-full flex items-center justify-between px-4 py-2.5 text-sm transition-colors hover:bg-cream-100 ${language === 'en' ? 'text-coral-600 font-semibold' : 'text-ink-secondary'}`}
>
English
{language === 'en' && <Check size={14} />}
</button>
<button
onClick={() => { setLanguage('zh'); setShowLangMenu(false); }}
className={`w-full flex items-center justify-between px-4 py-2 text-sm transition-colors hover:bg-gray-50 ${language === 'zh' ? 'text-blue-600 font-medium' : 'text-gray-700'}`}
className={`w-full flex items-center justify-between px-4 py-2.5 text-sm transition-colors hover:bg-cream-100 ${language === 'zh' ? 'text-coral-600 font-semibold' : 'text-ink-secondary'}`}
>
{language === 'zh' && <Check size={14} />}
@@ -86,52 +87,52 @@ export default function AppNavbar() {
{/* User Guide Button */}
<button
id="guide-button"
className="flex items-center gap-2 px-3 py-2 hover:bg-gray-100 rounded-lg text-gray-700 text-sm font-medium transition-colors"
className="flex items-center gap-1.5 px-2.5 py-1.5 hover:bg-cream-200/60 rounded-lg text-ink-secondary text-sm font-medium transition-colors"
onClick={() => {
window.dispatchEvent(new CustomEvent('start-user-guide'));
}}
>
<HelpCircle size={18} />
<span className="hidden sm:inline">{t.common.guide}</span>
<HelpCircle size={16} />
<span className="hidden sm:inline text-xs">{t.common.guide}</span>
</button>
{/* Reward Button */}
<div className="relative">
<button
onClick={() => setShowReward(!showReward)}
className="flex items-center gap-2 px-4 py-2 bg-gradient-to-r from-rose-500 to-pink-500 hover:from-rose-600 hover:to-pink-600 rounded-lg text-white text-sm font-medium transition-all shadow-sm hover:shadow-md"
className="flex items-center gap-1.5 px-3 py-1.5 bg-gradient-to-r from-coral-500 to-coral-400 hover:from-coral-600 hover:to-coral-500 rounded-lg text-white text-xs font-semibold transition-all shadow-sm hover:shadow-md"
>
<Heart size={14} className="fill-white" />
<Heart size={13} className="fill-white" />
<span>{t.common.reward}</span>
</button>
{showReward && (
<div
className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-[70] p-4"
className="fixed inset-0 bg-black/40 backdrop-blur-sm flex items-center justify-center z-[70] p-4"
onClick={() => setShowReward(false)}
>
<div
className="bg-white rounded-xl shadow-xl max-w-sm w-full p-6 animate-in fade-in zoom-in-95 duration-200"
className="bg-white rounded-2xl shadow-2xl max-w-sm w-full p-6"
onClick={e => e.stopPropagation()}
>
<div className="flex items-center justify-between mb-4">
<span className="text-lg font-bold text-gray-900">{t.navbar.rewardTitle}</span>
<span className="text-lg font-display font-bold text-ink">{t.navbar.rewardTitle}</span>
<button
onClick={() => setShowReward(false)}
className="p-2 hover:bg-gray-100 rounded-lg transition-colors"
className="p-1.5 hover:bg-cream-200 rounded-lg transition-colors"
>
<X size={20} className="text-gray-500" />
<X size={18} className="text-ink-muted" />
</button>
</div>
<div className="flex flex-col items-center">
<img
src="https://cdn.texpixel.com/public/rewardcode.png"
alt={t.navbar.rewardTitle}
className="w-64 h-64 object-contain rounded-lg shadow-sm"
className="w-60 h-60 object-contain rounded-xl"
/>
<p className="text-sm text-gray-500 text-center mt-4">
<p className="text-sm text-ink-secondary text-center mt-4">
{t.navbar.rewardThanks}<br />
<span className="text-xs text-gray-400 mt-1 block">{t.navbar.rewardSubtitle}</span>
<span className="text-xs text-ink-muted mt-1 block">{t.navbar.rewardSubtitle}</span>
</p>
</div>
</div>
@@ -143,46 +144,46 @@ export default function AppNavbar() {
<div className="relative" ref={dropdownRef}>
<button
onClick={() => setShowContact(!showContact)}
className="flex items-center gap-2 px-4 py-2 bg-gray-100 hover:bg-gray-200 rounded-lg text-gray-700 text-sm font-medium transition-colors"
className="flex items-center gap-1.5 px-3 py-1.5 bg-cream-200 hover:bg-cream-300 rounded-lg text-ink-secondary text-xs font-semibold transition-colors"
>
<MessageCircle size={14} />
<MessageCircle size={13} />
<span>{t.common.contactUs}</span>
<ChevronDown
size={14}
size={12}
className={`transition-transform duration-200 ${showContact ? 'rotate-180' : ''}`}
/>
</button>
{showContact && (
<div className="absolute right-0 top-full mt-2 w-64 bg-white rounded-xl shadow-lg border border-gray-200 py-2 z-50 animate-in fade-in slide-in-from-top-2 duration-200">
<div className="absolute right-0 top-full mt-2 w-64 bg-white rounded-xl shadow-lg border border-cream-300 py-2 z-50">
<a
href="mailto:yogecoder@gmail.com"
className="flex items-center gap-3 px-4 py-3 hover:bg-gray-50 transition-colors"
className="flex items-center gap-3 px-4 py-3 hover:bg-cream-100 transition-colors"
>
<div className="w-8 h-8 bg-blue-100 rounded-lg flex items-center justify-center">
<Mail size={16} className="text-blue-600" />
<div className="w-8 h-8 bg-coral-50 rounded-lg flex items-center justify-center">
<Mail size={15} className="text-coral-600" />
</div>
<div>
<div className="text-xs text-gray-500">{t.common.email}</div>
<div className="text-sm font-medium text-gray-900">yogecoder@gmail.com</div>
<div className="text-xs text-ink-muted">{t.common.email}</div>
<div className="text-sm font-medium text-ink">yogecoder@gmail.com</div>
</div>
</a>
<div
className={`flex items-center gap-3 px-4 py-3 hover:bg-gray-50 transition-all cursor-pointer ${copied ? 'bg-green-50' : ''}`}
className={`flex items-center gap-3 px-4 py-3 hover:bg-cream-100 transition-all cursor-pointer ${copied ? 'bg-sage-50' : ''}`}
onClick={handleCopyQQ}
>
<div className={`w-8 h-8 rounded-lg flex items-center justify-center transition-colors ${copied ? 'bg-green-500' : 'bg-green-100'}`}>
<div className={`w-8 h-8 rounded-lg flex items-center justify-center transition-colors ${copied ? 'bg-sage-500' : 'bg-sage-50'}`}>
{copied ? (
<Check size={16} className="text-white" />
<Check size={15} className="text-white" />
) : (
<Users size={16} className="text-green-600" />
<Users size={15} className="text-sage-600" />
)}
</div>
<div>
<div className={`text-xs transition-colors ${copied ? 'text-green-600 font-medium' : 'text-gray-500'}`}>
<div className={`text-xs transition-colors ${copied ? 'text-sage-600 font-medium' : 'text-ink-muted'}`}>
{copied ? t.common.copied : t.common.qqGroup}
</div>
<div className="text-sm font-medium text-gray-900">1018282100</div>
<div className="text-sm font-medium text-ink">1018282100</div>
</div>
</div>
</div>

View File

@@ -5,42 +5,76 @@ export default function Footer() {
const { t } = useLanguage();
return (
<footer className="bg-gray-900 text-gray-400 py-12 px-6">
<div className="max-w-6xl mx-auto grid grid-cols-1 md:grid-cols-4 gap-8">
<div>
<div className="flex items-center gap-2 mb-4">
<img src="/texpixel-app-icon.svg" alt="TexPixel" className="w-6 h-6" />
<span className="text-white font-bold">TexPixel</span>
</div>
<p className="text-sm">{t.marketing.footer.tagline}</p>
</div>
<footer className="relative bg-ink text-cream-300 overflow-hidden">
{/* Decorative top edge */}
<div className="absolute top-0 left-0 right-0 h-px bg-gradient-to-r from-transparent via-coral-500/40 to-transparent" />
<div>
<h4 className="text-white font-semibold mb-3 text-sm">{t.marketing.footer.product}</h4>
<div className="flex flex-col gap-2 text-sm">
<Link to="/app" className="hover:text-white transition-colors">{t.marketing.nav.launchApp}</Link>
<a href="/#pricing" className="hover:text-white transition-colors">{t.marketing.nav.pricing}</a>
<div className="max-w-6xl mx-auto px-6 py-16">
<div className="grid grid-cols-1 md:grid-cols-12 gap-10">
{/* Brand column */}
<div className="md:col-span-4">
<div className="flex items-center gap-2.5 mb-4">
<img src="/texpixel-app-icon.svg" alt="TexPixel" className="w-7 h-7" />
<span className="text-white font-display font-bold text-lg">TexPixel</span>
</div>
<p className="text-sm text-cream-300/70 leading-relaxed max-w-xs">
{t.marketing.footer.tagline}
</p>
</div>
{/* Product */}
<div className="md:col-span-2 md:col-start-6">
<h4 className="text-white font-display font-semibold text-sm mb-4 tracking-wide uppercase">
{t.marketing.footer.product}
</h4>
<div className="flex flex-col gap-2.5 text-sm">
<Link to="/app" className="text-cream-300/70 hover:text-white transition-colors w-fit">
{t.marketing.nav.launchApp}
</Link>
<a href="/#pricing" className="text-cream-300/70 hover:text-white transition-colors w-fit">
{t.marketing.nav.pricing}
</a>
</div>
</div>
{/* Resources */}
<div className="md:col-span-2">
<h4 className="text-white font-display font-semibold text-sm mb-4 tracking-wide uppercase">
{t.marketing.footer.resources}
</h4>
<div className="flex flex-col gap-2.5 text-sm">
<Link to="/docs" className="text-cream-300/70 hover:text-white transition-colors w-fit">
{t.marketing.nav.docs}
</Link>
<Link to="/blog" className="text-cream-300/70 hover:text-white transition-colors w-fit">
{t.marketing.nav.blog}
</Link>
</div>
</div>
{/* Contact */}
<div className="md:col-span-2">
<h4 className="text-white font-display font-semibold text-sm mb-4 tracking-wide uppercase">
{t.marketing.footer.contactTitle}
</h4>
<div className="flex flex-col gap-2.5 text-sm">
<a href="mailto:yogecoder@gmail.com" className="text-cream-300/70 hover:text-white transition-colors w-fit">
yogecoder@gmail.com
</a>
</div>
</div>
</div>
<div>
<h4 className="text-white font-semibold mb-3 text-sm">{t.marketing.footer.resources}</h4>
<div className="flex flex-col gap-2 text-sm">
<Link to="/docs" className="hover:text-white transition-colors">{t.marketing.nav.docs}</Link>
<Link to="/blog" className="hover:text-white transition-colors">{t.marketing.nav.blog}</Link>
<div className="mt-12 pt-8 border-t border-white/10 flex flex-col sm:flex-row items-center justify-between gap-4">
<p className="text-xs text-cream-300/50">
&copy; {new Date().getFullYear()} TexPixel. All rights reserved.
</p>
<div className="flex items-center gap-1 text-xs text-cream-300/40">
<span>Built with</span>
<span className="text-coral-400">care</span>
<span>for students & researchers</span>
</div>
</div>
<div>
<h4 className="text-white font-semibold mb-3 text-sm">{t.marketing.footer.contactTitle}</h4>
<div className="flex flex-col gap-2 text-sm">
<a href="mailto:yogecoder@gmail.com" className="hover:text-white transition-colors">yogecoder@gmail.com</a>
</div>
</div>
</div>
<div className="max-w-6xl mx-auto mt-8 pt-8 border-t border-gray-800 text-sm text-center">
&copy; {new Date().getFullYear()} TexPixel. All rights reserved.
</div>
</footer>
);

View File

@@ -8,6 +8,7 @@ export default function MarketingNavbar() {
const location = useLocation();
const [showLangMenu, setShowLangMenu] = useState(false);
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
const [scrolled, setScrolled] = useState(false);
const langMenuRef = useRef<HTMLDivElement>(null);
const isHome = location.pathname === '/';
@@ -34,53 +35,73 @@ export default function MarketingNavbar() {
return () => document.removeEventListener('mousedown', handleClickOutside);
}, []);
useEffect(() => {
const handleScroll = () => setScrolled(window.scrollY > 10);
window.addEventListener('scroll', handleScroll, { passive: true });
return () => window.removeEventListener('scroll', handleScroll);
}, []);
return (
<nav className="h-16 bg-white border-b border-gray-200 flex items-center justify-between px-6 flex-shrink-0 z-[60] relative">
<Link to="/" className="flex items-center gap-2">
<img src="/texpixel-app-icon.svg" alt="TexPixel" className="w-8 h-8" />
<span className="text-xl font-bold text-gray-900 tracking-tight">TexPixel</span>
<nav
className={`h-16 flex items-center justify-between px-6 flex-shrink-0 z-[60] relative transition-all duration-300 ${
scrolled
? 'bg-white/90 backdrop-blur-md border-b border-cream-300 shadow-sm'
: 'bg-transparent border-b border-transparent'
}`}
>
<Link to="/" className="flex items-center gap-2.5 group">
<img src="/texpixel-app-icon.svg" alt="TexPixel" className="w-8 h-8 transition-transform group-hover:scale-110 duration-200" />
<span className="text-xl font-display font-bold text-ink tracking-tight">TexPixel</span>
</Link>
<div className="hidden md:flex items-center gap-6">
{navLinks.map((link) => (
<Link
key={link.to}
to={link.to}
className={`text-sm font-medium transition-colors ${
location.pathname === link.to ? 'text-blue-600' : 'text-gray-700 hover:text-gray-900'
}`}
>
{link.label}
</Link>
))}
<div className="hidden md:flex items-center gap-1">
{navLinks.map((link) => {
const isActive = location.pathname === link.to;
return (
<Link
key={link.to}
to={link.to}
className={`relative px-4 py-2 text-sm font-medium rounded-lg transition-colors duration-200 ${
isActive
? 'text-coral-600 bg-coral-50'
: 'text-ink-secondary hover:text-ink hover:bg-cream-200/60'
}`}
>
{link.label}
</Link>
);
})}
{anchorLinks.map((link) => (
<a
key={link.href}
href={link.href}
className="text-sm font-medium text-gray-700 hover:text-gray-900 transition-colors"
className="px-4 py-2 text-sm font-medium text-ink-secondary hover:text-ink hover:bg-cream-200/60 rounded-lg transition-colors duration-200"
>
{link.label}
</a>
))}
</div>
<div className="flex items-center gap-3">
<div className="flex items-center gap-2">
{/* Language Switcher */}
<div className="relative" ref={langMenuRef}>
<button
onClick={() => setShowLangMenu(!showLangMenu)}
className="flex items-center gap-2 px-3 py-2 hover:bg-gray-100 rounded-lg text-gray-700 text-sm font-medium transition-colors"
className="flex items-center gap-1.5 px-3 py-2 hover:bg-cream-200/60 rounded-lg text-ink-secondary text-sm font-medium transition-colors"
>
<Languages size={18} />
<Languages size={16} />
<span className="hidden sm:inline">{language === 'en' ? 'EN' : '中文'}</span>
<ChevronDown size={14} className={`transition-transform duration-200 ${showLangMenu ? 'rotate-180' : ''}`} />
<ChevronDown size={12} className={`transition-transform duration-200 ${showLangMenu ? 'rotate-180' : ''}`} />
</button>
{showLangMenu && (
<div className="absolute right-0 top-full mt-2 w-32 bg-white rounded-xl shadow-lg border border-gray-200 py-1 z-50">
<div className="absolute right-0 top-full mt-2 w-36 bg-white rounded-xl shadow-lg border border-cream-300 py-1.5 z-50">
{(['en', 'zh'] as const).map((lang) => (
<button
key={lang}
onClick={() => { setLanguage(lang); setShowLangMenu(false); }}
className={`w-full flex items-center justify-between px-4 py-2 text-sm transition-colors hover:bg-gray-50 ${language === lang ? 'text-blue-600 font-medium' : 'text-gray-700'}`}
className={`w-full flex items-center justify-between px-4 py-2.5 text-sm transition-colors hover:bg-cream-100 ${
language === lang ? 'text-coral-600 font-semibold' : 'text-ink-secondary'
}`}
>
{lang === 'en' ? 'English' : '简体中文'}
{language === lang && <Check size={14} />}
@@ -92,29 +113,46 @@ export default function MarketingNavbar() {
<Link
to="/app"
className="hidden sm:inline-flex items-center px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white text-sm font-medium rounded-lg transition-colors"
className="hidden sm:inline-flex items-center px-5 py-2 btn-primary text-sm rounded-lg"
>
{t.marketing.nav.launchApp}
</Link>
<button className="md:hidden p-2" onClick={() => setMobileMenuOpen(!mobileMenuOpen)}>
{mobileMenuOpen ? <X size={20} /> : <Menu size={20} />}
<button className="md:hidden p-2 text-ink-secondary hover:text-ink" onClick={() => setMobileMenuOpen(!mobileMenuOpen)}>
{mobileMenuOpen ? <X size={22} /> : <Menu size={22} />}
</button>
</div>
{/* Mobile menu */}
{mobileMenuOpen && (
<div className="absolute top-16 left-0 right-0 bg-white border-b border-gray-200 shadow-lg md:hidden z-50 py-4 px-6 flex flex-col gap-3">
<div className="absolute top-16 left-0 right-0 bg-white/95 backdrop-blur-md border-b border-cream-300 shadow-lg md:hidden z-50 py-3 px-6 flex flex-col gap-1">
{navLinks.map((link) => (
<Link key={link.to} to={link.to} className="text-sm font-medium text-gray-700 py-2" onClick={() => setMobileMenuOpen(false)}>
<Link
key={link.to}
to={link.to}
className={`text-sm font-medium py-2.5 px-3 rounded-lg transition-colors ${
location.pathname === link.to ? 'text-coral-600 bg-coral-50' : 'text-ink-secondary hover:bg-cream-200/60'
}`}
onClick={() => setMobileMenuOpen(false)}
>
{link.label}
</Link>
))}
{anchorLinks.map((link) => (
<a key={link.href} href={link.href} className="text-sm font-medium text-gray-700 py-2" onClick={() => setMobileMenuOpen(false)}>
<a
key={link.href}
href={link.href}
className="text-sm font-medium text-ink-secondary py-2.5 px-3 rounded-lg hover:bg-cream-200/60"
onClick={() => setMobileMenuOpen(false)}
>
{link.label}
</a>
))}
<Link to="/app" className="text-sm font-medium text-blue-600 py-2" onClick={() => setMobileMenuOpen(false)}>
<Link
to="/app"
className="text-sm font-semibold text-coral-600 py-2.5 px-3"
onClick={() => setMobileMenuOpen(false)}
>
{t.marketing.nav.launchApp}
</Link>
</div>

View File

@@ -2,6 +2,112 @@
@tailwind components;
@tailwind utilities;
@layer base {
:root {
/* Warm Coral + Ink palette */
--color-primary: #e05a33;
--color-primary-hover: #c94a28;
--color-primary-light: #fef0ec;
--color-primary-glow: rgba(224, 90, 51, 0.15);
--color-secondary: #2a9d8f;
--color-secondary-light: #eef9f7;
--color-accent: #e9c46a;
--color-accent-light: #fdf6e3;
--color-bg: #faf8f4;
--color-surface: #ffffff;
--color-surface-raised: #f5f3ee;
--color-ink: #1c1917;
--color-ink-secondary: #57534e;
--color-ink-muted: #a8a29e;
--color-border: #e7e5e4;
--color-border-light: #f0eeea;
}
html {
scroll-behavior: smooth;
}
body {
font-family: 'DM Sans', system-ui, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
color: var(--color-ink);
background-color: var(--color-bg);
}
h1, h2, h3, h4, h5, h6 {
font-family: 'Plus Jakarta Sans', system-ui, sans-serif;
}
}
@layer components {
/* Distinctive button styles */
.btn-primary {
@apply inline-flex items-center gap-2 font-semibold rounded-xl transition-all duration-200;
background-color: var(--color-primary);
color: white;
box-shadow: 0 4px 14px var(--color-primary-glow), 0 1px 3px rgba(0,0,0,0.08);
}
.btn-primary:hover {
background-color: var(--color-primary-hover);
box-shadow: 0 6px 20px var(--color-primary-glow), 0 2px 4px rgba(0,0,0,0.1);
transform: translateY(-1px);
}
.btn-secondary {
@apply inline-flex items-center gap-2 font-semibold rounded-xl transition-all duration-200;
background-color: var(--color-surface);
color: var(--color-ink);
border: 1.5px solid var(--color-border);
}
.btn-secondary:hover {
border-color: var(--color-ink-muted);
box-shadow: 0 2px 8px rgba(0,0,0,0.06);
transform: translateY(-1px);
}
/* Card with warm shadow */
.card {
background-color: var(--color-surface);
border: 1px solid var(--color-border-light);
border-radius: 1rem;
box-shadow: 0 1px 3px rgba(0,0,0,0.04), 0 4px 12px rgba(0,0,0,0.02);
transition: all 0.25s ease;
}
.card:hover {
box-shadow: 0 4px 16px rgba(0,0,0,0.06), 0 8px 24px rgba(0,0,0,0.03);
transform: translateY(-2px);
}
/* Section spacing */
.section-padding {
@apply py-24 lg:py-32 px-6;
}
/* Grain texture overlay */
.grain::after {
content: '';
position: absolute;
inset: 0;
opacity: 0.3;
pointer-events: none;
background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='0.04'/%3E%3C/svg%3E");
}
/* Prose overrides for blog/docs */
.prose-warm {
--tw-prose-body: var(--color-ink-secondary);
--tw-prose-headings: var(--color-ink);
--tw-prose-links: var(--color-primary);
--tw-prose-bold: var(--color-ink);
--tw-prose-code: var(--color-primary);
}
}
@layer utilities {
.custom-scrollbar::-webkit-scrollbar {
width: 6px;
@@ -13,15 +119,34 @@
}
.custom-scrollbar::-webkit-scrollbar-thumb {
background-color: rgba(156, 163, 175, 0.3);
background-color: rgba(168, 162, 158, 0.3);
border-radius: 20px;
}
.custom-scrollbar::-webkit-scrollbar-thumb:hover {
background-color: rgba(156, 163, 175, 0.5);
background-color: rgba(168, 162, 158, 0.5);
}
}
body {
@apply antialiased text-gray-900 bg-gray-50;
/* Fade-in animation */
@keyframes fade-up {
from {
opacity: 0;
transform: translateY(16px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.animate-fade-up {
animation: fade-up 0.6s ease-out forwards;
}
/* Stagger delays */
.delay-100 { animation-delay: 100ms; }
.delay-200 { animation-delay: 200ms; }
.delay-300 { animation-delay: 300ms; }
.delay-400 { animation-delay: 400ms; }
.delay-500 { animation-delay: 500ms; }
}

View File

@@ -1,5 +1,6 @@
import { useEffect, useState } from 'react';
import { useParams } from 'react-router-dom';
import { useParams, Link } from 'react-router-dom';
import { ArrowLeft, Calendar } from 'lucide-react';
import SEOHead from '../components/seo/SEOHead';
import { useLanguage } from '../contexts/LanguageContext';
import { loadContent, type ContentItem } from '../lib/content';
@@ -19,8 +20,13 @@ export default function BlogDetailPage() {
if (!content) {
return (
<div className="max-w-4xl mx-auto py-16 px-6">
<p className="text-gray-600">{language === 'en' ? 'Loading...' : '加载中...'}</p>
<div className="max-w-3xl mx-auto py-20 px-6">
<div className="animate-pulse space-y-4">
<div className="h-4 bg-cream-200 rounded w-24" />
<div className="h-8 bg-cream-200 rounded w-3/4" />
<div className="h-4 bg-cream-200 rounded w-full" />
<div className="h-4 bg-cream-200 rounded w-5/6" />
</div>
</div>
);
}
@@ -34,10 +40,44 @@ export default function BlogDetailPage() {
type="article"
publishedTime={content.meta.date}
/>
<div className="max-w-4xl mx-auto py-16 px-6">
<div className="text-sm text-gray-400 mb-4">{content.meta.date}</div>
<div className="max-w-3xl mx-auto py-16 lg:py-20 px-6">
{/* Back link */}
<Link
to="/blog"
className="inline-flex items-center gap-1.5 text-sm text-ink-muted hover:text-ink transition-colors mb-8"
>
<ArrowLeft size={14} />
{language === 'en' ? 'All posts' : '所有文章'}
</Link>
{/* Article header */}
<header className="mb-10">
<div className="flex items-center gap-2 text-sm text-ink-muted mb-4">
<Calendar size={14} />
<time>{content.meta.date}</time>
</div>
{content.meta.tags && content.meta.tags.length > 0 && (
<div className="flex gap-2 mb-4">
{content.meta.tags.map((tag: string) => (
<span key={tag} className="text-xs bg-coral-50 text-coral-600 px-2.5 py-1 rounded-full font-medium">
{tag}
</span>
))}
</div>
)}
</header>
{/* Article body */}
<article
className="prose prose-gray max-w-none"
className="prose prose-lg prose-warm max-w-none
prose-headings:font-display prose-headings:tracking-tight
prose-h1:text-3xl prose-h1:font-bold
prose-h2:text-2xl prose-h2:font-semibold prose-h2:mt-10
prose-a:text-coral-600 prose-a:no-underline hover:prose-a:underline
prose-code:text-coral-600 prose-code:bg-coral-50 prose-code:px-1.5 prose-code:py-0.5 prose-code:rounded-md prose-code:text-sm
prose-pre:bg-ink prose-pre:text-cream-100 prose-pre:rounded-xl
prose-img:rounded-xl
prose-blockquote:border-coral-300 prose-blockquote:bg-coral-50/30 prose-blockquote:rounded-r-xl prose-blockquote:py-1"
dangerouslySetInnerHTML={{ __html: content.html }}
/>
</div>

View File

@@ -1,5 +1,6 @@
import { useEffect, useState } from 'react';
import { Link } from 'react-router-dom';
import { ArrowRight, Calendar } from 'lucide-react';
import SEOHead from '../components/seo/SEOHead';
import { useLanguage } from '../contexts/LanguageContext';
import { loadManifest, type ContentMeta } from '../lib/content';
@@ -14,27 +15,96 @@ export default function BlogListPage() {
});
}, [language]);
const featured = posts[0];
const rest = posts.slice(1);
return (
<>
<SEOHead title="Blog" description="TexPixel blog — updates, tutorials, and insights" path="/blog" />
<div className="max-w-4xl mx-auto py-16 px-6">
<h1 className="text-3xl font-bold text-gray-900 mb-8">{language === 'en' ? 'Blog' : '博客'}</h1>
<div className="space-y-6">
{posts.map((post) => (
<Link key={post.slug} to={`/blog/${post.slug}`} className="block p-6 bg-gray-50 rounded-lg hover:bg-gray-100 transition-colors">
<div className="text-xs text-gray-400 mb-2">{post.date}</div>
<h2 className="text-lg font-semibold text-gray-900">{post.title}</h2>
<p className="text-gray-600 mt-1 text-sm">{post.description}</p>
{post.tags.length > 0 && (
<div className="flex gap-2 mt-3">
{post.tags.map(tag => (
<span key={tag} className="text-xs bg-gray-200 text-gray-600 px-2 py-0.5 rounded">{tag}</span>
))}
</div>
)}
</Link>
))}
<div className="max-w-5xl mx-auto py-16 lg:py-20 px-6">
{/* Header */}
<div className="mb-12">
<h1 className="font-display text-4xl lg:text-5xl font-bold text-ink tracking-tight mb-4">
{language === 'en' ? 'Blog' : '博客'}
</h1>
<p className="text-ink-secondary text-lg max-w-xl">
{language === 'en'
? 'Updates, tutorials, and insights on formula recognition and LaTeX.'
: '关于公式识别和 LaTeX 的更新、教程和见解。'}
</p>
</div>
{/* Featured post */}
{featured && (
<Link
to={`/blog/${featured.slug}`}
className="card block p-8 mb-8 group hover:border-coral-200"
>
<div className="flex items-center gap-2 text-xs text-ink-muted mb-3">
<Calendar size={13} />
<time>{featured.date}</time>
</div>
<h2 className="font-display text-2xl lg:text-3xl font-bold text-ink mb-3 group-hover:text-coral-600 transition-colors">
{featured.title}
</h2>
<p className="text-ink-secondary leading-relaxed mb-4 max-w-2xl">
{featured.description}
</p>
<div className="flex items-center justify-between">
<div className="flex gap-2">
{featured.tags.map(tag => (
<span key={tag} className="text-xs bg-coral-50 text-coral-600 px-2.5 py-1 rounded-full font-medium">
{tag}
</span>
))}
</div>
<span className="inline-flex items-center gap-1 text-sm font-medium text-coral-500 group-hover:gap-2 transition-all">
{language === 'en' ? 'Read more' : '阅读全文'}
<ArrowRight size={14} />
</span>
</div>
</Link>
)}
{/* Rest of posts */}
{rest.length > 0 && (
<div className="grid md:grid-cols-2 gap-5">
{rest.map((post) => (
<Link
key={post.slug}
to={`/blog/${post.slug}`}
className="card p-6 group hover:border-coral-200"
>
<div className="text-xs text-ink-muted mb-2">
<time>{post.date}</time>
</div>
<h2 className="font-display text-lg font-semibold text-ink mb-2 group-hover:text-coral-600 transition-colors">
{post.title}
</h2>
<p className="text-ink-secondary text-sm leading-relaxed mb-3">{post.description}</p>
{post.tags.length > 0 && (
<div className="flex gap-2">
{post.tags.map(tag => (
<span key={tag} className="text-[11px] bg-cream-200 text-ink-secondary px-2 py-0.5 rounded-full">
{tag}
</span>
))}
</div>
)}
</Link>
))}
</div>
)}
{/* Empty state */}
{posts.length === 0 && (
<div className="card p-12 text-center">
<p className="text-ink-muted text-sm">
{language === 'en' ? 'No posts yet. Check back soon!' : '暂无文章,敬请期待!'}
</p>
</div>
)}
</div>
</>
);

View File

@@ -1,5 +1,6 @@
import { useEffect, useState } from 'react';
import { useParams } from 'react-router-dom';
import { useParams, Link } from 'react-router-dom';
import { ArrowLeft } from 'lucide-react';
import SEOHead from '../components/seo/SEOHead';
import { useLanguage } from '../contexts/LanguageContext';
import { loadContent, type ContentItem } from '../lib/content';
@@ -19,8 +20,12 @@ export default function DocDetailPage() {
if (!content) {
return (
<div className="max-w-4xl mx-auto py-16 px-6">
<p className="text-gray-600">{language === 'en' ? 'Loading...' : '加载中...'}</p>
<div className="max-w-3xl mx-auto py-20 px-6">
<div className="animate-pulse space-y-4">
<div className="h-4 bg-cream-200 rounded w-24" />
<div className="h-8 bg-cream-200 rounded w-3/4" />
<div className="h-4 bg-cream-200 rounded w-full" />
</div>
</div>
);
}
@@ -32,9 +37,27 @@ export default function DocDetailPage() {
description={content.meta.description}
path={`/docs/${slug}`}
/>
<div className="max-w-4xl mx-auto py-16 px-6">
<div className="max-w-3xl mx-auto py-16 lg:py-20 px-6">
{/* Back link */}
<Link
to="/docs"
className="inline-flex items-center gap-1.5 text-sm text-ink-muted hover:text-ink transition-colors mb-8"
>
<ArrowLeft size={14} />
{language === 'en' ? 'All docs' : '所有文档'}
</Link>
{/* Doc body */}
<article
className="prose prose-gray max-w-none"
className="prose prose-lg prose-warm max-w-none
prose-headings:font-display prose-headings:tracking-tight
prose-h1:text-3xl prose-h1:font-bold
prose-h2:text-2xl prose-h2:font-semibold prose-h2:mt-10
prose-a:text-sage-700 prose-a:no-underline hover:prose-a:underline
prose-code:text-sage-700 prose-code:bg-sage-50 prose-code:px-1.5 prose-code:py-0.5 prose-code:rounded-md prose-code:text-sm
prose-pre:bg-ink prose-pre:text-cream-100 prose-pre:rounded-xl
prose-img:rounded-xl
prose-blockquote:border-sage-300 prose-blockquote:bg-sage-50/30 prose-blockquote:rounded-r-xl prose-blockquote:py-1"
dangerouslySetInnerHTML={{ __html: content.html }}
/>
</div>

View File

@@ -1,5 +1,6 @@
import { useEffect, useState } from 'react';
import { Link } from 'react-router-dom';
import { ArrowRight, BookOpen, FileText } from 'lucide-react';
import SEOHead from '../components/seo/SEOHead';
import { useLanguage } from '../contexts/LanguageContext';
import { loadManifest, type ContentMeta } from '../lib/content';
@@ -17,16 +18,54 @@ export default function DocsListPage() {
return (
<>
<SEOHead title="Documentation" description="TexPixel documentation and guides" path="/docs" />
<div className="max-w-4xl mx-auto py-16 px-6">
<h1 className="text-3xl font-bold text-gray-900 mb-8">{language === 'en' ? 'Documentation' : '文档'}</h1>
<div className="max-w-5xl mx-auto py-16 lg:py-20 px-6">
{/* Header */}
<div className="mb-12">
<div className="inline-flex items-center gap-2 px-3 py-1.5 bg-sage-50 border border-sage-100 rounded-full text-xs font-medium text-sage-700 mb-6">
<BookOpen size={13} />
{language === 'en' ? 'Documentation' : '文档中心'}
</div>
<h1 className="font-display text-4xl lg:text-5xl font-bold text-ink tracking-tight mb-4">
{language === 'en' ? 'Learn TexPixel' : '了解 TexPixel'}
</h1>
<p className="text-ink-secondary text-lg max-w-xl">
{language === 'en'
? 'Everything you need to get started with formula recognition.'
: '公式识别入门所需的一切。'}
</p>
</div>
{/* Docs grid */}
<div className="space-y-4">
{docs.map((doc) => (
<Link key={doc.slug} to={`/docs/${doc.slug}`} className="block p-6 bg-gray-50 rounded-lg hover:bg-gray-100 transition-colors">
<h2 className="text-lg font-semibold text-gray-900">{doc.title}</h2>
<p className="text-gray-600 mt-1 text-sm">{doc.description}</p>
<Link
key={doc.slug}
to={`/docs/${doc.slug}`}
className="card p-6 flex items-start gap-5 group hover:border-sage-200 block"
>
<div className="w-11 h-11 bg-sage-50 rounded-xl flex items-center justify-center flex-shrink-0 group-hover:scale-110 transition-transform">
<FileText size={20} className="text-sage-600" />
</div>
<div className="flex-1 min-w-0">
<h2 className="font-display text-lg font-semibold text-ink mb-1 group-hover:text-sage-700 transition-colors">
{doc.title}
</h2>
<p className="text-ink-secondary text-sm leading-relaxed">{doc.description}</p>
</div>
<ArrowRight size={16} className="text-ink-muted group-hover:text-sage-600 transition-colors mt-1 flex-shrink-0" />
</Link>
))}
</div>
{/* Empty state */}
{docs.length === 0 && (
<div className="card p-12 text-center">
<p className="text-ink-muted text-sm">
{language === 'en' ? 'Documentation coming soon.' : '文档即将发布。'}
</p>
</div>
)}
</div>
</>
);

View File

@@ -2,7 +2,51 @@
export default {
content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
theme: {
extend: {},
extend: {
colors: {
coral: {
50: '#fef0ec',
100: '#fdddd5',
200: '#fbb9ab',
300: '#f48e78',
400: '#e86f54',
500: '#e05a33',
600: '#c94a28',
700: '#a53d22',
800: '#84331e',
900: '#6c2c1c',
},
ink: {
DEFAULT: '#1c1917',
secondary: '#57534e',
muted: '#a8a29e',
},
cream: {
50: '#fdfcfa',
100: '#faf8f4',
200: '#f5f3ee',
300: '#e7e5e4',
},
sage: {
50: '#eef9f7',
100: '#d5f0eb',
200: '#aee0d7',
300: '#7ccbbe',
400: '#4db3a4',
500: '#2a9d8f',
600: '#1f7d72',
700: '#1c655d',
},
},
fontFamily: {
display: ['"Plus Jakarta Sans"', 'system-ui', 'sans-serif'],
body: ['"DM Sans"', 'system-ui', 'sans-serif'],
},
borderRadius: {
'2xl': '1rem',
'3xl': '1.25rem',
},
},
},
plugins: [
require('@tailwindcss/typography'),