From 88761c77e120a7c3e96dc06a41ba2be0232baa99 Mon Sep 17 00:00:00 2001 From: chengpeiquan Date: Sun, 11 Oct 2026 23:59:20 +0800 Subject: [PATCH 1/5] feat(website): add bilingual scenario examples and code explorer --- docs/content.config.ts | 16 ++ docs/docs.config.ts | 4 + docs/package.json | 1 + docs/pnpm-lock.yaml | 3 + docs/src/app/globals.css | 15 ++ .../src/contents/en/examples/file-changes.mdx | 30 +++ docs/src/contents/en/examples/index.mdx | 21 ++ docs/src/contents/en/examples/monorepo.mdx | 24 ++ docs/src/contents/en/examples/skills.mdx | 31 +++ docs/src/contents/en/examples/variants.mdx | 26 ++ .../src/contents/en/guide/getting-started.mdx | 4 + .../src/contents/zh/examples/file-changes.mdx | 30 +++ docs/src/contents/zh/examples/index.mdx | 21 ++ docs/src/contents/zh/examples/monorepo.mdx | 24 ++ docs/src/contents/zh/examples/skills.mdx | 31 +++ docs/src/contents/zh/examples/variants.mdx | 26 ++ .../src/contents/zh/guide/getting-started.mdx | 4 + docs/src/examples/changes.test.ts | 21 ++ docs/src/examples/changes.ts | 50 ++++ docs/src/examples/code-types.ts | 31 +++ docs/src/examples/code-view.module.css | 79 +++++++ docs/src/examples/code-view.tsx | 33 +++ docs/src/examples/copy.ts | 70 ++++++ docs/src/examples/example-explorer-client.tsx | 106 +++++++++ docs/src/examples/example-explorer.tsx | 10 + docs/src/examples/example-list.tsx | 25 ++ docs/src/examples/fixtures.json | 222 ++++++++++++++++++ docs/src/examples/highlight.server.ts | 73 ++++++ docs/src/site/navigation.ts | 4 + 29 files changed, 1035 insertions(+) create mode 100644 docs/src/contents/en/examples/file-changes.mdx create mode 100644 docs/src/contents/en/examples/index.mdx create mode 100644 docs/src/contents/en/examples/monorepo.mdx create mode 100644 docs/src/contents/en/examples/skills.mdx create mode 100644 docs/src/contents/en/examples/variants.mdx create mode 100644 docs/src/contents/zh/examples/file-changes.mdx create mode 100644 docs/src/contents/zh/examples/index.mdx create mode 100644 docs/src/contents/zh/examples/monorepo.mdx create mode 100644 docs/src/contents/zh/examples/skills.mdx create mode 100644 docs/src/contents/zh/examples/variants.mdx create mode 100644 docs/src/examples/changes.test.ts create mode 100644 docs/src/examples/changes.ts create mode 100644 docs/src/examples/code-types.ts create mode 100644 docs/src/examples/code-view.module.css create mode 100644 docs/src/examples/code-view.tsx create mode 100644 docs/src/examples/copy.ts create mode 100644 docs/src/examples/example-explorer-client.tsx create mode 100644 docs/src/examples/example-explorer.tsx create mode 100644 docs/src/examples/example-list.tsx create mode 100644 docs/src/examples/fixtures.json create mode 100644 docs/src/examples/highlight.server.ts diff --git a/docs/content.config.ts b/docs/content.config.ts index 54b51cd..1151b52 100644 --- a/docs/content.config.ts +++ b/docs/content.config.ts @@ -18,6 +18,22 @@ export const docsContentConfig = { }, sections: { showcase: { layout: 'content' }, + examples: { + layout: 'docs', + sidebar: [ + { + type: 'group', + label: { en: 'Examples', zh: '使用示例' }, + items: [ + { type: 'item', href: '/examples', label: { en: 'Overview', zh: '示例概览' } }, + { type: 'item', href: '/examples/variants', label: { en: 'Template variants', zh: '同一模板,不同版本' } }, + { type: 'item', href: '/examples/skills', label: { en: 'Shared skills', zh: '复用常用 Skills' } }, + { type: 'item', href: '/examples/monorepo', label: { en: 'Extract from a monorepo', zh: '提取 monorepo 子项目' } }, + { type: 'item', href: '/examples/file-changes', label: { en: 'Edit files during creation', zh: '创建时调整文件' } }, + ], + }, + ], + }, guide: { layout: 'docs', sidebar: [ diff --git a/docs/docs.config.ts b/docs/docs.config.ts index 94c1ba8..9f56260 100644 --- a/docs/docs.config.ts +++ b/docs/docs.config.ts @@ -8,6 +8,8 @@ import { PresetFooter } from './src/site/footer' import { DocsSiteLink } from './src/site/docs-link' import { headerNavigation } from './src/site/navigation' import { ShowcaseProject } from './src/showcase/showcase-project' +import { ExampleExplorer } from './src/examples/example-explorer' +import { ExampleList } from './src/examples/example-list' export const docsConfig = defineDocsConfig({ content: docsContentConfig, @@ -40,6 +42,8 @@ export const docsConfig = defineDocsConfig({ FadePreview: DocsFadePreview, PackageManagerCommands, ShowcaseProject, + ExampleExplorer, + ExampleList, }, }, theme: { diff --git a/docs/package.json b/docs/package.json index bb16426..acb011e 100644 --- a/docs/package.json +++ b/docs/package.json @@ -23,6 +23,7 @@ "next": "^16.3.8", "react": "^19.2.8", "react-dom": "^19.2.8", + "shiki": "^4.4.3", "tailwind-merge": "^3.6.0" }, "devDependencies": { diff --git a/docs/pnpm-lock.yaml b/docs/pnpm-lock.yaml index 1a736d4..d18240f 100644 --- a/docs/pnpm-lock.yaml +++ b/docs/pnpm-lock.yaml @@ -199,6 +199,9 @@ importers: react-dom: specifier: ^19.2.8 version: 19.2.8(react@19.2.8) + shiki: + specifier: ^4.4.3 + version: 4.4.3 tailwind-merge: specifier: ^3.6.0 version: 3.6.0 diff --git a/docs/src/app/globals.css b/docs/src/app/globals.css index b56dd14..e8bdfaa 100644 --- a/docs/src/app/globals.css +++ b/docs/src/app/globals.css @@ -23,6 +23,21 @@ background-color: hsl(var(--foreground) / 0.35); } +/* File examples need room for both paths and code inside the shared docs shell. */ +@media (width >= 64rem) { + [data-docs-region='docs-shell']:has([data-preset-example]) { + column-gap: 3rem; + } + + [data-docs-region='docs-shell']:has([data-preset-example]) [data-docs-region='sidebar'] { + width: 14rem; + } + + [data-docs-region='docs-shell']:has([data-preset-example]) [data-docs-region='toc'] { + display: none; + } +} + /* Keep shared header controls reachable on phones, including docs pages. */ @media (width < 40rem) { [data-docs-region='header'] > div { diff --git a/docs/src/contents/en/examples/file-changes.mdx b/docs/src/contents/en/examples/file-changes.mdx new file mode 100644 index 0000000..63b1298 --- /dev/null +++ b/docs/src/contents/en/examples/file-changes.mdx @@ -0,0 +1,30 @@ +--- +title: Edit files during project creation +description: Remove files, write documentation, replace text, and merge JSON during creation. +order: 4 +--- + +# Edit files during project creation + +Every clone of a service starter needs the same cleanup: remove demo files, rewrite the README, change a default value, and delete the demo command from `package.json`. + +Describe these steps in a preset and apply them when generating the project. + + + +## Order of operations + +1. `exclude` skips `demo.js` before copying. +2. `write` replaces `README.md` with the entries in `lines`, one per line. +3. `replace` changes `APP_LABEL=Starter` to `APP_LABEL=Team` in `.env.example`. +4. `json` merges `package.json`. Setting `scripts.demo` to `null` removes that command while keeping `scripts.start`. + +Switch to Output and open each file to see the result. + +## Preview an edited config + +```bash +pnpm create preset init my-service --from ./presets/service.json --dry-run +``` + +The preview lists the creation plan. It does not clone the repository or check whether replacement text exists. Run creation and inspect the generated files afterward. See [Preset Configs](/guide/preset-configs) for the full reference. diff --git a/docs/src/contents/en/examples/index.mdx b/docs/src/contents/en/examples/index.mdx new file mode 100644 index 0000000..6d7905f --- /dev/null +++ b/docs/src/contents/en/examples/index.mdx @@ -0,0 +1,21 @@ +--- +title: Examples +description: Compare source files, preset configs, and output for common project setup tasks. +order: 0 +--- + +# Examples + +Nearly identical templates need the same fix in several places. Common skills get copied into every project. A starter inside a monorepo needs its files and dependencies cleaned up before it can stand alone. + +These examples show how to describe those steps in a preset. Each includes source files, a JSON config, and the resulting files. + + + +## Reading an example + +Start with the source, inspect the preset, then compare the output. Some examples offer multiple editions generated from the same source. + +Repository URLs are fictional and code is simplified to focus on file operations. Config fields and commands match the CLI. Replace repository URLs and provide the corresponding source files before running. If a template author already supplies a command, use it directly. + +See [Getting Started](/guide/getting-started) for prerequisites and [Preset Configs](/guide/preset-configs) for the full reference. Real projects are listed in the [showcase](/showcase). diff --git a/docs/src/contents/en/examples/monorepo.mdx b/docs/src/contents/en/examples/monorepo.mdx new file mode 100644 index 0000000..515bc24 --- /dev/null +++ b/docs/src/contents/en/examples/monorepo.mdx @@ -0,0 +1,24 @@ +--- +title: Extract a project from a monorepo +description: Select a directory, exclude internal files, and replace workspace dependencies. +order: 3 +--- + +# Extract a project from a monorepo + +The docs starter lives in `apps/docs` and the component library in `packages/ui`. To use only the starter, someone has to extract its directory and adjust its dependencies. + +The preset selects `apps/docs`, excludes internal starter tests, and replaces a `workspace:` dependency with a specified version. + + + +## What changes + +- `source.directory` selects `apps/docs`. Its contents become the new project's root files. +- Exclusion paths are relative to that selected directory, so the config uses `src/__tests__`. +- `packages/ui` and the root `pnpm-workspace.yaml` are not copied. +- `packageJson.resolveWorkspace` enables dependency conversion. `workspaceVersions` pins `@example/ui` to `1.2.3`, skipping the registry lookup for that package. + +`@example/ui` is a fictional package. Use a published package and version that your environment can install. The component implementation is not automatically bundled from the monorepo into the new project. + +Without a pinned version, the CLI queries `npm view` using your npm registry settings. See [Resolve workspace dependencies](/guide/preset-configs#resolve-workspace-dependencies) for the resolution rules. diff --git a/docs/src/contents/en/examples/skills.mdx b/docs/src/contents/en/examples/skills.mdx new file mode 100644 index 0000000..cda3cb3 --- /dev/null +++ b/docs/src/contents/en/examples/skills.mdx @@ -0,0 +1,31 @@ +--- +title: Reuse skills across projects +description: Keep common skills in one repository and bring the needed files into each project. +order: 2 +--- + +# Reuse skills across projects + +Code review, documentation, and testing often use the same conventions across projects. Keep those skills in one repository and bring them into a project with one command. + +This example selects the `skills` directory in `team-resources`. Switch to “Without testing” to see how `exclude` leaves out a skill that this project does not need. + + + +## Where the files go + +Run the example command from the project root, targeting a new or empty `.agents/skills` directory. The target should match the skills location read by your agent. Change the command's target path if needed. + +The source file `skills/code-review/SKILL.md` becomes `.agents/skills/code-review/SKILL.md` in the project. Several projects can use this same collection without publishing an npm package to distribute it. + +## If the project already has skills + +**For a nonempty target, the CLI asks to replace the entire target directory. It does not merge individual skills.** Generate into a new temporary directory first, then inspect and copy the files needed: + +```bash +pnpm create preset init skills-preview --from ./presets/skills.json +``` + +Copy the selected skill folders into the project's skills directory. Compare same-name files before overwriting them. Use the same process to review later updates. Create Preset does not continuously sync skills or link them into other agents' directories. + +This example distributes skills already collected in a single repository. Collect files from multiple upstream repositories there first; each preset specifies one source repository. diff --git a/docs/src/contents/en/examples/variants.mdx b/docs/src/contents/en/examples/variants.mdx new file mode 100644 index 0000000..b8738eb --- /dev/null +++ b/docs/src/contents/en/examples/variants.mdx @@ -0,0 +1,26 @@ +--- +title: One template, different editions +description: Keep one source and describe file differences in separate presets. +order: 1 +--- + +# One template, different editions + +The basic edition does not need analytics; the team edition keeps it. The rest of the code is shared, yet maintaining two templates means fixing both copies. + +Keep one complete source. Use a preset for each edition to describe what changes. The basic preset removes analytics and its entry-point references. The team preset keeps them. Generated projects remain independent. + + + +## What changes + +- Both configs use the same `source`, selecting the repository's `starter` directory. +- The basic preset excludes `src/analytics.ts` and uses `replace` to remove its import and call. +- The team preset keeps the complete source. Switch editions and inspect `src/main.ts` in Output. +- The generated `package.json` name follows the target directory. + +Shared code is maintained once in the source repository. Edition-specific changes run during creation. Existing generated projects do not receive later source updates automatically. + +`replace` matches exact text. If the source changes and that text disappears, creation fails until the preset is updated. Use this approach for small, explicit differences. The CLI does not analyze module references or convert TypeScript automatically. + +See [Preset Configs](/guide/preset-configs) for all operations. diff --git a/docs/src/contents/en/guide/getting-started.mdx b/docs/src/contents/en/guide/getting-started.mdx index 728513f..b4457fd 100644 --- a/docs/src/contents/en/guide/getting-started.mdx +++ b/docs/src/contents/en/guide/getting-started.mdx @@ -6,6 +6,10 @@ order: 1 # Getting Started +Start with the [examples](/examples) to compare source files, configs, and output for template variants, shared skills, and monorepo projects. + +If a template author already provides a creation command, run it directly. There is no need to write a JSON config first. + Run Create Preset with a package manager: diff --git a/docs/src/contents/zh/examples/file-changes.mdx b/docs/src/contents/zh/examples/file-changes.mdx new file mode 100644 index 0000000..ff22305 --- /dev/null +++ b/docs/src/contents/zh/examples/file-changes.mdx @@ -0,0 +1,30 @@ +--- +title: 创建时调整文件和配置 +description: 把删除文件、写入说明、替换文本和修改 JSON 放进创建流程。 +order: 4 +--- + +# 创建时调整文件和配置 + +每次拉取服务模板,都要删除演示文件、重写 README、改掉默认配置,再清理 `package.json` 中的演示命令。 + +将这些固定步骤写进一个 Preset,生成项目时一起处理。 + + + +## 按什么顺序处理 + +1. `exclude`:复制前排除 `demo.js`。 +2. `write`:用 `lines` 中的内容覆盖 `README.md`,每项对应一行。 +3. `replace`:将 `.env.example` 中的 `APP_LABEL=Starter` 换成 `APP_LABEL=Team`。 +4. `json`:合并 `package.json`;将 `scripts.demo` 设为 `null`,删除这条命令,保留 `scripts.start`。 + +切换到「生成结果」,逐个打开这些文件,即可看到修改后的内容。 + +## 调整配置后先预览 + +```bash +pnpm create preset init my-service --from ./presets/service.json --dry-run +``` + +预览列出创建计划,不会下载仓库或验证替换文本是否存在。正式创建后,再检查生成目录中的文件。完整字段见[Preset 配置](/zh/guide/preset-configs)。 diff --git a/docs/src/contents/zh/examples/index.mdx b/docs/src/contents/zh/examples/index.mdx new file mode 100644 index 0000000..3f9cc4a --- /dev/null +++ b/docs/src/contents/zh/examples/index.mdx @@ -0,0 +1,21 @@ +--- +title: 使用示例 +description: 从常见的重复工作开始,对照源文件、配置和生成结果。 +order: 0 +--- + +# 使用示例 + +模板只差几处,却要维护多份;常用 Skills 换个项目又要复制一遍;monorepo 里的子项目,取出来之后还得整理文件和依赖。 + +下面的示例分别演示这些步骤如何放进 Preset。每篇都有源文件、JSON 配置和生成结果,可以切换文件查看差异。 + + + +## 如何阅读示例 + +先看源文件,再看 Preset 修改了什么,最后对照生成结果。部分示例可以切换版本,比较同一份源码的不同输出。 + +示例使用虚构仓库和简化代码,重点是目录和文件处理方式。配置字段和命令与 CLI 一致;实际使用时,需要替换仓库地址,并保证仓库中存在对应文件。模板作者已提供命令时,直接使用即可。 + +运行环境见[快速开始](/zh/guide/getting-started),完整字段见[Preset 配置](/zh/guide/preset-configs)。真实项目的使用情况见[案例展示](/zh/showcase)。 diff --git a/docs/src/contents/zh/examples/monorepo.mdx b/docs/src/contents/zh/examples/monorepo.mdx new file mode 100644 index 0000000..a8902b3 --- /dev/null +++ b/docs/src/contents/zh/examples/monorepo.mdx @@ -0,0 +1,24 @@ +--- +title: 从 monorepo 提取子项目 +description: 选取子目录、排除内部文件,将 workspace 依赖换成独立项目可用的版本。 +order: 3 +--- + +# 从 monorepo 提取子项目 + +文档模板放在 `apps/docs`,组件库放在 `packages/ui`。只想用文档模板,却要先克隆整个仓库,再手动整理目录和依赖。 + +Preset 选取 `apps/docs`,排除模板内部测试文件,并将 `workspace:` 依赖替换为指定版本。 + + + +## 看看发生了什么 + +- `source.directory` 选择 `apps/docs`,其中的文件直接成为新项目的内容。 +- `exclude` 中的路径相对于选中的模板目录,因此写 `src/__tests__`。 +- `packages/ui` 和根目录的 `pnpm-workspace.yaml` 不会复制到新项目。 +- `packageJson.resolveWorkspace` 启用依赖转换,`workspaceVersions` 将 `@example/ui` 固定为 `1.2.3`,跳过该包的 registry 查询。 + +`@example/ui` 是演示名称。实际配置应使用已经发布、且当前环境有权限安装的包和版本。组件实现不会从 monorepo 自动打包进新项目。 + +不指定固定版本时,CLI 通过 `npm view` 查询版本,沿用 npm 的 registry 配置。更多规则见[处理 workspace 依赖](/zh/guide/preset-configs#处理-workspace-依赖)。 diff --git a/docs/src/contents/zh/examples/skills.mdx b/docs/src/contents/zh/examples/skills.mdx new file mode 100644 index 0000000..25fe72d --- /dev/null +++ b/docs/src/contents/zh/examples/skills.mdx @@ -0,0 +1,31 @@ +--- +title: 在多个项目中复用 Skills +description: 将常用 Skills 放在一个仓库,按需引入项目。 +order: 2 +--- + +# 在多个项目中复用 Skills + +代码审查、文档写作、测试约定,在不同项目里经常都会用到。将这些 Skills 集中放在一个仓库,项目需要时,运行一条命令引入。 + +示例从 `team-resources` 仓库选取 `skills` 目录。切换到「不含测试 Skill」,可以看到 `exclude` 如何排除这次用不上的内容。 + + + +## 放到项目的哪个位置 + +从项目根目录运行示例命令,将文件生成到新建或空的 `.agents/skills` 目录。该目录应与你使用的 agent 的 Skills 读取位置一致;需要其他位置时,修改命令的目标路径。 + +源文件中的 `skills/code-review/SKILL.md` 会出现在项目的 `.agents/skills/code-review/SKILL.md`。仓库中同一批 Skills 可以供多个项目使用,无需为分发再发布一个 npm 包。 + +## 项目已经有 Skills 时 + +**目标目录已有内容时,CLI 会询问是否替换整个目标目录,不会逐个 Skill 合并。** 可以先生成到一个新的临时目录,再检查并复制需要的文件: + +```bash +pnpm create preset init skills-preview --from ./presets/skills.json +``` + +将需要的 Skill 文件夹复制到项目的 Skills 目录;同名文件先比较再决定是否覆盖。后续更新也可以用这个方式检查变化。Create Preset 不会持续同步 Skills,亦不会自动链接到其他 agent 的目录。 + +这里展示的是集中保存在同一仓库的 Skills。多个上游仓库需要先整理到该仓库中;一份 Preset 只指定一个源仓库。 diff --git a/docs/src/contents/zh/examples/variants.mdx b/docs/src/contents/zh/examples/variants.mdx new file mode 100644 index 0000000..4e2b8be --- /dev/null +++ b/docs/src/contents/zh/examples/variants.mdx @@ -0,0 +1,26 @@ +--- +title: 同一模板,生成不同版本 +description: 共用一份源码,用不同 Preset 处理模板之间的文件差异。 +order: 1 +--- + +# 同一模板,生成不同版本 + +基础版不需要统计模块,团队版需要保留。其他代码相同,修复问题时却要同时修改两份模板。 + +保留一份完整源码,用两份 Preset 描述差异。基础版移除统计模块及入口引用,团队版保留,生成的项目仍然各自独立。 + + + +## 看看发生了什么 + +- 两份配置的 `source` 相同,都读取仓库中的 `starter` 目录。 +- 基础版用 `exclude` 排除 `src/analytics.ts`,再用 `replace` 删除入口中的导入和调用。 +- 团队版保留完整源码。切换版本后,查看「生成结果」中的 `src/main.ts`。 +- `package.json` 的 `name` 会按目标目录更新。 + +公共代码在源仓库中维护一份,生成时应用各版本的差异。已经生成的项目不会随后自动同步。 + +`replace` 按原文精确匹配。源码改动后,如果旧文本不再存在,创建会报错,需要一起更新配置。这个示例适合少量、明确的文件差异;CLI 不会自动分析模块引用或转换 TypeScript。 + +完整操作说明见[Preset 配置](/zh/guide/preset-configs)。 diff --git a/docs/src/contents/zh/guide/getting-started.mdx b/docs/src/contents/zh/guide/getting-started.mdx index 6ea87b3..ed4e0ca 100644 --- a/docs/src/contents/zh/guide/getting-started.mdx +++ b/docs/src/contents/zh/guide/getting-started.mdx @@ -6,6 +6,10 @@ order: 1 # 快速开始 +想先看看能解决哪些问题?从[使用示例](/zh/examples)开始,对照源文件、配置和生成结果,了解如何复用模板、引入 Skills 或提取 monorepo 子项目。 + +如果模板作者已经提供创建命令,直接运行即可,无需自己编写 JSON。 + 使用包管理器运行 Create Preset: diff --git a/docs/src/examples/changes.test.ts b/docs/src/examples/changes.test.ts new file mode 100644 index 0000000..aa79290 --- /dev/null +++ b/docs/src/examples/changes.test.ts @@ -0,0 +1,21 @@ +import { describe, expect, test } from 'vitest' +import { changedLines, sourceChange } from './changes' + +describe('example change annotations', () => { + test('does not mark retained lines after a removed import', () => { + expect(changedLines("import './analytics'\nstart()\n", 'start()\n')).toEqual({ before: [0], after: [] }) + }) + + test('matches repeated lines in order when content is inserted', () => { + expect(changedLines('a\nb\na\n', 'a\nx\nb\na\n')).toEqual({ before: [], after: [1] }) + }) + + test('highlights the two sides of a replacement', () => { + expect(changedLines('APP=Starter\n', 'APP=Team\n')).toEqual({ before: [0], after: [0] }) + }) + + test('distinguishes excluded files from files outside the selected directory', () => { + expect(sourceChange('apps/docs/test.ts', 'test()\n', 'apps/docs', {})).toEqual({ kind: 'removed', lines: [0] }) + expect(sourceChange('apps/docs-other/test.ts', 'test()\n', 'apps/docs', {})).toEqual({ kind: 'outside', lines: [] }) + }) +}) diff --git a/docs/src/examples/changes.ts b/docs/src/examples/changes.ts new file mode 100644 index 0000000..7799edf --- /dev/null +++ b/docs/src/examples/changes.ts @@ -0,0 +1,50 @@ +import type { FileChange } from './code-types' + +export function codeLines(content: string) { + return content.replace(/\n$/, '').split('\n') +} + +// Match lines in order, so a removed import does not mark every later line as changed. +export function changedLines(before: string, after: string) { + const left = codeLines(before) + const right = codeLines(after) + const lengths = Array.from({ length: left.length + 1 }, () => + new Array(right.length + 1).fill(0), + ) + for (let i = left.length - 1; i >= 0; i--) { + for (let j = right.length - 1; j >= 0; j--) { + lengths[i][j] = left[i] === right[j] + ? lengths[i + 1][j + 1] + 1 + : Math.max(lengths[i + 1][j], lengths[i][j + 1]) + } + } + const removed: number[] = [] + const added: number[] = [] + let i = 0 + let j = 0 + while (i < left.length || j < right.length) { + if (i < left.length && j < right.length && left[i] === right[j]) { + i++ + j++ + } else if (j < right.length && (i === left.length || lengths[i][j + 1] > lengths[i + 1][j])) { + added.push(j++) + } else { + removed.push(i++) + } + } + return { before: removed, after: added } +} + +export function sourceChange( + path: string, + content: string, + directory: string, + output: Record, +): { kind: FileChange; lines: number[] } { + const prefix = directory ? `${directory}/` : '' + if (!path.startsWith(prefix)) return { kind: 'outside', lines: [] } + const next = output[path.slice(prefix.length)] + if (next === undefined) return { kind: 'removed', lines: codeLines(content).map((_, index) => index) } + if (next === content) return { kind: 'unchanged', lines: [] } + return { kind: 'modified', lines: changedLines(content, next).before } +} diff --git a/docs/src/examples/code-types.ts b/docs/src/examples/code-types.ts new file mode 100644 index 0000000..11b61dc --- /dev/null +++ b/docs/src/examples/code-types.ts @@ -0,0 +1,31 @@ +export type FileChange = 'unchanged' | 'modified' | 'added' | 'removed' | 'outside' + +export interface CodeToken { + text: string + light?: string + dark?: string +} + +export interface ExampleFile { + path: string + lines: CodeToken[][] + change: FileChange + changedLines: number[] +} + +export interface PresetView { + id: string + label: { en: string; zh: string } + target: string + directory: string + source: ExampleFile[] + config: ExampleFile[] + excerpt: ExampleFile + output: ExampleFile[] +} + +export interface ExampleView { + id: string + repository: string + presets: PresetView[] +} diff --git a/docs/src/examples/code-view.module.css b/docs/src/examples/code-view.module.css new file mode 100644 index 0000000..71a3f94 --- /dev/null +++ b/docs/src/examples/code-view.module.css @@ -0,0 +1,79 @@ +.code { + margin: 0; + max-height: 32rem; + overflow: auto; + padding-block: 1.25rem; + font-family: var(--font-mono); + font-size: 0.8125rem; + line-height: 1.8; + tab-size: 2; + background: hsl(var(--background)); +} + +.code > code { + display: block; + min-width: max-content; +} + +.compact { + font-size: 0.75rem; + max-height: 20rem; +} + +.line { + display: flex; + padding-right: 1rem; + min-height: 1.8em; +} + +.number { + width: 2.75rem; + flex-shrink: 0; + padding-right: 0.875rem; + text-align: right; + color: hsl(var(--muted-foreground) / 0.6); + user-select: none; +} + +.token { + color: var(--code-light); +} + +:global(.dark) .token { + color: var(--code-dark); +} + +.line[data-edit='source'] { + background: rgb(217 119 6 / 0.09); + box-shadow: inset 2px 0 #b7791f; +} + +.line[data-edit='output'] { + background: rgb(16 185 129 / 0.09); + box-shadow: inset 2px 0 #059669; +} + +.mark { + flex-shrink: 0; + font-size: 0.625rem; + line-height: 1.6; + color: hsl(var(--muted-foreground)); +} + +.mark[data-change='modified'], +.mark[data-change='removed'] { + color: #9a6700; +} + +.mark[data-change='added'] { + color: #047857; +} + +:global(.dark) .mark[data-change='modified'], +:global(.dark) .mark[data-change='removed'] { + color: #e9b65e; +} + +:global(.dark) .mark[data-change='added'] { + color: #6ee7b7; +} diff --git a/docs/src/examples/code-view.tsx b/docs/src/examples/code-view.tsx new file mode 100644 index 0000000..e225ef7 --- /dev/null +++ b/docs/src/examples/code-view.tsx @@ -0,0 +1,33 @@ +import type { CSSProperties } from 'react' +import type { ExampleFile, FileChange } from './code-types' +import styles from './code-view.module.css' + +export const changeLabels: Record<'zh' | 'en', Record> = { + zh: { unchanged: '保留', modified: '修改', added: '新增', removed: '排除', outside: '未选取' }, + en: { unchanged: 'Kept', modified: 'Edited', added: 'Added', removed: 'Excluded', outside: 'Not selected' }, +} + +export function ChangeMark({ change, locale }: { change: FileChange; locale: 'zh' | 'en' }) { + return {changeLabels[locale][change]} +} + +export function CodeView({ file, side = 'output', compact = false }: { + file: ExampleFile + side?: 'source' | 'config' | 'output' + compact?: boolean +}) { + return ( +
+      
+        {file.lines.map((line, index) => (
+          
+            
+            {line.map((token, tokenIndex) => (
+              {token.text}
+            ))}{line.length === 0 ? ' ' : ''}{'\n'}
+          
+        ))}
+      
+    
+ ) +} diff --git a/docs/src/examples/copy.ts b/docs/src/examples/copy.ts new file mode 100644 index 0000000..e61cbb5 --- /dev/null +++ b/docs/src/examples/copy.ts @@ -0,0 +1,70 @@ +export const exampleCopy = { + zh: { + title: '这些步骤,每个项目都要再做一遍?', + description: '从一个常见问题开始,看看源文件、Preset 配置和生成结果。', + before: '手动维护', + after: '使用 Create Preset', + view: '查看示例', + all: '使用示例', + scenarios: [ + { + id: 'variants', + title: '模板差别不大,却要维护好几份?', + before: '基础版、团队版各复制一份源码。修一个问题,每份模板都要检查和修改。', + after: '共用一份源码,把文件和配置差异写进 Preset,创建时生成需要的版本。', + }, + { + id: 'skills', + title: '每个项目都要复制同一批 Skills?', + before: '找到常用 Skills,逐个复制到项目目录,再删掉这次用不上的。', + after: '常用 Skills 集中放在一个仓库,按配置选取,一次引入到项目的 Skills 目录。', + }, + { + id: 'monorepo', + title: '只想用 monorepo 里的一个子项目?', + before: '克隆整个仓库,取出子目录,删除无关文件,再处理 workspace: 依赖。', + after: '指定子目录、排除文件和依赖处理规则,直接生成独立项目。', + }, + { + id: 'file-changes', + title: '模板拉下来,每次都要改同样的配置?', + before: '删除示例文件、修改 README、替换配置值,每次创建项目都再做一遍。', + after: '将固定步骤写进 Preset,创建项目时一起完成。', + }, + ], + }, + en: { + title: 'Repeating the same steps for every project?', + description: 'Start with a familiar problem. Compare the source files, preset config, and generated output.', + before: 'Manual upkeep', + after: 'With Create Preset', + view: 'View example', + all: 'Examples', + scenarios: [ + { + id: 'variants', + title: 'Almost identical templates, maintained separately?', + before: 'Keep separate copies for basic and team editions. Check and fix the same code in every copy.', + after: 'Keep one source. Describe the file and config differences in presets and generate the edition needed.', + }, + { + id: 'skills', + title: 'Copying the same skills into every project?', + before: 'Find the usual skills, copy them one by one, then remove the ones this project does not need.', + after: 'Keep common skills in one repository. Select them with a preset and bring them into a project together.', + }, + { + id: 'monorepo', + title: 'Only need one project from a monorepo?', + before: 'Clone the repository, extract a directory, remove unrelated files, then fix workspace: dependencies.', + after: 'Choose the directory, exclusions, and dependency rules to generate a standalone project.', + }, + { + id: 'file-changes', + title: 'Making the same edits after every clone?', + before: 'Remove sample files, rewrite the README, and change config values every time a project is created.', + after: 'Describe those steps in a preset and apply them during project creation.', + }, + ], + }, +} as const diff --git a/docs/src/examples/example-explorer-client.tsx b/docs/src/examples/example-explorer-client.tsx new file mode 100644 index 0000000..b73eb37 --- /dev/null +++ b/docs/src/examples/example-explorer-client.tsx @@ -0,0 +1,106 @@ +'use client' + +import { useId, useState } from 'react' +import { Tabs, TabsContent, TabsList, TabsTrigger } from 'blackwork' +import type { ExampleFile, ExampleView } from './code-types' +import { ChangeMark, CodeView } from './code-view' + +type Locale = 'en' | 'zh' + +const labels = { + zh: { + source: '源文件', config: 'Preset 配置', output: '生成结果', + files: '文件列表', edition: '生成版本', command: '创建命令', + note: '示例仓库为虚构地址。替换仓库地址,将配置保存到命令中的本地路径后再运行。生成结果是对应配置的静态演示。', + }, + en: { + source: 'Source', config: 'Preset config', output: 'Output', + files: 'Files', edition: 'Edition', command: 'Create command', + note: 'Repository URLs are placeholders. Replace the repository URL and save the config at the local path used below before running. Output files are a static demonstration of the selected preset.', + }, +} + +function FilePanel({ files, locale, root, side }: { + files: ExampleFile[] + locale: Locale + root: string + side: 'source' | 'config' | 'output' +}) { + const [selected, setSelected] = useState(files[0].path) + const file = files.find((item) => item.path === selected) ?? files[0] + const panelId = useId() + + return ( +
+ +
+

{selected}

+
+
+
+ ) +} + +export function ExampleExplorerClient({ data, locale }: { data: ExampleView; locale: Locale }) { + const copy = labels[locale] + const [presetIndex, setPresetIndex] = useState(0) + const preset = data.presets[presetIndex] + const selectId = useId() + const command = `pnpm create preset init ${preset.target} --from ./presets/${preset.id}.json` + const panels = [ + { id: 'source', title: copy.source, files: preset.source, root: data.repository }, + { id: 'config', title: copy.config, files: preset.config, root: 'presets' }, + { id: 'output', title: copy.output, files: preset.output, root: preset.target }, + ] as const + + return ( +
+

{copy.note}

+ {data.presets.length > 1 && ( +
+ + +
+ )} + +
+ + {panels.map((panel) => {panel.title})} + +
+ {panels.map((panel) => ( + + + + ))} +
+

{copy.command}

+
{command}
+
+ ) +} diff --git a/docs/src/examples/example-explorer.tsx b/docs/src/examples/example-explorer.tsx new file mode 100644 index 0000000..1131879 --- /dev/null +++ b/docs/src/examples/example-explorer.tsx @@ -0,0 +1,10 @@ +import type fixtures from './fixtures.json' +import { getExampleView } from './highlight.server' +import { ExampleExplorerClient } from './example-explorer-client' + +export async function ExampleExplorer({ example, locale = 'en' }: { + example: keyof typeof fixtures + locale?: 'en' | 'zh' +}) { + return +} diff --git a/docs/src/examples/example-list.tsx b/docs/src/examples/example-list.tsx new file mode 100644 index 0000000..f0e6808 --- /dev/null +++ b/docs/src/examples/example-list.tsx @@ -0,0 +1,25 @@ +import Link from 'next/link' +import { exampleCopy } from './copy' + +export function ExampleList({ locale = 'en' }: { locale?: 'en' | 'zh' }) { + const copy = exampleCopy[locale] + const prefix = locale === 'zh' ? '/zh' : '' + + return ( +
+ {copy.scenarios.map((scenario) => ( +
+

+ + {scenario.title} + +

+

{scenario.after}

+
+ ))} +
+ ) +} diff --git a/docs/src/examples/fixtures.json b/docs/src/examples/fixtures.json new file mode 100644 index 0000000..5e251ed --- /dev/null +++ b/docs/src/examples/fixtures.json @@ -0,0 +1,222 @@ +{ + "variants": { + "repo": "https://github.com/example/app-starter.git", + "sourceFiles": { + "starter/src/main.ts": "import { track } from './analytics'\n\ntrack('app-started')\nconsole.log('Hello')\n", + "starter/src/analytics.ts": "export function track(event: string) {\n console.log('[analytics]', event)\n}\n", + "starter/package.json": "{\n \"name\": \"app-starter\",\n \"version\": \"1.0.0\",\n \"private\": true\n}\n" + }, + "presets": [ + { + "id": "basic", + "label": { + "zh": "基础版", + "en": "Basic" + }, + "target": "my-basic-app", + "config": { + "$schema": "https://preset.js.org/schema/preset.schema.json", + "version": 1, + "source": { + "repo": "https://github.com/example/app-starter.git", + "directory": "starter" + }, + "exclude": [ + "src/analytics.ts" + ], + "replace": [ + { + "path": "src/main.ts", + "from": "import { track } from './analytics'\n\ntrack('app-started')\n", + "to": "" + } + ] + }, + "outputFiles": { + "src/main.ts": "console.log('Hello')\n", + "package.json": "{\n \"name\": \"my-basic-app\",\n \"version\": \"1.0.0\",\n \"private\": true\n}\n" + } + }, + { + "id": "team", + "label": { + "zh": "团队版", + "en": "Team" + }, + "target": "my-team-app", + "config": { + "$schema": "https://preset.js.org/schema/preset.schema.json", + "version": 1, + "source": { + "repo": "https://github.com/example/app-starter.git", + "directory": "starter" + } + }, + "outputFiles": { + "src/main.ts": "import { track } from './analytics'\n\ntrack('app-started')\nconsole.log('Hello')\n", + "src/analytics.ts": "export function track(event: string) {\n console.log('[analytics]', event)\n}\n", + "package.json": "{\n \"name\": \"my-team-app\",\n \"version\": \"1.0.0\",\n \"private\": true\n}\n" + } + } + ] + }, + "skills": { + "repo": "https://github.com/example/team-resources.git", + "sourceFiles": { + "skills/code-review/SKILL.md": "---\nname: code-review\ndescription: Review code changes for correctness and edge cases.\n---\n\nRead the diff. Report concrete problems with file references.\n", + "skills/writing/SKILL.md": "---\nname: writing\ndescription: Edit project documentation for clarity.\n---\n\nUse short, direct sentences. Keep commands and API names accurate.\n", + "skills/testing/SKILL.md": "---\nname: testing\ndescription: Check changed behavior with focused tests.\n---\n\nRead the project test commands. Cover the changed behavior and failure paths.\n" + }, + "presets": [ + { + "id": "skills", + "label": { + "zh": "全部 Skills", + "en": "All skills" + }, + "target": ".agents/skills", + "config": { + "$schema": "https://preset.js.org/schema/preset.schema.json", + "version": 1, + "source": { + "repo": "https://github.com/example/team-resources.git", + "directory": "skills" + } + }, + "outputFiles": { + "code-review/SKILL.md": "---\nname: code-review\ndescription: Review code changes for correctness and edge cases.\n---\n\nRead the diff. Report concrete problems with file references.\n", + "writing/SKILL.md": "---\nname: writing\ndescription: Edit project documentation for clarity.\n---\n\nUse short, direct sentences. Keep commands and API names accurate.\n", + "testing/SKILL.md": "---\nname: testing\ndescription: Check changed behavior with focused tests.\n---\n\nRead the project test commands. Cover the changed behavior and failure paths.\n" + } + }, + { + "id": "skills-without-testing", + "label": { + "zh": "不含测试 Skill", + "en": "Without testing" + }, + "target": ".agents/skills", + "config": { + "$schema": "https://preset.js.org/schema/preset.schema.json", + "version": 1, + "source": { + "repo": "https://github.com/example/team-resources.git", + "directory": "skills" + }, + "exclude": [ + "testing" + ] + }, + "outputFiles": { + "code-review/SKILL.md": "---\nname: code-review\ndescription: Review code changes for correctness and edge cases.\n---\n\nRead the diff. Report concrete problems with file references.\n", + "writing/SKILL.md": "---\nname: writing\ndescription: Edit project documentation for clarity.\n---\n\nUse short, direct sentences. Keep commands and API names accurate.\n" + } + } + ] + }, + "monorepo": { + "repo": "https://github.com/example/product.git", + "sourceFiles": { + "apps/docs/src/page.ts": "import { Button } from '@example/ui'\n\nexport const action = Button({ label: 'Get started' })\n", + "apps/docs/package.json": "{\n \"name\": \"docs-starter\",\n \"version\": \"1.0.0\",\n \"private\": true,\n \"dependencies\": {\n \"@example/ui\": \"workspace:*\"\n }\n}\n", + "apps/docs/src/__tests__/page.test.ts": "// Tests used while maintaining this starter.\n", + "packages/ui/package.json": "{\n \"name\": \"@example/ui\",\n \"version\": \"1.2.3\"\n}\n", + "pnpm-workspace.yaml": "packages:\n - apps/*\n - packages/*\n" + }, + "presets": [ + { + "id": "docs", + "label": { + "zh": "独立文档项目", + "en": "Standalone docs" + }, + "target": "my-docs", + "config": { + "$schema": "https://preset.js.org/schema/preset.schema.json", + "version": 1, + "source": { + "repo": "https://github.com/example/product.git", + "directory": "apps/docs" + }, + "exclude": [ + "src/__tests__" + ], + "packageJson": { + "resolveWorkspace": true, + "workspaceVersions": { + "@example/ui": "1.2.3" + } + } + }, + "outputFiles": { + "src/page.ts": "import { Button } from '@example/ui'\n\nexport const action = Button({ label: 'Get started' })\n", + "package.json": "{\n \"name\": \"my-docs\",\n \"version\": \"1.0.0\",\n \"private\": true,\n \"dependencies\": {\n \"@example/ui\": \"1.2.3\"\n }\n}\n" + } + } + ] + }, + "file-changes": { + "repo": "https://github.com/example/service-starter.git", + "sourceFiles": { + "index.js": "console.log('Service ready')\n", + "demo.js": "console.log('Sample data')\n", + "README.md": "# Service starter\n\nA template with sample data.\n", + ".env.example": "APP_LABEL=Starter\n", + "package.json": "{\n \"name\": \"service-starter\",\n \"version\": \"1.0.0\",\n \"private\": true,\n \"scripts\": {\n \"start\": \"node index.js\",\n \"demo\": \"node demo.js\"\n }\n}\n" + }, + "presets": [ + { + "id": "service", + "label": { + "zh": "团队服务", + "en": "Team service" + }, + "target": "my-service", + "config": { + "$schema": "https://preset.js.org/schema/preset.schema.json", + "version": 1, + "source": { + "repo": "https://github.com/example/service-starter.git", + "directory": "" + }, + "exclude": [ + "demo.js" + ], + "write": [ + { + "path": "README.md", + "lines": [ + "# Team service", + "", + "Run `npm start` to start the service." + ] + } + ], + "replace": [ + { + "path": ".env.example", + "from": "APP_LABEL=Starter", + "to": "APP_LABEL=Team" + } + ], + "json": [ + { + "path": "package.json", + "value": { + "scripts": { + "demo": null + } + } + } + ] + }, + "outputFiles": { + "index.js": "console.log('Service ready')\n", + "README.md": "# Team service\n\nRun `npm start` to start the service.\n", + ".env.example": "APP_LABEL=Team\n", + "package.json": "{\n \"name\": \"my-service\",\n \"version\": \"1.0.0\",\n \"private\": true,\n \"scripts\": {\n \"start\": \"node index.js\"\n }\n}\n" + } + } + ] + } +} diff --git a/docs/src/examples/highlight.server.ts b/docs/src/examples/highlight.server.ts new file mode 100644 index 0000000..93268c7 --- /dev/null +++ b/docs/src/examples/highlight.server.ts @@ -0,0 +1,73 @@ +import { cache } from 'react' +import { createHighlighter } from 'shiki' +import fixtures from './fixtures.json' +import { changedLines, sourceChange } from './changes' +import type { ExampleFile, ExampleView } from './code-types' + +let highlighter: ReturnType | undefined + +async function highlight(path: string, content: string): Promise { + highlighter ??= createHighlighter({ + themes: ['github-light', 'github-dark'], + langs: ['json', 'typescript', 'javascript', 'markdown', 'yaml', 'dotenv'], + }) + const syntax = await highlighter + const lang = path.endsWith('.json') ? 'json' + : path.endsWith('.ts') ? 'typescript' + : path.endsWith('.js') ? 'javascript' + : path.endsWith('.md') ? 'markdown' + : path.endsWith('.yaml') ? 'yaml' + : path.startsWith('.env') ? 'dotenv' : 'text' + return syntax.codeToTokensWithThemes(content.replace(/\n$/, ''), { + lang, + themes: { light: 'github-light', dark: 'github-dark' }, + }).map((line) => line.map((token) => ({ + text: token.content, + light: token.variants.light.color, + dark: token.variants.dark.color, + }))) +} + +export const getExampleView = cache(async (id: keyof typeof fixtures): Promise => { + const fixture = fixtures[id] + const sourceFiles: Record = fixture.sourceFiles + return { + id, + repository: fixture.repo.split('/').pop()!.replace('.git', ''), + presets: await Promise.all(fixture.presets.map(async (preset) => { + const outputFiles = preset.outputFiles as Record + const directory = preset.config.source.directory + const prefix = directory ? `${directory}/` : '' + const configText = JSON.stringify(preset.config, null, 2) + // The homepage labels this as an excerpt; the full config remains in Examples. + const excerpt = { + source: { directory }, + ...Object.fromEntries(Object.entries(preset.config).filter(([key]) => + !['$schema', 'version', 'source'].includes(key), + )), + } + return { + id: preset.id, + label: preset.label, + target: preset.target, + directory, + source: await Promise.all(Object.entries(sourceFiles).map(async ([path, content]) => { + const change = sourceChange(path, content, directory, outputFiles) + return { path, lines: await highlight(path, content), change: change.kind, changedLines: change.lines } + })), + config: [{ path: `${preset.id}.json`, lines: await highlight('preset.json', configText), change: 'unchanged' as const, changedLines: [] }], + excerpt: { path: `${preset.id}.json`, lines: await highlight('preset.json', JSON.stringify(excerpt, null, 2)), change: 'unchanged' as const, changedLines: [] }, + output: await Promise.all(Object.entries(outputFiles).map(async ([path, content]) => { + const original = sourceFiles[`${prefix}${path}`] + const lines = await highlight(path, content) + return { + path, + lines, + change: original === undefined ? 'added' as const : original === content ? 'unchanged' as const : 'modified' as const, + changedLines: original === undefined ? lines.map((_, index) => index) : changedLines(original, content).after, + } + })), + } + })), + } +}) diff --git a/docs/src/site/navigation.ts b/docs/src/site/navigation.ts index f960624..c34d74f 100644 --- a/docs/src/site/navigation.ts +++ b/docs/src/site/navigation.ts @@ -1,6 +1,10 @@ import { docsContentConfig } from '../../content.config' export const headerNavigation = [ + { + href: '/examples', + label: { en: 'Examples', zh: '使用示例' }, + }, { href: '/guide/getting-started', label: { en: 'Guide', zh: '指南' }, From a48bfe6205ffbd5cbd4fc3c12bafc4c45d90b9ed Mon Sep 17 00:00:00 2001 From: chengpeiquan Date: Sun, 11 Oct 2026 23:59:32 +0800 Subject: [PATCH 2/5] feat(website): showcase preset transformations on the homepage --- docs/docs.config.ts | 4 +- docs/src/home/preset-home.module.css | 147 +++++++++++++-- docs/src/home/preset-home.tsx | 110 ++++-------- docs/src/home/scenario-demo.module.css | 238 +++++++++++++++++++++++++ docs/src/home/scenario-demo.tsx | 144 +++++++++++++++ docs/src/home/scenarios.tsx | 9 + docs/src/home/source-diagram.tsx | 38 ++++ 7 files changed, 602 insertions(+), 88 deletions(-) create mode 100644 docs/src/home/scenario-demo.module.css create mode 100644 docs/src/home/scenario-demo.tsx create mode 100644 docs/src/home/scenarios.tsx create mode 100644 docs/src/home/source-diagram.tsx diff --git a/docs/docs.config.ts b/docs/docs.config.ts index 9f56260..05cc600 100644 --- a/docs/docs.config.ts +++ b/docs/docs.config.ts @@ -28,8 +28,8 @@ export const docsConfig = defineDocsConfig({ eyebrow: false, title: 'Create Preset', description: { - en: 'Provides the ability to quickly create preset projects.', - zh: '提供快速创建预设项目模板的能力。', + en: 'Similar templates. Still maintaining separate copies?', + zh: '模板相似,却要维护多份?', }, primaryAction: { href: '/guide/getting-started', diff --git a/docs/src/home/preset-home.module.css b/docs/src/home/preset-home.module.css index b03d6bf..0d00f00 100644 --- a/docs/src/home/preset-home.module.css +++ b/docs/src/home/preset-home.module.css @@ -1,26 +1,143 @@ -/* Preserve the mint banner and toolbox identity from the VitePress homepage. */ .hero { - background-color: #d1fae5; + border-bottom: 1px solid hsl(var(--border)); + background: hsl(var(--muted) / 0.18); } -:global(.dark) .hero { - background-color: #a7f3d0; +.heroInner { + display: grid; + grid-template-columns: 1.2fr 1fr; + align-items: center; + gap: 4rem; + width: 91.6667%; + max-width: 72rem; + margin-inline: auto; + padding-block: 4rem; } +.brand { + display: flex; + align-items: center; + gap: 0.75rem; + margin-bottom: 2rem; + font-size: 0.875rem; + font-weight: 600; +} + +.brandRule { height: 1rem; width: 1px; background: hsl(var(--border)); } +.brandNote { font-size: 0.6875rem; font-weight: 400; color: hsl(var(--muted-foreground)); } + .title { - color: #047857; - background-image: linear-gradient(120deg, #34d399 30%, #047857); - background-clip: text; - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; + font-size: clamp(2.25rem, 4.6vw, 3.75rem); + font-weight: 600; + letter-spacing: -0.06em; + line-height: 1.3; +} + +.title > span { color: #047857; } +:global(.dark) .title > span { color: #6ee7b7; } + +.description { + margin-top: 1.5rem; + max-width: 31rem; + font-size: 0.9375rem; + line-height: 1.95; + color: hsl(var(--muted-foreground)); + text-wrap: pretty; } -.badgeLink:focus-visible { - outline: 2px solid #065f46; - outline-offset: 4px; +.heroMeta { display: flex; align-items: center; flex-wrap: wrap; gap: 1.25rem; margin-top: 1.75rem; font-size: 0.6875rem; color: hsl(var(--muted-foreground)); } +.heroMeta a:focus-visible { outline: 2px solid currentColor; outline-offset: 4px; } +.heroMeta a:hover { color: hsl(var(--foreground)); } +.badgeLink { display: inline-flex; border-radius: 2px; } + +.diagram { + min-width: 0; + padding: 1.5rem 0; +} + +.source { + position: relative; + width: 75%; + margin-inline: auto; + border: 1px solid rgb(5 150 105 / 0.35); + border-radius: 0.625rem; + background: hsl(var(--background)); + box-shadow: 0 8px 24px -16px rgb(5 150 105 / 0.3); +} + +.diagramHeader { + display: flex; + align-items: center; + gap: 0.5rem; + border-bottom: 1px solid hsl(var(--border)); + padding: 0.875rem 1rem; + font: 0.75rem var(--font-mono); +} + +.diagramHeader > svg { color: #059669; } +.sourceTag { margin-left: auto; font-family: var(--blackwork-font-sans); font-size: 0.625rem; color: #047857; } +:global(.dark) .sourceTag { color: #6ee7b7; } +.sourceFiles { padding: 0.75rem 1rem; font: 0.75rem / 2 var(--font-mono); } +.sourceFiles p { display: flex; align-items: center; gap: 0.5rem; } +.sourceFiles svg { opacity: 0.35; } + +.branch { + position: relative; + height: 3.75rem; +} + +.branch::before { + content: ''; + position: absolute; + left: 50%; + top: 0; + height: 1.875rem; + border-left: 1px solid hsl(var(--border)); +} + +.branch > span { + position: absolute; + inset: 1.875rem 25% 0; + border: 1px solid hsl(var(--border)); + border-bottom: 0; + border-radius: 0.5rem 0.5rem 0 0; +} + +.branch > svg { + position: absolute; + left: calc(50% - 0.75rem); + top: 1.125rem; + width: 1.5rem; + height: 1.5rem; + padding: 0.25rem; + border: 1px solid hsl(var(--border)); + border-radius: 50%; + background: hsl(var(--background)); + color: hsl(var(--muted-foreground)); +} + +.outputs { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; } +.edition { min-width: 0; border: 1px solid hsl(var(--border)); border-radius: 0.5rem; background: hsl(var(--background)); } +.presetLabel { padding: 0.625rem 0.875rem; border-bottom: 1px solid hsl(var(--border)); font: 0.6875rem var(--font-mono); color: hsl(var(--muted-foreground)); } +.editionBody { padding: 0.875rem; font: 0.625rem / 2 var(--font-mono); } +.editionTitle { display: flex; align-items: center; gap: 0.5rem; margin-bottom: 0.375rem; font-family: var(--blackwork-font-sans); font-size: 0.75rem; font-weight: 600; } +.statusDot { width: 0.3125rem; height: 0.3125rem; border-radius: 50%; background: #059669; } +.excluded { color: hsl(var(--muted-foreground)); text-decoration: line-through; text-decoration-color: #b7791f; } +.excluded > span { float: right; color: #9a6700; text-decoration: none; } +.diagram figcaption { margin-top: 1.5rem; text-align: center; font-size: 0.6875rem; color: hsl(var(--muted-foreground)); } + +@media (width < 64rem) { + .heroInner { gap: 2rem; } + .title { font-size: 2.625rem; } + .source { width: 90%; } } -.badgeLink { - display: inline-flex; - border-radius: 2px; +@media (width < 48rem) { + .heroInner { grid-template-columns: 1fr; gap: 2rem; padding-block: 2.5rem; } + .brand { margin-bottom: 1.5rem; } + .title { font-size: clamp(1.875rem, 7.8vw, 2.625rem); } + .description { font-size: 0.875rem; } + .diagram { width: 100%; max-width: 26rem; margin-inline: auto; padding-block: 0; } + .source { width: 80%; } + .brandNote { font-size: 0.625rem; } } diff --git a/docs/src/home/preset-home.tsx b/docs/src/home/preset-home.tsx index 4fa5a9e..88a77fd 100644 --- a/docs/src/home/preset-home.tsx +++ b/docs/src/home/preset-home.tsx @@ -17,6 +17,9 @@ import { PresetFooter } from '@/site/footer' import { DocsSiteLink } from '@/site/docs-link' import { cn } from '@/utils/class-name' import { homeCopy } from './copy' +import { exampleCopy } from '@/examples/copy' +import { HomeScenarios } from './scenarios' +import { SourceDiagram } from './source-diagram' import styles from './preset-home.module.css' const githubUrl = 'https://github.com/preset-cli/create-preset' @@ -31,6 +34,8 @@ export function PresetHome({ source: DocsSource }) { const copy = homeCopy[entry.locale === 'zh' ? 'zh' : 'en'] + const locale = entry.locale === 'zh' ? 'zh' : 'en' + const examplesHref = `${locale === 'zh' ? '/zh' : ''}/examples` const home = createHomeData({ config, locale: entry.locale, source }) const navigation = buildHeaderNavigation({ config, @@ -88,89 +93,52 @@ export function PresetHome({ data-pagefind-body="" >