diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml
new file mode 100644
index 0000000..70644f3
--- /dev/null
+++ b/.github/workflows/pages.yml
@@ -0,0 +1,44 @@
+name: Build and deploy React portfolio
+on:
+ push:
+ branches: [main]
+ pull_request:
+ branches: [main]
+ workflow_dispatch:
+permissions:
+ contents: read
+concurrency:
+ group: pages-${{ github.ref }}
+ cancel-in-progress: true
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - uses: pnpm/action-setup@v4
+ with:
+ version: 10.15.1
+ - uses: actions/setup-node@v4
+ with:
+ node-version: 22
+ cache: pnpm
+ - run: pnpm install --frozen-lockfile
+ - run: pnpm test
+ - run: pnpm build
+ - uses: actions/upload-pages-artifact@v3
+ with:
+ path: dist
+ deploy:
+ if: github.ref == 'refs/heads/main' && github.event_name != 'pull_request'
+ needs: build
+ runs-on: ubuntu-latest
+ permissions:
+ pages: write
+ id-token: write
+ environment:
+ name: github-pages
+ url: ${{ steps.deployment.outputs.page_url }}
+ steps:
+ - uses: actions/configure-pages@v5
+ - uses: actions/deploy-pages@v4
+ id: deployment
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..46f4486
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,4 @@
+node_modules/
+dist/
+.env*
+!.env.example
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..9e8ea4c
--- /dev/null
+++ b/README.md
@@ -0,0 +1,80 @@
+# Bo Li · React Portfolio
+
+基于 React + Vite 的中英文个人主页,继续使用 GitHub Pages。默认中文,右上角切换英文。无需后端或付费托管服务。
+
+## 本地开发
+
+安装 Node.js 22.12+(或 Node.js 24)和 pnpm 10.15.1,然后在本目录运行:
+
+```sh
+pnpm install --frozen-lockfile
+pnpm dev
+```
+
+发布前执行 `pnpm test` 和 `pnpm build`。`pnpm preview` 可以预览 `dist/` 中的生产构建。不要直接双击 index.html。
+
+## 手动修改的位置
+
+| 内容 | 文件 |
+| ------------------------------------------------ | ----------------------------- |
+| 个人简介、联系方式、工作、教育、技能、中英文文案 | `src/data/profile.js` |
+| 展示项目、AI 视频、研究、更多项目 | `src/data/projects.js` |
+| 主题颜色、间距、移动端布局 | `src/styles.css` |
+| 页面区块顺序 | `src/App.jsx` |
+| 可复用项目卡片、导航、经历区块、标题 | `src/components/` |
+| 头像 | `public/images/bo-li.jpg` |
+| 最新 PDF 简历 | `public/resumes/` |
+| 视频、封面、字幕 | `public/media/` |
+| 自动构建和部署 | `.github/workflows/pages.yml` |
+
+文案用 `text('中文', 'English')` 管理,不需要在组件里查找简历内容。新增项目只需在 `projects` 数组添加记录,使用唯一 `id`。
+
+## 添加 AI 视频
+
+把视频和封面放入 `public/media/`,在 `src/data/projects.js` 中修改 AI 影像记录:
+
+```js
+{
+ id: 'video',
+ category: 'AI / MOTION',
+ title: text('我的作品标题', 'My film title'),
+ description: text('作品说明', 'Film description'),
+ tags: ['Generative AI', 'Video'],
+ visual: 'motion',
+ status: 'published',
+ media: {
+ src: '/media/my-film.mp4',
+ poster: '/media/my-film.jpg',
+ // 可选 WebVTT 字幕;仅在文件真实存在时填写。
+ // captions: '/media/my-film.zh.vtt',
+ // captionsLang: 'zh',
+ },
+ url: null,
+}
+```
+
+也可将 `media.src` 替换为 HTTPS 直链视频。YouTube/Bilibili 等播放页面链接放入 `url` 并保留 `media: null`,卡片会显示外链。视频使用浏览器原生控件、不自动播放、移动端内联播放、按需加载,播放失败提供原视频入口。包含对白时可添加字幕。
+
+当前 AI 卡片是预留位置,没有虚构作品或播放按钮。大型视频建议使用视频平台或对象存储,避免把大文件提交到 Git 仓库。
+
+## GitHub Pages 发布
+
+GitHub Pages 支持 React 的静态构建。此用户主页仓库部署在域名根目录,所以 Vite `base` 是 `/`。页面采用锚点导航,不依赖服务端路由,刷新不会出现 SPA 子路由 404。
+
+1. 在仓库 **Settings → Pages → Build and deployment → Source** 选择 **GitHub Actions**。
+2. 将重构分支合并到 `main`。PR 阶段仅构建检查;`main` 更新后自动发布 `dist/`。
+3. 在 Actions 中确认 `Build and deploy React portfolio` 成功,然后访问 https://li-bo-github.github.io/ 。
+
+如需撤回,revert 本次提交并恢复原来的 Pages 发布来源设置。旧 MDB 的 css/js/scss 和 License.pdf 保留在仓库中用于历史兼容及版权说明,但不会进入新的 Vite 构建。
+
+## 内容来源
+
+- 工作与教育信息以用户提供的《简历中文 - 李博.pdf》为主要来源,《resume bo 中英.pdf》补充英文表达。
+- Amazon 2025 的工作终止日期为 2025.12,没有标为在职;跨境事件采用中文版的 40,000+ 汇总口径。
+- Tharzen、ClaimTrust、其他研究和早期项目保留自原主页,并保留已有代码链接。
+- 下载入口提供用户提交的原始 PDF;两份 PDF 中电话号码和内容细节存在差异,文件未被改写。页面联系方式采用两份简历一致的邮箱。
+- 公共网站没有后端上传功能。手动修改数据、添加媒体、提交到 main 即可更新。
+
+## 验证范围
+
+`pnpm test` 检查双语字段、项目 ID、链接协议和静态资源路径;`pnpm build` 检查 React/Vite 生产构建。未提供真实视频,因此发布后的真实媒体还需自行确认浏览器解码、外链访问和字幕同步。
diff --git a/index.html b/index.html
index 9923fe6..95bbf87 100644
--- a/index.html
+++ b/index.html
@@ -1,588 +1,20 @@
-
-
+
+
-
-
- Bo Li Profolio
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+ 李博 Bo Li — 软件工程师 · AI 探索者
-
-
-
-
-
-
-
-
-
-
-
-
-
- Bo Li
-
-
-
-
-
-
-
- ABOUT
-
- Hi, I'm interested in the field of supervised learning and autonomous driving.
- I am planing to work as software engineer after graduate.
-
- Resume
-
-
-
-
-
-
-
-
-
-
-
- EDUCATION
-
-
-
-
-
-
-
-
-
-
Master of Computer science
-
University of Illinois Urbana-Champaign, Jan 2024 - May 2025
-
-
-
-
-
-
-
B.S. Computer Science, Data Science
-
University of Wisconsin-Madison, Sep 2019 - May 2023
-
-
-
-
-
-
-
-
-
- EXPERIENCE
-
-
-
-
-
-
-
-
-
-
Tharzen - Software Engineer Internship
-
May 2024 - Aug 2024
-
- Designed and developed a reversible PHP interpreter based on bidirectional evaluation, leading Test-Driven Development (TDD) to ensure robust feature implementation and comprehensive testing.
- Implemented interpreter functionality using Rust classes and traits to enhance the core logic and maintainability of the reversible PHP interpreter.
-
-
-
-
-
-
-
-
Spark Education Group - Software Engineer Internship
-
Aug 2023 - Dec 2023
-
- Designed and developed a Python application utilizing longchain embedding, chat completion, and vector store, allowing users to interact with the project codebase effectively.
- Led the deployment of a large language model platform using React, Node.js, Python, FastAPI, and PostgreSQL, integrating new features such as user statistics, top prompts display, and custom masks.
- Drove database and API enhancements, contributing to a 30% increase in weekly active users and achieving a peak of 185 daily active users.
-
-
-
-
-
-
-
-
Amazon - Software Engineer Internship
-
May 2022 - Aug 2022
-
- Designed and implemented the backend API for the changelog function using Node.js and GraphQL, enhancing user interaction with changelog data based on permissions.
- Created GraphQL schema, field resolver, and record resolver for seamless data mapping and CRUD operations, ensuring high performance and scalability.
- Managed the end-to-end development cycle, from design documents and UI wireframes to code implementation, code review, and deployment.
-
-
-
-
-
-
-
-
-
-
- RESEARCH
-
-
-
-
-
-
-
-
-
-
-
Professor: Heng Ji, Aug 2024 - Dec 2024
-
- Developed a graph-based trust scoring algorithm to evaluate document reliability, improving RAG system response quality by 11.2%.
- Processed 814 articles to extract claims and classify relationships for trust propagation.
- Optimized claim extraction and relationship classification, reducing computational complexity from O(n²) to O(n).
-
-
-
-
-
-
-
-
Simulation of the Collaborative Automated Driving Systems
-
Professor: Bin Ran, Feb 2022 - Dec 2022
-
- working with the prediction group focusing on trajectory prediction of autonomous driving cars and investigating trajectory prediction methods for the project.
- Generating status info of vehicles from Carla simulator in the format that can be used for future tasks as the data for fitting trajectory prediction models.
- Contributed to the unification of simulator export data format and model input data format.
-
-
-
-
-
-
-
-
-
Offloading pushdown db onto SmartNIC based JBOF device
-
Professor: Ming Liu , Oct 2021 - Dec 2021
-
- Customize FlexPushdownDB developed based on C++ hybrid with caching and pushdown capabilities and weighted cache policy from AWS cloud to JBOF device.
-
-
-
-
-
-
-
-
-
- PROJECTS
-
-
-
-
-
-
-
-
-
-
-
algorithm developer
-
- Designed and implemented image recognition and calibration transposition algorithms using lidar and camera sensors to enhance parking system accuracy.
- Developed and deployed an MPC trajectory tracker and hybrid A* route planning for autonomous vehicle navigation.
-
-
-
-
-
-
-
-
Cloud-Based IoT Infrastructure for Vehicle Monitoring
-
AWS developer
-
- Designed and deployed scalable IoT infrastructure using AWS IoT Core, AWS GreenGrass, and AWS IoT Analytics to monitor data from a fleet of 120,000 simulated vehicles.
- Implemented AWS Lambda functions to analyze CO2 emissions and return diagnostics, utilizing the publish-subscribe MQTT model for seamless device communication.
- Built data visualization pipelines with AWS Firehose, AWS Glue, and AWS Athena, leveraging AWS Sagemaker Jupyter Notebooks for insights.
-
-
-
-
-
-
-
-
-
Java programmer
-
- Designed the system and user interface using JavaFX and CSS.
- Utilizing java packages processing data from csv documents for computing and visualizing.
- Allows users to search data from the database on different categories, displaying the data by farm report, annual report, monthly report, and date Range report with percent of total weight.
-
-
-
-
-
-
-
-
-
-
Python programmer
-
- Custom distance functions based on address and joint account information using scikit-learn hierarchical clustering, and access data from Snowflake flatform.
- The cost of the model using i7-9700 CPUis exponential 1.21 seconds for 1000 records and 43.09 seconds for 10,000 records, and the highest accuracy of the model is 96.6% with different distance threshold.
- Write a distance clustering function to weight address clustering method, utilize shared account relationship columns in clustering function, and test functionality of the project.
-
-
-
-
-
-
-
-
-
-
Data analysis
-
- Utilized Python to develop web crawlers for collecting data from online databases.
- Preprocessed data by slicing, reformatting, and implementing a pipeline with StandardScaler transformer.
- Analyzed data using PCA and linear regression methods from the PyTorch library.
-
-
-
-
-
-
-
-
-
-
Web develope
-
- Maintained and updated the website for the Chinese Students and Scholars Association at UW-Madison.
- Implemented new features and functionality based on user requirements.
- Collected and analyzed data from blog and tweet sources. Extracted relevant insights and trends from the data for further analysis.
-
-
-
-
-
-
-
-
-
- SKILLS
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+ 请启用 JavaScript 浏览主页。Please enable JavaScript. Contact: libo.tom.mail@gmail.com
+
diff --git a/package.json b/package.json
index b00d481..ca47c01 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,21 @@
{
- "name": "mdb-standard-app",
- "version": "6.2.0",
- "homepage": "https://mdbootstrap.com/docs/standard/",
- "license": "MIT"
+ "name": "bo-li-portfolio",
+ "version": "1.0.0",
+ "private": true,
+ "type": "module",
+ "packageManager": "pnpm@10.15.1",
+ "scripts": {
+ "dev": "vite --host 127.0.0.1",
+ "build": "vite build",
+ "preview": "vite preview --host 127.0.0.1",
+ "test": "node --test tests/*.test.js"
+ },
+ "dependencies": {
+ "react": "^19.1.1",
+ "react-dom": "^19.1.1"
+ },
+ "devDependencies": {
+ "vite": "^7.1.5",
+ "@vitejs/plugin-react": "^5.0.2"
+ }
}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
new file mode 100644
index 0000000..fa61ad4
--- /dev/null
+++ b/pnpm-lock.yaml
@@ -0,0 +1,1108 @@
+lockfileVersion: '9.0'
+
+settings:
+ autoInstallPeers: true
+ excludeLinksFromLockfile: false
+
+importers:
+
+ .:
+ dependencies:
+ react:
+ specifier: ^19.1.1
+ version: 19.2.8
+ react-dom:
+ specifier: ^19.1.1
+ version: 19.2.8(react@19.2.8)
+ devDependencies:
+ '@vitejs/plugin-react':
+ specifier: ^5.0.2
+ version: 5.2.0(vite@7.3.6)
+ vite:
+ specifier: ^7.1.5
+ version: 7.3.6
+
+packages:
+
+ '@babel/code-frame@7.29.7':
+ resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/compat-data@7.29.7':
+ resolution: {integrity: sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/core@7.29.7':
+ resolution: {integrity: sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/generator@7.29.8':
+ resolution: {integrity: sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-compilation-targets@7.29.7':
+ resolution: {integrity: sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-globals@7.29.7':
+ resolution: {integrity: sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-module-imports@7.29.7':
+ resolution: {integrity: sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-module-transforms@7.29.7':
+ resolution: {integrity: sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0
+
+ '@babel/helper-plugin-utils@7.29.7':
+ resolution: {integrity: sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-string-parser@7.29.7':
+ resolution: {integrity: sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-validator-identifier@7.29.7':
+ resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-validator-option@7.29.7':
+ resolution: {integrity: sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helpers@7.29.7':
+ resolution: {integrity: sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/parser@7.29.8':
+ resolution: {integrity: sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==}
+ engines: {node: '>=6.0.0'}
+ hasBin: true
+
+ '@babel/plugin-transform-react-jsx-self@7.29.7':
+ resolution: {integrity: sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-react-jsx-source@7.29.7':
+ resolution: {integrity: sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/template@7.29.7':
+ resolution: {integrity: sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/traverse@7.29.8':
+ resolution: {integrity: sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/types@7.29.8':
+ resolution: {integrity: sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==}
+ engines: {node: '>=6.9.0'}
+
+ '@esbuild/aix-ppc64@0.28.2':
+ resolution: {integrity: sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==}
+ engines: {node: '>=18'}
+ cpu: [ppc64]
+ os: [aix]
+
+ '@esbuild/android-arm64@0.28.2':
+ resolution: {integrity: sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [android]
+
+ '@esbuild/android-arm@0.28.2':
+ resolution: {integrity: sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==}
+ engines: {node: '>=18'}
+ cpu: [arm]
+ os: [android]
+
+ '@esbuild/android-x64@0.28.2':
+ resolution: {integrity: sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [android]
+
+ '@esbuild/darwin-arm64@0.28.2':
+ resolution: {integrity: sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@esbuild/darwin-x64@0.28.2':
+ resolution: {integrity: sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [darwin]
+
+ '@esbuild/freebsd-arm64@0.28.2':
+ resolution: {integrity: sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [freebsd]
+
+ '@esbuild/freebsd-x64@0.28.2':
+ resolution: {integrity: sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [freebsd]
+
+ '@esbuild/linux-arm64@0.28.2':
+ resolution: {integrity: sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [linux]
+
+ '@esbuild/linux-arm@0.28.2':
+ resolution: {integrity: sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==}
+ engines: {node: '>=18'}
+ cpu: [arm]
+ os: [linux]
+
+ '@esbuild/linux-ia32@0.28.2':
+ resolution: {integrity: sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==}
+ engines: {node: '>=18'}
+ cpu: [ia32]
+ os: [linux]
+
+ '@esbuild/linux-loong64@0.28.2':
+ resolution: {integrity: sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==}
+ engines: {node: '>=18'}
+ cpu: [loong64]
+ os: [linux]
+
+ '@esbuild/linux-mips64el@0.28.2':
+ resolution: {integrity: sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==}
+ engines: {node: '>=18'}
+ cpu: [mips64el]
+ os: [linux]
+
+ '@esbuild/linux-ppc64@0.28.2':
+ resolution: {integrity: sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==}
+ engines: {node: '>=18'}
+ cpu: [ppc64]
+ os: [linux]
+
+ '@esbuild/linux-riscv64@0.28.2':
+ resolution: {integrity: sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==}
+ engines: {node: '>=18'}
+ cpu: [riscv64]
+ os: [linux]
+
+ '@esbuild/linux-s390x@0.28.2':
+ resolution: {integrity: sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==}
+ engines: {node: '>=18'}
+ cpu: [s390x]
+ os: [linux]
+
+ '@esbuild/linux-x64@0.28.2':
+ resolution: {integrity: sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [linux]
+
+ '@esbuild/netbsd-arm64@0.28.2':
+ resolution: {integrity: sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [netbsd]
+
+ '@esbuild/netbsd-x64@0.28.2':
+ resolution: {integrity: sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [netbsd]
+
+ '@esbuild/openbsd-arm64@0.28.2':
+ resolution: {integrity: sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [openbsd]
+
+ '@esbuild/openbsd-x64@0.28.2':
+ resolution: {integrity: sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [openbsd]
+
+ '@esbuild/openharmony-arm64@0.28.2':
+ resolution: {integrity: sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [openharmony]
+
+ '@esbuild/sunos-x64@0.28.2':
+ resolution: {integrity: sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [sunos]
+
+ '@esbuild/win32-arm64@0.28.2':
+ resolution: {integrity: sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [win32]
+
+ '@esbuild/win32-ia32@0.28.2':
+ resolution: {integrity: sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==}
+ engines: {node: '>=18'}
+ cpu: [ia32]
+ os: [win32]
+
+ '@esbuild/win32-x64@0.28.2':
+ resolution: {integrity: sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [win32]
+
+ '@jridgewell/gen-mapping@0.3.13':
+ resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==}
+
+ '@jridgewell/remapping@2.3.5':
+ resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==}
+
+ '@jridgewell/resolve-uri@3.1.2':
+ resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==}
+ engines: {node: '>=6.0.0'}
+
+ '@jridgewell/sourcemap-codec@1.6.0':
+ resolution: {integrity: sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==}
+
+ '@jridgewell/trace-mapping@0.3.31':
+ resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==}
+
+ '@napi-rs/lzma-linux-x64-gnu@1.5.1':
+ resolution: {integrity: sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==}
+ engines: {node: ^22.20 || ^24.12 || >=25}
+ cpu: [x64]
+ os: [linux]
+ libc: [glibc]
+
+ '@rolldown/pluginutils@1.0.0-rc.3':
+ resolution: {integrity: sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q==}
+
+ '@rollup/rollup-android-arm-eabi@4.63.1':
+ resolution: {integrity: sha512-UZ8sUxPTiHWYX9QNdJedb1kDZSpS1t/VPWBWGSgqHNi9w3Cu6IXvu2mzbhiTiPvtrqgTQJ+zqiAq2iPIPilpaQ==}
+ cpu: [arm]
+ os: [android]
+
+ '@rollup/rollup-android-arm64@4.63.1':
+ resolution: {integrity: sha512-cQ4nFQABN5cDvDpbvJ7bMStCpnaVxynZrRMfUJYgxcIk9Sh54FIO1vtfkg0B69REjER77ioZ/ov+eAApx/KmLQ==}
+ cpu: [arm64]
+ os: [android]
+
+ '@rollup/rollup-darwin-arm64@4.63.1':
+ resolution: {integrity: sha512-FQNqd1lRy/0QhDk3xeRIkSBiCpXCiDnZO3YLVdcDKN1UBiKToNftCzcXYNLshmPDUMlu2TdeS8tGcsU6f3YF1Q==}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@rollup/rollup-darwin-x64@4.63.1':
+ resolution: {integrity: sha512-pvD16V939D3CloK0+qikpGaxiPrDUXTe7Y5cWOMkMSy7m1cawa8EGy/kXYi/G/cKAC4HDAbSnzCIk1WmsoOKXg==}
+ cpu: [x64]
+ os: [darwin]
+
+ '@rollup/rollup-freebsd-arm64@4.63.1':
+ resolution: {integrity: sha512-pcFGeL2345VwdTnJhA6zLbew+YgWB0qBG2+dMtXjCicf6+rm6kO6cOoh5VnTe0ZMrMRgRyuHmCJxZWrIdzYuOw==}
+ cpu: [arm64]
+ os: [freebsd]
+
+ '@rollup/rollup-freebsd-x64@4.63.1':
+ resolution: {integrity: sha512-mRJlqSRulVzcKq/LKA6ICSIc3K/l4fzlVn/gePn2nXIHy8seRi5z/eeRE0d/XMBxcMldiXtQTSpRj0tkkC3g8Q==}
+ cpu: [x64]
+ os: [freebsd]
+
+ '@rollup/rollup-linux-arm-gnueabihf@4.63.1':
+ resolution: {integrity: sha512-YDUNvVM85TI3g/1OpnqKP1h4NeW/j64DfWMf+G3M809xNk1bJSnpFp4sh83NpmVE5DXnkh8ULor4LTVZKoYLHw==}
+ cpu: [arm]
+ os: [linux]
+ libc: [glibc]
+
+ '@rollup/rollup-linux-arm-musleabihf@4.63.1':
+ resolution: {integrity: sha512-7Mcn71p9ZuQFAj+h+dhQXy/yeLePRS2yKRnmW1DijA9thKO5qap0GNOIQK4yQ6iP3SU0Mrb/yWo8h8vgRba8lw==}
+ cpu: [arm]
+ os: [linux]
+ libc: [musl]
+
+ '@rollup/rollup-linux-arm64-gnu@4.63.1':
+ resolution: {integrity: sha512-4YiLQTX6U4CSl0L9cluep9A9W6UmTfqBDc2/CH6wlu54pl4E7Jn3cOD8oxzvBDEGk/JMKgJ47C8g+radF7mwvg==}
+ cpu: [arm64]
+ os: [linux]
+ libc: [glibc]
+
+ '@rollup/rollup-linux-arm64-musl@4.63.1':
+ resolution: {integrity: sha512-2ra8F7w8OquwZN9z2/fKFnli69wa8PLwaVzRMIPGb13ByMJwC28Fbp8YcVGoUhlYMTt7j5j9bNgpysrN2UM+vw==}
+ cpu: [arm64]
+ os: [linux]
+ libc: [musl]
+
+ '@rollup/rollup-linux-loong64-gnu@4.63.1':
+ resolution: {integrity: sha512-Sy20ncyhjmBP0Ml+UvQbimjlk6VFgjW5uNP+qqwHB00mTE8Bl2C1TuHTlRwK2YoXeZbee5lP2XevBWVkAQAtSQ==}
+ cpu: [loong64]
+ os: [linux]
+ libc: [glibc]
+
+ '@rollup/rollup-linux-loong64-musl@4.63.1':
+ resolution: {integrity: sha512-noITLp8oNjYliPnGWmLyelIHwULGqbHloQHGw1rtxbWhTuWooRpnZarZQJ1y9EUC4szuCusCc+HEpUtxpIwYvA==}
+ cpu: [loong64]
+ os: [linux]
+ libc: [musl]
+
+ '@rollup/rollup-linux-ppc64-gnu@4.63.1':
+ resolution: {integrity: sha512-hlxxXd+F1mWiAcaFR7Sv9ZQT6m6UfI8+Vy/kFJzztq2pDMU/0wZ9sish0iszNZvsQDo8Gc0i5yuFEOz5dDf6fA==}
+ cpu: [ppc64]
+ os: [linux]
+ libc: [glibc]
+
+ '@rollup/rollup-linux-ppc64-musl@4.63.1':
+ resolution: {integrity: sha512-EF7OpqQTQ/BvGqLzUi4rEHuagCV9MugAUXSHemwPW5vxZ75RR+jxO/2j95Ph2dalMpFHSVECjRoioHZgA9zOYA==}
+ cpu: [ppc64]
+ os: [linux]
+ libc: [musl]
+
+ '@rollup/rollup-linux-riscv64-gnu@4.63.1':
+ resolution: {integrity: sha512-wQO3JesW9PRkwlabQ27y7sPfVOOTLRG73I4F2UYHG5PXun3J9U3y+b7ezVKSYbsvSKGQ1k1cq8Qlun4C9kLt3w==}
+ cpu: [riscv64]
+ os: [linux]
+ libc: [glibc]
+
+ '@rollup/rollup-linux-riscv64-musl@4.63.1':
+ resolution: {integrity: sha512-ouAGwhO6wHRXdnOVCOsB0tRFkA7nhNB2Nwax6oECXN0YiN8EYUTBAOudADOB1PI+yDL61TeNx/u7MVCzksNbkQ==}
+ cpu: [riscv64]
+ os: [linux]
+ libc: [musl]
+
+ '@rollup/rollup-linux-s390x-gnu@4.63.1':
+ resolution: {integrity: sha512-q2R38Sn+1J8RxhfJ+T54wSWmyKXWec+9jgDfqO2AtArEqHO5R2aeayp5H5OYLr5UYDVGsVaZPEFUooMhYCdz5A==}
+ cpu: [s390x]
+ os: [linux]
+ libc: [glibc]
+
+ '@rollup/rollup-linux-x64-gnu@4.63.1':
+ resolution: {integrity: sha512-gfI5T24WLLuFfSKw7Go/zDXjAAV0fny0swTaDv+WjK7vqcw4cRhFfdsyKL1n+ukI+ooBxn3bVQnyrn06WpI50w==}
+ cpu: [x64]
+ os: [linux]
+ libc: [glibc]
+
+ '@rollup/rollup-linux-x64-musl@4.63.1':
+ resolution: {integrity: sha512-4h6XqthmB4Hspji84wvgk+ElodTsGj+dbZqHJHHtKxj4mYq0ANSEEPX9ys3moJueqsRjwpaJYH7874Itwnj2ow==}
+ cpu: [x64]
+ os: [linux]
+ libc: [musl]
+
+ '@rollup/rollup-openbsd-x64@4.63.1':
+ resolution: {integrity: sha512-dlfCOa87o1VAYegLQ9EKilx2JCeRofiyPGhTCmqnuXZ6bMPiycO1rq1+sKoulAp7pGLIsTIw+1x5R+zgh5LhhA==}
+ cpu: [x64]
+ os: [openbsd]
+
+ '@rollup/rollup-openharmony-arm64@4.63.1':
+ resolution: {integrity: sha512-cjkLbOlfcm3QGhMM1J5zaZjsw1GggbN6rw9UTSSRrPrR1KkcXnN7Uq9rPw34xImQ9VOY9GN+6u2Zj80B9ptkcw==}
+ cpu: [arm64]
+ os: [openharmony]
+
+ '@rollup/rollup-win32-arm64-msvc@4.63.1':
+ resolution: {integrity: sha512-Li1KdUnWGE4N3e1F/B4RTB1ms+nG4WBgjByO46pkeBVX/2UBsY53xf5vK9WygVmnH3RwncIST7lkSdLSY6P9lg==}
+ cpu: [arm64]
+ os: [win32]
+
+ '@rollup/rollup-win32-ia32-msvc@4.63.1':
+ resolution: {integrity: sha512-t4ZYOSoLTgwhuFMrmTMLx/+i1DQVK7HYqMc6kY46EApwi8X0nIVphzdNoThU3xt6n+N5urG1/gxBdCaKDLavfg==}
+ cpu: [ia32]
+ os: [win32]
+
+ '@rollup/rollup-win32-x64-gnu@4.63.1':
+ resolution: {integrity: sha512-RgroPfMmKlD1RzSDxvwgcPiy2HNQKoYV7OmwIXDsk73uKW5t6B/V8KIy27SMv/FNXFo/oSBtWc9J0X7t91ezZg==}
+ cpu: [x64]
+ os: [win32]
+
+ '@rollup/rollup-win32-x64-msvc@4.63.1':
+ resolution: {integrity: sha512-at8QVep6S3h5Y6gSbdGU06bRY5WJkf6WUduM9YtvYMbYhB1MOFfUgc6kehitQXzOtMSaT70q7f9ydPhpqu821w==}
+ cpu: [x64]
+ os: [win32]
+
+ '@types/babel__core@7.20.5':
+ resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==}
+
+ '@types/babel__generator@7.27.0':
+ resolution: {integrity: sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==}
+
+ '@types/babel__template@7.4.4':
+ resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==}
+
+ '@types/babel__traverse@7.28.0':
+ resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==}
+
+ '@types/estree@1.0.9':
+ resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==}
+
+ '@vitejs/plugin-react@5.2.0':
+ resolution: {integrity: sha512-YmKkfhOAi3wsB1PhJq5Scj3GXMn3WvtQ/JC0xoopuHoXSdmtdStOpFrYaT1kie2YgFBcIe64ROzMYRjCrYOdYw==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ peerDependencies:
+ vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0
+
+ baseline-browser-mapping@2.11.21:
+ resolution: {integrity: sha512-uh8vpY/1/YyFkunIDFH/12p7/7VdPKA1hejMVEbdkEaWnUz0Hesvx5EbiU6XxjyHZIOju+ZMbQJkRh+es3/spQ==}
+ engines: {node: '>=6.0.0'}
+ hasBin: true
+
+ browserslist@4.28.9:
+ resolution: {integrity: sha512-EWazOblFYUvlGZcfGhPUPmYh3nikUxBVb+y9MJun5f3hBi812X+8MSQTujLBtgK3cf51fJWbWfOjyeO954d+Eg==}
+ engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
+ hasBin: true
+
+ caniuse-lite@1.0.30001810:
+ resolution: {integrity: sha512-TITQPUkaz+aVk5GL6NhOdwk1aEaNTSDPsGFWrTuhKGtjTF70jL/Oht2W4c6rXUe5fu7Ie19VIahAXHIIiWWNeg==}
+
+ convert-source-map@2.0.0:
+ resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
+
+ debug@4.4.3:
+ resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==}
+ engines: {node: '>=6.0'}
+ peerDependencies:
+ supports-color: '*'
+ peerDependenciesMeta:
+ supports-color:
+ optional: true
+
+ electron-to-chromium@1.5.423:
+ resolution: {integrity: sha512-rRZfTSY8ptHYMQxa+uIycJMFKmY1T0GIApNMXJYGehguTZa56TEEl19pKPCoBqk5Gpf7QizZn/jt7xur+DYxag==}
+
+ esbuild@0.28.2:
+ resolution: {integrity: sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==}
+ engines: {node: '>=18'}
+ hasBin: true
+
+ escalade@3.2.0:
+ resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==}
+ engines: {node: '>=6'}
+
+ fdir@6.5.0:
+ resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==}
+ engines: {node: '>=12.0.0'}
+ peerDependencies:
+ picomatch: ^3 || ^4
+ peerDependenciesMeta:
+ picomatch:
+ optional: true
+
+ fsevents@2.3.3:
+ resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
+ engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
+ os: [darwin]
+
+ gensync@1.0.0-beta.2:
+ resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==}
+ engines: {node: '>=6.9.0'}
+
+ js-tokens@4.0.0:
+ resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
+
+ jsesc@3.1.0:
+ resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==}
+ engines: {node: '>=6'}
+ hasBin: true
+
+ json5@2.2.3:
+ resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==}
+ engines: {node: '>=6'}
+ hasBin: true
+
+ lru-cache@5.1.1:
+ resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
+
+ ms@2.1.3:
+ resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
+
+ nanoid@3.3.18:
+ resolution: {integrity: sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==}
+ engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
+ hasBin: true
+
+ node-releases@2.0.54:
+ resolution: {integrity: sha512-YHs7BmmcsdAI5Ozuf8JZo6PT0mv2GIWC9vMfvUC3dp65M8hn7Ux8CPL+2oBI7juNuj9d0ndhTcznq2ODBps9cQ==}
+ engines: {node: '>=18'}
+
+ picocolors@1.1.1:
+ resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
+
+ picomatch@4.0.7:
+ resolution: {integrity: sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==}
+ engines: {node: '>=12'}
+
+ postcss@8.5.28:
+ resolution: {integrity: sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==}
+ engines: {node: ^10 || ^12 || >=14}
+
+ react-dom@19.2.8:
+ resolution: {integrity: sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ==}
+ peerDependencies:
+ react: ^19.2.8
+
+ react-refresh@0.18.0:
+ resolution: {integrity: sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==}
+ engines: {node: '>=0.10.0'}
+
+ react@19.2.8:
+ resolution: {integrity: sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==}
+ engines: {node: '>=0.10.0'}
+
+ rollup@4.63.1:
+ resolution: {integrity: sha512-3Df9jsstwhccuEfmAMi9l8XUh/GOkVObmFTU7CCVBysEbcOZLl84jCtaAZMcPiMz2EGKsATzQcU+Xr3n/wU6cg==}
+ engines: {node: '>=18.0.0', npm: '>=8.0.0'}
+ hasBin: true
+
+ scheduler@0.27.0:
+ resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==}
+
+ semver@6.3.1:
+ resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==}
+ hasBin: true
+
+ source-map-js@1.2.1:
+ resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
+ engines: {node: '>=0.10.0'}
+
+ tinyglobby@0.2.17:
+ resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==}
+ engines: {node: '>=12.0.0'}
+
+ update-browserslist-db@1.3.2:
+ resolution: {integrity: sha512-UQ+MSxlhRm1bzjhU+DcuXfjFO1FzNtqhK5+9Yvlp90ItDLk5vT932A0rFu619nf7RVS+Y/VeaUW1jaRDqZ8VJw==}
+ hasBin: true
+ peerDependencies:
+ browserslist: '>= 4.21.0'
+
+ vite@7.3.6:
+ resolution: {integrity: sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ hasBin: true
+ peerDependencies:
+ '@types/node': ^20.19.0 || >=22.12.0
+ jiti: '>=1.21.0'
+ less: ^4.0.0
+ lightningcss: ^1.21.0
+ sass: ^1.70.0
+ sass-embedded: ^1.70.0
+ stylus: '>=0.54.8'
+ sugarss: ^5.0.0
+ terser: ^5.16.0
+ tsx: ^4.8.1
+ yaml: ^2.4.2
+ peerDependenciesMeta:
+ '@types/node':
+ optional: true
+ jiti:
+ optional: true
+ less:
+ optional: true
+ lightningcss:
+ optional: true
+ sass:
+ optional: true
+ sass-embedded:
+ optional: true
+ stylus:
+ optional: true
+ sugarss:
+ optional: true
+ terser:
+ optional: true
+ tsx:
+ optional: true
+ yaml:
+ optional: true
+
+ yallist@3.1.1:
+ resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==}
+
+snapshots:
+
+ '@babel/code-frame@7.29.7':
+ dependencies:
+ '@babel/helper-validator-identifier': 7.29.7
+ js-tokens: 4.0.0
+ picocolors: 1.1.1
+
+ '@babel/compat-data@7.29.7': {}
+
+ '@babel/core@7.29.7':
+ dependencies:
+ '@babel/code-frame': 7.29.7
+ '@babel/generator': 7.29.8
+ '@babel/helper-compilation-targets': 7.29.7
+ '@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7)
+ '@babel/helpers': 7.29.7
+ '@babel/parser': 7.29.8
+ '@babel/template': 7.29.7
+ '@babel/traverse': 7.29.8
+ '@babel/types': 7.29.8
+ '@jridgewell/remapping': 2.3.5
+ convert-source-map: 2.0.0
+ debug: 4.4.3
+ gensync: 1.0.0-beta.2
+ json5: 2.2.3
+ semver: 6.3.1
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/generator@7.29.8':
+ dependencies:
+ '@babel/parser': 7.29.8
+ '@babel/types': 7.29.8
+ '@jridgewell/gen-mapping': 0.3.13
+ '@jridgewell/trace-mapping': 0.3.31
+ jsesc: 3.1.0
+
+ '@babel/helper-compilation-targets@7.29.7':
+ dependencies:
+ '@babel/compat-data': 7.29.7
+ '@babel/helper-validator-option': 7.29.7
+ browserslist: 4.28.9
+ lru-cache: 5.1.1
+ semver: 6.3.1
+
+ '@babel/helper-globals@7.29.7': {}
+
+ '@babel/helper-module-imports@7.29.7':
+ dependencies:
+ '@babel/traverse': 7.29.8
+ '@babel/types': 7.29.8
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/helper-module-transforms@7.29.7(@babel/core@7.29.7)':
+ dependencies:
+ '@babel/core': 7.29.7
+ '@babel/helper-module-imports': 7.29.7
+ '@babel/helper-validator-identifier': 7.29.7
+ '@babel/traverse': 7.29.8
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/helper-plugin-utils@7.29.7': {}
+
+ '@babel/helper-string-parser@7.29.7': {}
+
+ '@babel/helper-validator-identifier@7.29.7': {}
+
+ '@babel/helper-validator-option@7.29.7': {}
+
+ '@babel/helpers@7.29.7':
+ dependencies:
+ '@babel/template': 7.29.7
+ '@babel/types': 7.29.8
+
+ '@babel/parser@7.29.8':
+ dependencies:
+ '@babel/types': 7.29.8
+
+ '@babel/plugin-transform-react-jsx-self@7.29.7(@babel/core@7.29.7)':
+ dependencies:
+ '@babel/core': 7.29.7
+ '@babel/helper-plugin-utils': 7.29.7
+
+ '@babel/plugin-transform-react-jsx-source@7.29.7(@babel/core@7.29.7)':
+ dependencies:
+ '@babel/core': 7.29.7
+ '@babel/helper-plugin-utils': 7.29.7
+
+ '@babel/template@7.29.7':
+ dependencies:
+ '@babel/code-frame': 7.29.7
+ '@babel/parser': 7.29.8
+ '@babel/types': 7.29.8
+
+ '@babel/traverse@7.29.8':
+ dependencies:
+ '@babel/code-frame': 7.29.7
+ '@babel/generator': 7.29.8
+ '@babel/helper-globals': 7.29.7
+ '@babel/parser': 7.29.8
+ '@babel/template': 7.29.7
+ '@babel/types': 7.29.8
+ debug: 4.4.3
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/types@7.29.8':
+ dependencies:
+ '@babel/helper-string-parser': 7.29.7
+ '@babel/helper-validator-identifier': 7.29.7
+
+ '@esbuild/aix-ppc64@0.28.2':
+ optional: true
+
+ '@esbuild/android-arm64@0.28.2':
+ optional: true
+
+ '@esbuild/android-arm@0.28.2':
+ optional: true
+
+ '@esbuild/android-x64@0.28.2':
+ optional: true
+
+ '@esbuild/darwin-arm64@0.28.2':
+ optional: true
+
+ '@esbuild/darwin-x64@0.28.2':
+ optional: true
+
+ '@esbuild/freebsd-arm64@0.28.2':
+ optional: true
+
+ '@esbuild/freebsd-x64@0.28.2':
+ optional: true
+
+ '@esbuild/linux-arm64@0.28.2':
+ optional: true
+
+ '@esbuild/linux-arm@0.28.2':
+ optional: true
+
+ '@esbuild/linux-ia32@0.28.2':
+ optional: true
+
+ '@esbuild/linux-loong64@0.28.2':
+ optional: true
+
+ '@esbuild/linux-mips64el@0.28.2':
+ optional: true
+
+ '@esbuild/linux-ppc64@0.28.2':
+ optional: true
+
+ '@esbuild/linux-riscv64@0.28.2':
+ optional: true
+
+ '@esbuild/linux-s390x@0.28.2':
+ optional: true
+
+ '@esbuild/linux-x64@0.28.2':
+ optional: true
+
+ '@esbuild/netbsd-arm64@0.28.2':
+ optional: true
+
+ '@esbuild/netbsd-x64@0.28.2':
+ optional: true
+
+ '@esbuild/openbsd-arm64@0.28.2':
+ optional: true
+
+ '@esbuild/openbsd-x64@0.28.2':
+ optional: true
+
+ '@esbuild/openharmony-arm64@0.28.2':
+ optional: true
+
+ '@esbuild/sunos-x64@0.28.2':
+ optional: true
+
+ '@esbuild/win32-arm64@0.28.2':
+ optional: true
+
+ '@esbuild/win32-ia32@0.28.2':
+ optional: true
+
+ '@esbuild/win32-x64@0.28.2':
+ optional: true
+
+ '@jridgewell/gen-mapping@0.3.13':
+ dependencies:
+ '@jridgewell/sourcemap-codec': 1.6.0
+ '@jridgewell/trace-mapping': 0.3.31
+
+ '@jridgewell/remapping@2.3.5':
+ dependencies:
+ '@jridgewell/gen-mapping': 0.3.13
+ '@jridgewell/trace-mapping': 0.3.31
+
+ '@jridgewell/resolve-uri@3.1.2': {}
+
+ '@jridgewell/sourcemap-codec@1.6.0': {}
+
+ '@jridgewell/trace-mapping@0.3.31':
+ dependencies:
+ '@jridgewell/resolve-uri': 3.1.2
+ '@jridgewell/sourcemap-codec': 1.6.0
+
+ '@napi-rs/lzma-linux-x64-gnu@1.5.1':
+ optional: true
+
+ '@rolldown/pluginutils@1.0.0-rc.3': {}
+
+ '@rollup/rollup-android-arm-eabi@4.63.1':
+ optional: true
+
+ '@rollup/rollup-android-arm64@4.63.1':
+ optional: true
+
+ '@rollup/rollup-darwin-arm64@4.63.1':
+ optional: true
+
+ '@rollup/rollup-darwin-x64@4.63.1':
+ optional: true
+
+ '@rollup/rollup-freebsd-arm64@4.63.1':
+ optional: true
+
+ '@rollup/rollup-freebsd-x64@4.63.1':
+ optional: true
+
+ '@rollup/rollup-linux-arm-gnueabihf@4.63.1':
+ optional: true
+
+ '@rollup/rollup-linux-arm-musleabihf@4.63.1':
+ optional: true
+
+ '@rollup/rollup-linux-arm64-gnu@4.63.1':
+ optional: true
+
+ '@rollup/rollup-linux-arm64-musl@4.63.1':
+ optional: true
+
+ '@rollup/rollup-linux-loong64-gnu@4.63.1':
+ optional: true
+
+ '@rollup/rollup-linux-loong64-musl@4.63.1':
+ optional: true
+
+ '@rollup/rollup-linux-ppc64-gnu@4.63.1':
+ optional: true
+
+ '@rollup/rollup-linux-ppc64-musl@4.63.1':
+ optional: true
+
+ '@rollup/rollup-linux-riscv64-gnu@4.63.1':
+ optional: true
+
+ '@rollup/rollup-linux-riscv64-musl@4.63.1':
+ optional: true
+
+ '@rollup/rollup-linux-s390x-gnu@4.63.1':
+ optional: true
+
+ '@rollup/rollup-linux-x64-gnu@4.63.1':
+ optional: true
+
+ '@rollup/rollup-linux-x64-musl@4.63.1':
+ optional: true
+
+ '@rollup/rollup-openbsd-x64@4.63.1':
+ optional: true
+
+ '@rollup/rollup-openharmony-arm64@4.63.1':
+ optional: true
+
+ '@rollup/rollup-win32-arm64-msvc@4.63.1':
+ optional: true
+
+ '@rollup/rollup-win32-ia32-msvc@4.63.1':
+ optional: true
+
+ '@rollup/rollup-win32-x64-gnu@4.63.1':
+ optional: true
+
+ '@rollup/rollup-win32-x64-msvc@4.63.1':
+ optional: true
+
+ '@types/babel__core@7.20.5':
+ dependencies:
+ '@babel/parser': 7.29.8
+ '@babel/types': 7.29.8
+ '@types/babel__generator': 7.27.0
+ '@types/babel__template': 7.4.4
+ '@types/babel__traverse': 7.28.0
+
+ '@types/babel__generator@7.27.0':
+ dependencies:
+ '@babel/types': 7.29.8
+
+ '@types/babel__template@7.4.4':
+ dependencies:
+ '@babel/parser': 7.29.8
+ '@babel/types': 7.29.8
+
+ '@types/babel__traverse@7.28.0':
+ dependencies:
+ '@babel/types': 7.29.8
+
+ '@types/estree@1.0.9': {}
+
+ '@vitejs/plugin-react@5.2.0(vite@7.3.6)':
+ dependencies:
+ '@babel/core': 7.29.7
+ '@babel/plugin-transform-react-jsx-self': 7.29.7(@babel/core@7.29.7)
+ '@babel/plugin-transform-react-jsx-source': 7.29.7(@babel/core@7.29.7)
+ '@rolldown/pluginutils': 1.0.0-rc.3
+ '@types/babel__core': 7.20.5
+ react-refresh: 0.18.0
+ vite: 7.3.6
+ transitivePeerDependencies:
+ - supports-color
+
+ baseline-browser-mapping@2.11.21: {}
+
+ browserslist@4.28.9:
+ dependencies:
+ baseline-browser-mapping: 2.11.21
+ caniuse-lite: 1.0.30001810
+ electron-to-chromium: 1.5.423
+ node-releases: 2.0.54
+ update-browserslist-db: 1.3.2(browserslist@4.28.9)
+
+ caniuse-lite@1.0.30001810: {}
+
+ convert-source-map@2.0.0: {}
+
+ debug@4.4.3:
+ dependencies:
+ ms: 2.1.3
+
+ electron-to-chromium@1.5.423: {}
+
+ esbuild@0.28.2:
+ optionalDependencies:
+ '@esbuild/aix-ppc64': 0.28.2
+ '@esbuild/android-arm': 0.28.2
+ '@esbuild/android-arm64': 0.28.2
+ '@esbuild/android-x64': 0.28.2
+ '@esbuild/darwin-arm64': 0.28.2
+ '@esbuild/darwin-x64': 0.28.2
+ '@esbuild/freebsd-arm64': 0.28.2
+ '@esbuild/freebsd-x64': 0.28.2
+ '@esbuild/linux-arm': 0.28.2
+ '@esbuild/linux-arm64': 0.28.2
+ '@esbuild/linux-ia32': 0.28.2
+ '@esbuild/linux-loong64': 0.28.2
+ '@esbuild/linux-mips64el': 0.28.2
+ '@esbuild/linux-ppc64': 0.28.2
+ '@esbuild/linux-riscv64': 0.28.2
+ '@esbuild/linux-s390x': 0.28.2
+ '@esbuild/linux-x64': 0.28.2
+ '@esbuild/netbsd-arm64': 0.28.2
+ '@esbuild/netbsd-x64': 0.28.2
+ '@esbuild/openbsd-arm64': 0.28.2
+ '@esbuild/openbsd-x64': 0.28.2
+ '@esbuild/openharmony-arm64': 0.28.2
+ '@esbuild/sunos-x64': 0.28.2
+ '@esbuild/win32-arm64': 0.28.2
+ '@esbuild/win32-ia32': 0.28.2
+ '@esbuild/win32-x64': 0.28.2
+
+ escalade@3.2.0: {}
+
+ fdir@6.5.0(picomatch@4.0.7):
+ optionalDependencies:
+ picomatch: 4.0.7
+
+ fsevents@2.3.3:
+ optional: true
+
+ gensync@1.0.0-beta.2: {}
+
+ js-tokens@4.0.0: {}
+
+ jsesc@3.1.0: {}
+
+ json5@2.2.3: {}
+
+ lru-cache@5.1.1:
+ dependencies:
+ yallist: 3.1.1
+
+ ms@2.1.3: {}
+
+ nanoid@3.3.18: {}
+
+ node-releases@2.0.54: {}
+
+ picocolors@1.1.1: {}
+
+ picomatch@4.0.7: {}
+
+ postcss@8.5.28:
+ dependencies:
+ nanoid: 3.3.18
+ picocolors: 1.1.1
+ source-map-js: 1.2.1
+
+ react-dom@19.2.8(react@19.2.8):
+ dependencies:
+ react: 19.2.8
+ scheduler: 0.27.0
+
+ react-refresh@0.18.0: {}
+
+ react@19.2.8: {}
+
+ rollup@4.63.1:
+ dependencies:
+ '@types/estree': 1.0.9
+ optionalDependencies:
+ '@napi-rs/lzma-linux-x64-gnu': 1.5.1
+ '@rollup/rollup-android-arm-eabi': 4.63.1
+ '@rollup/rollup-android-arm64': 4.63.1
+ '@rollup/rollup-darwin-arm64': 4.63.1
+ '@rollup/rollup-darwin-x64': 4.63.1
+ '@rollup/rollup-freebsd-arm64': 4.63.1
+ '@rollup/rollup-freebsd-x64': 4.63.1
+ '@rollup/rollup-linux-arm-gnueabihf': 4.63.1
+ '@rollup/rollup-linux-arm-musleabihf': 4.63.1
+ '@rollup/rollup-linux-arm64-gnu': 4.63.1
+ '@rollup/rollup-linux-arm64-musl': 4.63.1
+ '@rollup/rollup-linux-loong64-gnu': 4.63.1
+ '@rollup/rollup-linux-loong64-musl': 4.63.1
+ '@rollup/rollup-linux-ppc64-gnu': 4.63.1
+ '@rollup/rollup-linux-ppc64-musl': 4.63.1
+ '@rollup/rollup-linux-riscv64-gnu': 4.63.1
+ '@rollup/rollup-linux-riscv64-musl': 4.63.1
+ '@rollup/rollup-linux-s390x-gnu': 4.63.1
+ '@rollup/rollup-linux-x64-gnu': 4.63.1
+ '@rollup/rollup-linux-x64-musl': 4.63.1
+ '@rollup/rollup-openbsd-x64': 4.63.1
+ '@rollup/rollup-openharmony-arm64': 4.63.1
+ '@rollup/rollup-win32-arm64-msvc': 4.63.1
+ '@rollup/rollup-win32-ia32-msvc': 4.63.1
+ '@rollup/rollup-win32-x64-gnu': 4.63.1
+ '@rollup/rollup-win32-x64-msvc': 4.63.1
+ fsevents: 2.3.3
+
+ scheduler@0.27.0: {}
+
+ semver@6.3.1: {}
+
+ source-map-js@1.2.1: {}
+
+ tinyglobby@0.2.17:
+ dependencies:
+ fdir: 6.5.0(picomatch@4.0.7)
+ picomatch: 4.0.7
+
+ update-browserslist-db@1.3.2(browserslist@4.28.9):
+ dependencies:
+ browserslist: 4.28.9
+ escalade: 3.2.0
+ picocolors: 1.1.1
+
+ vite@7.3.6:
+ dependencies:
+ esbuild: 0.28.2
+ fdir: 6.5.0(picomatch@4.0.7)
+ picomatch: 4.0.7
+ postcss: 8.5.28
+ rollup: 4.63.1
+ tinyglobby: 0.2.17
+ optionalDependencies:
+ fsevents: 2.3.3
+
+ yallist@3.1.1: {}
diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml
new file mode 100644
index 0000000..efc037a
--- /dev/null
+++ b/pnpm-workspace.yaml
@@ -0,0 +1,2 @@
+onlyBuiltDependencies:
+ - esbuild
diff --git a/public/favicon.svg b/public/favicon.svg
new file mode 100644
index 0000000..971b5e6
--- /dev/null
+++ b/public/favicon.svg
@@ -0,0 +1 @@
+b.
diff --git a/public/images/bo-li.jpg b/public/images/bo-li.jpg
new file mode 100644
index 0000000..24c303a
Binary files /dev/null and b/public/images/bo-li.jpg differ
diff --git a/public/media/.gitkeep b/public/media/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/public/resumes/bo-li-bilingual.pdf b/public/resumes/bo-li-bilingual.pdf
new file mode 100644
index 0000000..5217a30
Binary files /dev/null and b/public/resumes/bo-li-bilingual.pdf differ
diff --git a/public/resumes/bo-li-zh.pdf b/public/resumes/bo-li-zh.pdf
new file mode 100644
index 0000000..4d89cfb
Binary files /dev/null and b/public/resumes/bo-li-zh.pdf differ
diff --git a/src/App.jsx b/src/App.jsx
new file mode 100644
index 0000000..f429def
--- /dev/null
+++ b/src/App.jsx
@@ -0,0 +1,142 @@
+import { useEffect, useState } from "react";
+import Header from "./components/Header";
+import ProjectCard from "./components/ProjectCard";
+import ResumeSections from "./components/ResumeSections";
+import Section from "./components/Section";
+import { copy, profile } from "./data/profile";
+import { projects, research, archive } from "./data/projects";
+
+export default function App() {
+ const [lang, setLang] = useState("zh");
+ useEffect(() => {
+ document.documentElement.lang = lang === "zh" ? "zh-CN" : "en";
+ document.title =
+ lang === "zh"
+ ? "李博 Bo Li — 软件工程师 · AI 探索者"
+ : "Bo Li — Software Engineer & AI Explorer";
+ }, [lang]);
+ return (
+ <>
+
+ {lang === "zh" ? "跳转到正文" : "Skip to content"}
+
+
+
+
+
+
+ {profile.title[lang]}
+
+
+ {lang === "zh" ? "你好,我是李博。" : "Hi, I’m Bo Li."}
+
+
{profile.headline[lang]}
+
{profile.intro[lang]}
+
+
+ FULL STACK / CLOUD / GENERATIVE AI
+
+
+
+
+
+ BO LI / 李博
+ ENGINEER & EXPLORER
+
+
+
+
+ Amazon
+
+ UIUC MCS ’25
+
+
+ UW–Madison BS ’23
+
+
+ {lang === "zh" ? "工程 × 创造力" : "ENGINEERING × CREATIVITY"}
+
+
+
+
+
+
+ {research.map((p) => (
+
+ {p.date}
+
+ {p.url ? (
+
+ {p.title[lang]} ↗
+
+ ) : (
+ p.title[lang]
+ )}
+
+ {p.description[lang]}
+
+ ))}
+
+
+
+
+ >
+ );
+}
diff --git a/src/components/Header.jsx b/src/components/Header.jsx
new file mode 100644
index 0000000..183c801
--- /dev/null
+++ b/src/components/Header.jsx
@@ -0,0 +1,29 @@
+import { copy, profile } from "../data/profile";
+export default function Header({ lang, setLang }) {
+ return (
+
+
+ b.
+
+
+ {["projects", "experience", "research"].map((id) => (
+
+ {copy[id][lang]}
+
+ ))}
+
+
+
setLang(lang === "zh" ? "en" : "zh")}
+ aria-label={lang === "zh" ? "Switch to English" : "切换为中文"}
+ >
+ {lang === "zh" ? "EN" : "中文"}
+
+
+ {copy.contact[lang]} ↗
+
+
+
+ );
+}
diff --git a/src/components/ProjectCard.jsx b/src/components/ProjectCard.jsx
new file mode 100644
index 0000000..c23be0e
--- /dev/null
+++ b/src/components/ProjectCard.jsx
@@ -0,0 +1,90 @@
+import { useState } from "react";
+import { copy } from "../data/profile";
+
+export default function ProjectCard({ project, lang }) {
+ const [failed, setFailed] = useState(false);
+ const p = project;
+ const coming = p.status === "coming-soon" && !p.media?.src && !p.url;
+ return (
+
+ {p.media?.src ? (
+
+
setFailed(true)}
+ aria-label={p.title[lang]}
+ >
+
+ {p.media.captions && (
+
+ )}
+
+ {failed &&
{copy.videoError[lang]}
}
+
+ {copy.openVideo[lang]} ↗
+
+
+ ) : (
+
+ {p.metric ? (
+
+ {p.metric}
+ {p.metricLabel[lang]}
+
+ ) : p.visual === "motion" ? (
+ <>
+
+
IMAGINE. GENERATE. CREATE.
+ >
+ ) : (
+ <>
+
+
PERCEPTION → PLANNING → CONTROL
+ >
+ )}
+
+ )}
+
+
+ );
+}
diff --git a/src/components/ResumeSections.jsx b/src/components/ResumeSections.jsx
new file mode 100644
index 0000000..063a2ff
--- /dev/null
+++ b/src/components/ResumeSections.jsx
@@ -0,0 +1,56 @@
+import Section from "./Section";
+import { copy, education, experience, skills } from "../data/profile";
+export default function ResumeSections({ lang }) {
+ return (
+ <>
+
+
+ {experience.map((item) => (
+
+
+ {item.date}
+ {item.location[lang]}
+
+
+
+ {item.company[lang]}
+ {item.role[lang]}
+
+
+ {item.points.map((point, i) => (
+ {point[lang]}
+ ))}
+
+
+
+ ))}
+
+
+
+
+ {education.map((item) => (
+
+ {item.date}
+ {item.school[lang]}
+ {item.degree[lang]}
+
+ ))}
+
+
+
+
+ {skills.map((group) => (
+
+
{group.title[lang]}
+
+ {group.items.map((item) => (
+ {item}
+ ))}
+
+
+ ))}
+
+
+ >
+ );
+}
diff --git a/src/components/Section.jsx b/src/components/Section.jsx
new file mode 100644
index 0000000..a1d1874
--- /dev/null
+++ b/src/components/Section.jsx
@@ -0,0 +1,14 @@
+export default function Section({ id, number, title, children, intro }) {
+ return (
+
+
+
+ {number} /
+
{title}
+
+ {intro &&
{intro}
}
+
+ {children}
+
+ );
+}
diff --git a/src/data/profile.js b/src/data/profile.js
new file mode 100644
index 0000000..b284313
--- /dev/null
+++ b/src/data/profile.js
@@ -0,0 +1,154 @@
+// All bilingual copy lives here. zh is the default language.
+export const text = (zh, en) => ({ zh, en });
+export const profile = {
+ name: text("李博", "Bo Li"),
+ email: "libo.tom.mail@gmail.com",
+ github: "https://github.com/Li-Bo-github",
+ linkedin: "https://www.linkedin.com/in/bo-li-7b6a10105/",
+ instagram: "https://www.instagram.com/limbolavida/",
+ title: text("软件工程师 · AI 探索者", "Software engineer · AI explorer"),
+ headline: text("把想法,\n写成现实。", "Ideas into code.\nCode into impact."),
+ intro: text(
+ "专注于全栈开发、云基础设施与生成式 AI。从可靠的后端系统,到 AI 驱动的产品与影像,探索技术的更多可能。",
+ "I build across full-stack development, cloud infrastructure, and generative AI — from reliable backend systems to AI-powered products and visual experiments.",
+ ),
+};
+export const experience = [
+ {
+ id: "amazon-2025",
+ company: text("Amazon 亚马逊", "Amazon"),
+ role: text("软件工程师 I", "Software Development Engineer I"),
+ date: "2025.06 — 2025.12",
+ location: text("北京", "Beijing"),
+ points: [
+ text(
+ "通过并行 S3 copy 优化 CloudWatch 日志归档,将 backfill 时间从 2.5 小时缩短至 20 分钟(约 86%)。",
+ "Optimized CloudWatch log archiving with parallel S3 copies, reducing backfill from 2.5 hours to 20 minutes (approximately 86%).",
+ ),
+ text(
+ "基于 Step Functions、Lambda 和 DynamoDB TTL 构建事件驱动工作流,分析超过 40,000 条跨境传输事件,标记逾百个需进一步处理的实例。",
+ "Built event-driven workflows with Step Functions, Lambda, and DynamoDB TTL to analyze over 40,000 cross-border transfer events and flag over 100 instances for further review.",
+ ),
+ text(
+ "开发 Bedrock AI 助手及每日知识库同步管道,扩展 Python Lambda 以支持 .msg 邮件解析。",
+ "Developed a Bedrock AI assistant and daily knowledge-base sync pipeline, extending Python Lambda processing to support .msg email files.",
+ ),
+ ],
+ },
+ {
+ id: "tharzen",
+ company: text("Tharzen", "Tharzen"),
+ role: text("软件工程师实习生", "Software Engineering Intern"),
+ date: "2024.05 — 2024.08",
+ location: text("", ""),
+ points: [
+ text(
+ "使用 Rust 和双向求值开发可逆 PHP 解释器,通过测试驱动开发完善核心逻辑。",
+ "Developed a reversible PHP interpreter using Rust and bidirectional evaluation, with test-driven development for core functionality.",
+ ),
+ ],
+ },
+ {
+ id: "spark",
+ company: text("火花思维", "Spark Education Group"),
+ role: text("全栈开发实习生", "Full-stack Engineering Intern"),
+ date: "2023.08 — 2023.12",
+ location: text("北京", "Beijing"),
+ points: [
+ text(
+ "开发基于向量存储的内部 GenAI 代码库问答应用,使用 React、Node.js、FastAPI 和 PostgreSQL 部署大语言模型平台。",
+ "Developed an internal GenAI codebase assistant with vector storage, deploying an LLM platform with React, Node.js, FastAPI, and PostgreSQL.",
+ ),
+ text(
+ "优化数据库与 API,使每周活跃用户增长 30%,日活跃用户峰值达到 185。",
+ "Improved databases and APIs, contributing to 30% growth in weekly active users and a peak of 185 daily active users.",
+ ),
+ ],
+ },
+ {
+ id: "amazon-2022",
+ company: text("Amazon 亚马逊", "Amazon"),
+ role: text("软件工程师实习生", "Software Engineering Intern"),
+ date: "2022.05 — 2022.08",
+ location: text("美国 · 贝尔维尤", "Bellevue, WA"),
+ points: [
+ text(
+ "使用 Node.js、GraphQL 和 PostgreSQL 开发变更日志 API 与 CRUD 操作,参与从设计、评审到部署的完整开发流程。",
+ "Built changelog APIs and CRUD operations with Node.js, GraphQL, and PostgreSQL, owning the lifecycle from design and review through deployment.",
+ ),
+ ],
+ },
+];
+export const education = [
+ {
+ school: text(
+ "伊利诺伊大学香槟分校",
+ "University of Illinois Urbana-Champaign",
+ ),
+ degree: text("计算机科学硕士", "Master of Computer Science"),
+ date: "2024.01 — 2025.05",
+ },
+ {
+ school: text("威斯康星大学麦迪逊分校", "University of Wisconsin–Madison"),
+ degree: text(
+ "理学学士 · 计算机科学、数据科学",
+ "B.S. · Computer Science & Data Science",
+ ),
+ date: "2019.09 — 2023.05",
+ },
+];
+export const skills = [
+ {
+ title: text("语言与前端", "Languages & frontend"),
+ items: ["Java", "Python", "SQL", "TypeScript", "React"],
+ },
+ {
+ title: text("云与基础设施", "Cloud & infrastructure"),
+ items: [
+ "AWS CDK",
+ "Lambda",
+ "DynamoDB",
+ "Step Functions",
+ "EventBridge",
+ "S3",
+ "SQS",
+ "CloudWatch",
+ ],
+ },
+ {
+ title: text("AI 与后端", "AI & backend"),
+ items: [
+ "Amazon Bedrock",
+ "FastAPI",
+ "PostgreSQL",
+ "GraphQL",
+ "Scikit-learn",
+ "Google Guice",
+ "Dagger",
+ ],
+ },
+];
+export const copy = {
+ projects: text("项目展示", "Selected work"),
+ experience: text("工作经历", "Experience"),
+ research: text("研究探索", "Research"),
+ education: text("教育背景", "Education"),
+ skills: text("技术栈", "Toolkit"),
+ contact: text("联系我", "Get in touch"),
+ viewWork: text("看看我的项目", "Explore my work"),
+ resume: text("简历", "Résumé"),
+ projectIntro: text(
+ "从云端系统到 AI 创作,让技术落到实处。",
+ "Systems, experiments, and the things I build along the way.",
+ ),
+ source: text("查看代码", "View source"),
+ visit: text("查看项目", "View project"),
+ coming: text("作品待发布", "Coming soon"),
+ archive: text("更多项目", "More projects"),
+ footer: text("有想法?一起聊聊。", "Have something in mind? Let’s talk."),
+ videoError: text(
+ "视频暂时无法播放,可尝试打开原视频。",
+ "This video could not be played. Try opening the original video.",
+ ),
+ openVideo: text("打开原视频", "Open video"),
+};
diff --git a/src/data/projects.js b/src/data/projects.js
new file mode 100644
index 0000000..4e8f8ec
--- /dev/null
+++ b/src/data/projects.js
@@ -0,0 +1,124 @@
+import { text } from "./profile.js";
+// Add a record to add a card. Optional media: { src, poster, captions, captionsLang }.
+// Use public/media paths (e.g. /media/film.mp4) or an HTTPS video file URL.
+// Platform watch URLs belong in url, not media.src. Never invent project links.
+export const projects = [
+ {
+ id: "iot",
+ category: "CLOUD / IoT",
+ title: text("让 12 万台车,连接云端。", "120,000 vehicles. One cloud."),
+ description: text(
+ "面向模拟车队的可扩展物联网基础设施。通过 MQTT 连接设备,使用 Lambda 分析 CO₂ 排放,并构建数据洞察管道。",
+ "Scalable IoT infrastructure for a simulated fleet, connecting devices over MQTT, analyzing CO₂ emissions with Lambda, and delivering data insights.",
+ ),
+ tags: ["AWS IoT Core", "Python", "MQTT"],
+ visual: "cloud",
+ metric: "120K",
+ metricLabel: text("模拟车辆", "simulated vehicles"),
+ },
+ {
+ id: "video",
+ category: "AI / MOTION",
+ title: text("AI 影像实验", "AI motion experiments"),
+ description: text(
+ "用生成式 AI 探索影像叙事。新的作品将在这里呈现。",
+ "Exploring visual storytelling with generative AI. New work will appear here.",
+ ),
+ tags: ["Generative AI", "Video"],
+ visual: "motion",
+ status: "coming-soon",
+ media: null,
+ url: null,
+ },
+ {
+ id: "parking",
+ category: "AUTONOMOUS SYSTEMS",
+ title: text("GEM e4 自动泊车", "Autonomous parking · GEM e4"),
+ description: text(
+ "结合激光雷达与摄像头标定、Hybrid A* 路径规划和 MPC 轨迹跟踪,实现自动泊车导航。",
+ "Autonomous parking with LiDAR and camera calibration, Hybrid A* path planning, and MPC trajectory tracking.",
+ ),
+ tags: ["MPC", "Hybrid A*", "Robotics"],
+ visual: "path",
+ url: "https://github.com/Li-Bo-github/GEMstack",
+ },
+ {
+ id: "clustering",
+ category: "DATA SCIENCE",
+ title: text(
+ "从账户数据,发现家庭关联。",
+ "Finding connections in account data.",
+ ),
+ description: text(
+ "基于地址和联名账户构建定制距离函数,使用层次聚类分析账户持有人,模型准确率达到 96.6%。",
+ "Hierarchical clustering with custom distances based on addresses and joint accounts, reaching 96.6% model accuracy.",
+ ),
+ tags: ["Scikit-learn", "Snowflake", "Python"],
+ visual: "data",
+ metric: "96.6%",
+ metricLabel: text("模型准确率", "model accuracy"),
+ url: "https://github.com/CodySond/cs639-assocbank-team2",
+ },
+];
+export const research = [
+ {
+ id: "claimtrust",
+ title: text(
+ "ClaimTrust · RAG 信任评分",
+ "ClaimTrust · Trust scoring for RAG",
+ ),
+ date: "2024.08 — 2024.12",
+ description: text(
+ "基于图的文档可信度传播,处理 814 篇文章,将 RAG 系统回答质量提升 11.2%。指导教授:Heng Ji。",
+ "Graph-based trust propagation across 814 articles, improving RAG response quality by 11.2%. Advised by Heng Ji.",
+ ),
+ url: "https://github.com/Li-Bo-github/ClaimTrust-A-Propagation-Based-Trust-Scoring-Framework-for-Retrieval-Augmented-Generation-Systems",
+ },
+ {
+ id: "carla",
+ title: text(
+ "协同自动驾驶系统仿真",
+ "Collaborative automated driving simulation",
+ ),
+ date: "2022.02 — 2022.12",
+ description: text(
+ "研究车辆轨迹预测,统一 CARLA 仿真输出与预测模型输入的数据格式。指导教授:Bin Ran。",
+ "Investigated trajectory prediction and unified CARLA simulator exports with prediction model inputs. Advised by Bin Ran.",
+ ),
+ },
+ {
+ id: "smartnic",
+ title: text(
+ "SmartNIC / JBOF 数据库下推",
+ "Database pushdown on SmartNIC / JBOF",
+ ),
+ date: "2021.10 — 2021.12",
+ description: text(
+ "定制基于 C++ 的 FlexPushdownDB,探索缓存与数据库下推能力。指导教授:Ming Liu。",
+ "Customized C++ FlexPushdownDB to explore caching and database pushdown capabilities. Advised by Ming Liu.",
+ ),
+ },
+];
+export const archive = [
+ {
+ id: "farm",
+ title: text("农场数据管理系统", "Farm data management"),
+ tags: "JavaFX / Java",
+ url: "https://github.com/Li-Bo-github/milk-farm-management-system",
+ },
+ {
+ id: "enrollment",
+ title: text(
+ "本科招生数据研究与可视化",
+ "Undergraduate enrollment analysis",
+ ),
+ tags: "Python / Data",
+ url: "https://github.com/Li-Bo-github/school_undergrad-enrollment-study-and-visulization",
+ },
+ {
+ id: "cssa",
+ title: text("UW–Madison CSSA 网站", "UW–Madison CSSA website"),
+ tags: "Web development",
+ url: "https://github.com/cssaatuwmadisonIT",
+ },
+];
diff --git a/src/main.jsx b/src/main.jsx
new file mode 100644
index 0000000..0beeb11
--- /dev/null
+++ b/src/main.jsx
@@ -0,0 +1,9 @@
+import React from "react";
+import { createRoot } from "react-dom/client";
+import App from "./App";
+import "./styles.css";
+createRoot(document.getElementById("root")).render(
+
+
+ ,
+);
diff --git a/src/styles.css b/src/styles.css
new file mode 100644
index 0000000..0fbfcfb
--- /dev/null
+++ b/src/styles.css
@@ -0,0 +1,732 @@
+@import url("https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;600;700&family=Noto+Sans+SC:wght@400;500;600;700&display=swap");
+
+/* Theme: change these tokens to restyle every section. */
+:root {
+ font-family: "DM Sans", "Noto Sans SC", sans-serif;
+ color: #202720;
+ background: #f6f7f2;
+ font-synthesis: none;
+ --green: #335d45;
+ --line: #dce1d6;
+ --muted: #60695f;
+ --radius: 16px;
+}
+* {
+ box-sizing: border-box;
+}
+html {
+ scroll-behavior: smooth;
+ scroll-padding-top: 110px;
+}
+body {
+ margin: 0;
+}
+a {
+ color: inherit;
+ text-decoration: none;
+}
+button {
+ font: inherit;
+}
+a,
+button,
+summary {
+ -webkit-tap-highlight-color: transparent;
+ touch-action: manipulation;
+}
+a:focus-visible,
+button:focus-visible,
+summary:focus-visible {
+ outline: 3px solid #507b40;
+ outline-offset: 6px;
+}
+a:hover {
+ color: var(--green);
+}
+button,
+summary {
+ cursor: pointer;
+}
+p {
+ line-height: 1.8;
+ color: var(--muted);
+}
+.header,
+main {
+ width: min(1160px, calc(100% - 80px));
+ margin: auto;
+}
+.header {
+ min-height: 96px;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ border-bottom: 1px solid var(--line);
+ gap: 24px;
+}
+.brand {
+ font-size: 44px;
+ font-weight: 700;
+ letter-spacing: -5px;
+ line-height: 1;
+}
+.brand span {
+ color: #52784c;
+}
+.header nav,
+.header-actions {
+ display: flex;
+ gap: 30px;
+ align-items: center;
+ font-size: 14px;
+}
+.header nav a {
+ padding: 12px 0;
+}
+.language {
+ border: 0;
+ border-right: 1px solid var(--line);
+ background: none;
+ padding: 10px 24px 10px 0;
+}
+.contact-link span {
+ margin-left: 12px;
+}
+.hero {
+ display: grid;
+ grid-template-columns: 1.3fr 1fr;
+ gap: 80px;
+ padding: 78px 0 70px;
+ align-items: center;
+}
+.eyebrow {
+ font-size: 13px;
+ letter-spacing: 0.11em;
+ font-weight: 600;
+ color: var(--green);
+ line-height: 1.6;
+}
+.dot {
+ display: inline-block;
+ width: 8px;
+ height: 8px;
+ background: #4c7454;
+ border-radius: 50%;
+ margin-right: 8px;
+}
+.hello {
+ font-size: 20px;
+ color: #354031;
+ margin: 26px 0 15px;
+}
+h1 {
+ white-space: pre-line;
+ font-size: clamp(42px, 5vw, 67px);
+ line-height: 1.22;
+ letter-spacing: -0.055em;
+ font-weight: 600;
+ margin: 0 0 26px;
+}
+.intro {
+ max-width: 490px;
+ font-size: 16px;
+}
+.hero-actions {
+ display: flex;
+ align-items: center;
+ gap: 30px;
+ margin-top: 30px;
+}
+.button {
+ background: var(--green);
+ color: white;
+ padding: 16px 22px;
+ border-radius: 7px;
+ font-size: 15px;
+}
+.button:hover {
+ background: #244532;
+ color: white;
+}
+.button span {
+ margin-left: 25px;
+}
+.resume-link {
+ font-size: 15px;
+ border-bottom: 1px solid #82917f;
+ padding: 9px 0;
+}
+.hero-foot {
+ font-size: 12px;
+ letter-spacing: 0.09em;
+ color: #727a6e;
+ margin-top: 44px;
+}
+.hero-foot span {
+ margin: 0 12px;
+ color: #b0b8a9;
+}
+.portrait {
+ margin: 0;
+ position: relative;
+ padding: 12px;
+ background: #e7eadd;
+ border-radius: 120px 16px 16px 16px;
+ transform: rotate(2deg);
+}
+.portrait img {
+ display: block;
+ width: 100%;
+ height: 415px;
+ object-fit: cover;
+ object-position: center 40%;
+ border-radius: 110px 9px 9px 9px;
+ filter: saturate(0.75);
+}
+.portrait figcaption {
+ display: flex;
+ justify-content: space-between;
+ gap: 8px;
+ padding: 18px 7px 7px;
+ font-size: 12px;
+ letter-spacing: 0.03em;
+}
+.credential-strip {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ gap: 25px;
+ border-top: 1px solid var(--line);
+ border-bottom: 1px solid var(--line);
+ padding: 27px 0;
+ color: #4a5346;
+ font-size: 22px;
+ font-weight: 600;
+}
+.credential-strip small {
+ font-size: 12px;
+ font-weight: 400;
+ margin-left: 8px;
+}
+.credential-strip .strip-note {
+ font-size: 12px;
+ letter-spacing: 0.08em;
+ font-weight: 400;
+}
+.section {
+ padding-top: 85px;
+}
+.section-heading {
+ display: flex;
+ justify-content: space-between;
+ gap: 30px;
+ align-items: end;
+ margin-bottom: 30px;
+}
+.section-heading > div {
+ display: flex;
+ align-items: baseline;
+ gap: 18px;
+}
+h2 {
+ font-size: 32px;
+ letter-spacing: -0.04em;
+ font-weight: 500;
+ margin: 0;
+}
+.section-heading > p {
+ font-size: 14px;
+ max-width: 400px;
+ margin: 0;
+}
+.project-grid {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 28px;
+}
+.project-card {
+ border: 1px solid var(--line);
+ border-radius: var(--radius);
+ overflow: hidden;
+ background: #fdfefb;
+ transition: transform 0.2s;
+}
+.project-card:hover {
+ transform: translateY(-3px);
+}
+.project-art {
+ height: 245px;
+ position: relative;
+ overflow: hidden;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+.cloud {
+ background-color: #e2e9dd;
+ background-image: radial-gradient(#b2c3ad 1px, transparent 1px);
+ background-size: 18px 18px;
+}
+.metric {
+ font-size: 76px;
+ letter-spacing: -0.07em;
+ color: #345540;
+ text-align: center;
+}
+.metric span {
+ display: block;
+ font-size: 13px;
+ letter-spacing: 0.13em;
+}
+.motion {
+ background: #222b26;
+}
+.film-orbit {
+ width: 175px;
+ height: 175px;
+ border: 1px solid #9db199;
+ border-radius: 50%;
+ box-shadow:
+ 22px 0 0 -1px #222b26,
+ 23px 0 #6c8667,
+ -22px 0 0 -1px #222b26,
+ -23px 0 #6c8667;
+ transform: rotate(-30deg);
+}
+.film-orbit:after {
+ content: "";
+ position: absolute;
+ inset: 28px;
+ border: 1px solid #c0cbb1;
+ border-radius: 50%;
+}
+.art-label {
+ position: absolute;
+ bottom: 20px;
+ font-size: 12px;
+ letter-spacing: 0.06em;
+ color: #bac8b2;
+}
+.path {
+ background: #e9e7de;
+ background-image:
+ linear-gradient(#d6d5ca 1px, transparent 1px),
+ linear-gradient(90deg, #d6d5ca 1px, transparent 1px);
+ background-size: 32px 32px;
+}
+.parking-path {
+ width: 210px;
+ height: 130px;
+ border: 3px dashed #6d8066;
+ border-bottom: 0;
+ border-radius: 70px 70px 0 0;
+ transform: rotate(-15deg);
+}
+.parking-path:after {
+ content: "";
+ display: block;
+ width: 33px;
+ height: 55px;
+ background: #385a43;
+ border: 5px solid #768f73;
+ border-radius: 8px;
+ position: absolute;
+ right: -18px;
+ bottom: 0;
+}
+.path .art-label {
+ color: #59644d;
+}
+.data {
+ background: #e9ecdf;
+}
+.data:before,
+.data:after {
+ content: "";
+ position: absolute;
+ width: 180px;
+ height: 180px;
+ border: 1px solid #c4ccb7;
+ border-radius: 50%;
+ left: -55px;
+ top: -50px;
+}
+.data:after {
+ left: auto;
+ top: auto;
+ right: -50px;
+ bottom: -70px;
+ width: 260px;
+ height: 260px;
+}
+.project-content {
+ padding: 27px;
+}
+.card-meta {
+ font-size: 12px;
+ letter-spacing: 0.05em;
+ color: #67715f;
+ display: flex;
+ justify-content: space-between;
+ gap: 12px;
+ align-items: center;
+}
+.status {
+ font-size: 12px;
+ letter-spacing: 0;
+ background: #edf0e6;
+ padding: 4px 8px;
+ border-radius: 4px;
+}
+h3 {
+ font-size: 21px;
+ font-weight: 500;
+ letter-spacing: -0.025em;
+ line-height: 1.5;
+ margin: 16px 0 10px;
+}
+.project-content p {
+ font-size: 16px;
+ margin: 0 0 20px;
+ min-height: 86px;
+}
+.tags {
+ display: flex;
+ gap: 8px;
+ flex-wrap: wrap;
+}
+.tags span {
+ font-size: 14px;
+ padding: 5px 9px;
+ background: #edf0e8;
+ border-radius: 4px;
+ color: #55604e;
+ line-height: 1.6;
+}
+.text-link {
+ display: flex;
+ justify-content: space-between;
+ border-top: 1px solid var(--line);
+ margin-top: 24px;
+ padding-top: 17px;
+ font-size: 14px;
+}
+.video-wrap {
+ background: #202720;
+ color: white;
+}
+.video-wrap video {
+ width: 100%;
+ aspect-ratio: 16/9;
+ display: block;
+}
+.video-wrap p {
+ color: white;
+ padding: 10px;
+}
+.video-link {
+ display: block;
+ color: #fff;
+ padding: 12px;
+ font-size: 14px;
+}
+.video-link:hover {
+ color: #c6dcc3;
+}
+.archive {
+ border-bottom: 1px solid var(--line);
+ margin-top: 28px;
+}
+.archive summary {
+ font-size: 15px;
+ padding: 20px 0;
+}
+.archive > a {
+ display: flex;
+ justify-content: space-between;
+ gap: 20px;
+ padding: 18px 0;
+ border-top: 1px solid var(--line);
+}
+.archive small {
+ color: var(--muted);
+}
+.job {
+ display: grid;
+ grid-template-columns: 240px 1fr;
+ gap: 28px;
+ padding: 28px 0;
+ border-top: 1px solid var(--line);
+}
+.job-date {
+ font-size: 14px;
+ color: #627059;
+ padding-top: 8px;
+}
+.job-date span {
+ display: block;
+ font-size: 13px;
+ margin-top: 10px;
+}
+.job h3 {
+ margin: 0 0 15px;
+ font-size: 22px;
+}
+.role {
+ font-size: 15px;
+ color: var(--muted);
+ display: block;
+ margin-top: 4px;
+}
+.job ul {
+ padding-left: 18px;
+ margin: 0;
+ color: var(--muted);
+ line-height: 1.85;
+ font-size: 16px;
+}
+.job li {
+ padding-left: 5px;
+ margin: 8px 0;
+}
+.education-grid {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 28px;
+}
+.education-grid article {
+ border-top: 1px solid var(--line);
+ padding: 26px 0;
+}
+.education-grid p {
+ font-size: 16px;
+}
+.skills-grid {
+ display: grid;
+ grid-template-columns: 1fr 1.4fr 1.2fr;
+ gap: 30px;
+ border-top: 1px solid var(--line);
+ padding-top: 18px;
+}
+.skills-grid h3 {
+ font-size: 16px;
+ margin-bottom: 20px;
+}
+.research-list {
+ display: grid;
+ grid-template-columns: 1fr 1fr 1fr;
+ gap: 28px;
+}
+.research-list article {
+ border-top: 1px solid var(--line);
+ padding-top: 25px;
+}
+.research-list h3 {
+ font-size: 18px;
+}
+.research-list p {
+ font-size: 16px;
+}
+footer {
+ margin-top: 90px;
+ padding: 50px 0 0;
+ border-top: 1px solid var(--line);
+}
+footer h2 {
+ font-size: 40px;
+ margin: 18px 0;
+}
+.email {
+ font-size: clamp(17px, 3vw, 28px);
+ border-bottom: 1px solid #9aab91;
+ padding-bottom: 7px;
+ display: inline-block;
+}
+.footer-bottom {
+ display: flex;
+ justify-content: space-between;
+ gap: 20px;
+ padding: 50px 0 26px;
+ font-size: 14px;
+ color: #66715e;
+}
+.footer-bottom > div {
+ display: flex;
+ gap: 22px;
+}
+.skip-link {
+ position: absolute;
+ top: -100px;
+ left: 20px;
+ background: white;
+ padding: 15px;
+ z-index: 10;
+}
+.skip-link:focus {
+ top: 10px;
+}
+@media (min-width: 1450px) {
+ .hero {
+ padding-top: 90px;
+ padding-bottom: 85px;
+ }
+}
+@media (max-width: 900px) {
+ .header,
+ main {
+ width: calc(100% - 44px);
+ }
+ .hero {
+ gap: 35px;
+ padding: 55px 0;
+ }
+ .portrait img {
+ height: 350px;
+ }
+ .portrait figcaption {
+ flex-direction: column;
+ }
+ .header nav {
+ gap: 18px;
+ }
+ .credential-strip .strip-note {
+ display: none;
+ }
+ .job {
+ grid-template-columns: 180px 1fr;
+ }
+ .research-list {
+ grid-template-columns: 1fr;
+ }
+ .skills-grid {
+ grid-template-columns: 1fr 1fr;
+ }
+ .section-heading {
+ align-items: start;
+ flex-direction: column;
+ gap: 12px;
+ }
+}
+@media (max-width: 600px) {
+ .header,
+ main {
+ width: calc(100% - 36px);
+ }
+ .header {
+ flex-wrap: wrap;
+ padding: 18px 0;
+ gap: 15px;
+ }
+ .header nav {
+ order: 3;
+ width: 100%;
+ justify-content: space-between;
+ }
+ .header-actions {
+ gap: 14px;
+ }
+ .language {
+ padding-right: 15px;
+ }
+ .brand {
+ font-size: 36px;
+ }
+ .hero {
+ grid-template-columns: 1fr;
+ gap: 36px;
+ padding: 42px 0;
+ }
+ .hero-copy .eyebrow {
+ font-size: 12px;
+ }
+ h1 {
+ font-size: 48px;
+ }
+ .hero-foot {
+ font-size: 12px;
+ line-height: 1.8;
+ }
+ .hero-foot span {
+ margin: 0 4px;
+ }
+ .portrait {
+ max-width: 380px;
+ width: 90%;
+ margin: 0 auto;
+ }
+ .portrait img {
+ height: 340px;
+ }
+ .credential-strip {
+ gap: 14px;
+ flex-wrap: wrap;
+ font-size: 17px;
+ }
+ .credential-strip small {
+ display: block;
+ margin-left: 0;
+ margin-top: 5px;
+ }
+ .project-grid,
+ .education-grid,
+ .skills-grid {
+ grid-template-columns: 1fr;
+ }
+ .section {
+ padding-top: 58px;
+ }
+ .project-art {
+ height: 215px;
+ }
+ .project-content {
+ padding: 23px;
+ }
+ .project-content p {
+ min-height: 0;
+ }
+ .section-heading h2 {
+ font-size: 28px;
+ }
+ .job {
+ grid-template-columns: 1fr;
+ gap: 14px;
+ }
+ .job-date span {
+ display: inline;
+ margin-left: 14px;
+ }
+ .archive > a {
+ flex-direction: column;
+ gap: 6px;
+ }
+ .footer-bottom {
+ flex-wrap: wrap;
+ line-height: 2;
+ }
+ .footer-bottom > div {
+ gap: 15px;
+ }
+ footer h2 {
+ font-size: 32px;
+ }
+ .email {
+ overflow-wrap: anywhere;
+ }
+ .hero-actions {
+ gap: 25px;
+ }
+ .skills-grid {
+ gap: 12px;
+ }
+}
+@media (prefers-reduced-motion: reduce) {
+ html {
+ scroll-behavior: auto;
+ }
+ .project-card {
+ transition: none;
+ }
+ .project-card:hover {
+ transform: none;
+ }
+}
diff --git a/tests/content.test.js b/tests/content.test.js
new file mode 100644
index 0000000..b22e944
--- /dev/null
+++ b/tests/content.test.js
@@ -0,0 +1,61 @@
+import test from "node:test";
+import assert from "node:assert/strict";
+import { existsSync } from "node:fs";
+import { projects, research, archive } from "../src/data/projects.js";
+import {
+ profile,
+ experience,
+ education,
+ skills,
+ copy,
+} from "../src/data/profile.js";
+
+function checkTranslations(value) {
+ if (!value || typeof value !== "object") return;
+ if ("zh" in value || "en" in value) {
+ assert.equal(typeof value.zh, "string", "Missing Chinese translation");
+ assert.equal(typeof value.en, "string", "Missing English translation");
+ } else Object.values(value).forEach(checkTranslations);
+}
+test("all portfolio content provides both languages", () => {
+ [
+ projects,
+ research,
+ archive,
+ profile,
+ experience,
+ education,
+ skills,
+ copy,
+ ].forEach(checkTranslations);
+});
+test("project IDs are unique and linked resources are usable URLs", () => {
+ const records = [...projects, ...research, ...archive];
+ assert.equal(new Set(records.map((p) => p.id)).size, records.length);
+ for (const p of records) {
+ assert.ok(p.title.zh && p.title.en);
+ if (p.url) assert.equal(new URL(p.url).protocol, "https:");
+ if (p.media?.src) {
+ assert.ok(
+ !/youtube\.com\/watch|youtu\.be|bilibili\.com\/video/.test(p.media.src),
+ "Platform watch URLs must use url, not media.src",
+ );
+ for (const path of [p.media.src, p.media.poster, p.media.captions].filter(
+ Boolean,
+ )) {
+ if (path.startsWith("/"))
+ assert.ok(existsSync(`public${path}`), `Missing asset: ${path}`);
+ else assert.equal(new URL(path).protocol, "https:");
+ }
+ }
+ }
+});
+test("required public assets exist", () => {
+ for (const path of [
+ "public/images/bo-li.jpg",
+ "public/resumes/bo-li-zh.pdf",
+ "public/resumes/bo-li-bilingual.pdf",
+ "public/favicon.svg",
+ ])
+ assert.ok(existsSync(path), `Missing ${path}`);
+});
diff --git a/vite.config.js b/vite.config.js
new file mode 100644
index 0000000..582ec4f
--- /dev/null
+++ b/vite.config.js
@@ -0,0 +1,3 @@
+import { defineConfig } from "vite";
+import react from "@vitejs/plugin-react";
+export default defineConfig({ plugins: [react()], base: "/" });