+
diff --git a/docs/src/home/scenario-demo.module.css b/docs/src/home/scenario-demo.module.css
new file mode 100644
index 0000000..7884c5d
--- /dev/null
+++ b/docs/src/home/scenario-demo.module.css
@@ -0,0 +1,231 @@
+.section {
+ scroll-margin-top: 6rem;
+ padding-block: 4rem;
+}
+
+.heading {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: end;
+ justify-content: space-between;
+ gap: 1rem 2rem;
+ margin-bottom: 2rem;
+}
+
+.eyebrow {
+ margin-bottom: 0.875rem;
+ font: 0.6875rem var(--font-mono);
+ letter-spacing: 0.12em;
+ color: hsl(var(--muted-foreground));
+}
+
+.heading h2 {
+ font-size: clamp(1.4rem, 2.5vw, 2rem);
+ font-weight: 600;
+ letter-spacing: -0.035em;
+ text-wrap: balance;
+}
+
+.heading > p {
+ font-size: 0.8125rem;
+ line-height: 1.8;
+ color: hsl(var(--muted-foreground));
+}
+
+.tabs {
+ display: grid;
+ grid-template-columns: repeat(4, minmax(0, 1fr));
+ width: 100%;
+ height: auto;
+ padding: 0;
+ border-bottom: 1px solid hsl(var(--border));
+ border-radius: 0;
+ background: none;
+ gap: 0;
+}
+
+.tab {
+ height: auto;
+ justify-content: start;
+ padding: 1rem 1.25rem;
+ border: 0;
+ border-bottom: 2px solid transparent;
+ border-radius: 0;
+ font-size: 0.875rem;
+ gap: 0.75rem;
+ white-space: normal;
+}
+
+.tab[aria-selected='true'] {
+ border-bottom-color: #059669;
+ background: rgb(16 185 129 / 0.06);
+ box-shadow: none;
+}
+
+.tabNumber {
+ font-family: var(--font-mono);
+ font-size: 0.625rem;
+ opacity: 0.5;
+}
+
+.intro {
+ padding-block: 1.75rem;
+}
+
+.intro h3 {
+ font-weight: 600;
+ font-size: 1.125rem;
+ margin-bottom: 1rem;
+}
+
+.comparison {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 2rem;
+}
+
+.comparison p {
+ font-size: 0.8125rem;
+ line-height: 1.9;
+ color: hsl(var(--muted-foreground));
+}
+
+.comparison span {
+ display: block;
+ color: hsl(var(--foreground));
+ font-size: 0.6875rem;
+ margin-bottom: 0.375rem;
+}
+
+.workbench {
+ overflow: hidden;
+ border: 1px solid hsl(var(--border));
+ border-radius: 0.75rem;
+ box-shadow: 0 12px 40px -24px rgb(0 0 0 / 0.25);
+}
+
+.toolbar {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ justify-content: space-between;
+ gap: 0.75rem;
+ padding: 0.625rem 1.25rem;
+ border-bottom: 1px solid hsl(var(--border));
+ background: hsl(var(--muted) / 0.35);
+}
+
+.pipeline {
+ display: grid;
+ grid-template-columns: 1fr 1.1fr 1fr;
+}
+
+.pane {
+ min-width: 0;
+ min-height: 23rem;
+}
+
+.pane + .pane {
+ border-left: 1px solid hsl(var(--border));
+}
+
+.step {
+ display: flex;
+ align-items: center;
+ gap: 0.625rem;
+ padding: 1rem 1.25rem;
+ border-bottom: 1px solid hsl(var(--border));
+ font-size: 0.75rem;
+}
+
+.step > span:first-child {
+ font-family: var(--font-mono);
+ color: hsl(var(--muted-foreground));
+ font-size: 0.625rem;
+}
+
+.step h4 { font-weight: 600; }
+.step > svg { margin-left: auto; color: hsl(var(--muted-foreground)); }
+.outputStep { background: rgb(16 185 129 / 0.06); }
+
+.root {
+ padding: 1rem 1.25rem 0.75rem;
+ font: 0.6875rem / 1.7 var(--font-mono);
+ overflow-wrap: anywhere;
+ color: hsl(var(--muted-foreground));
+}
+
+.files {
+ padding: 0 0.875rem 1rem;
+ min-height: 10rem;
+}
+
+.files li {
+ display: flex;
+ align-items: baseline;
+ gap: 0.5rem;
+ padding: 0.3125rem 0.375rem;
+ font: 0.6875rem / 1.6 var(--font-mono);
+}
+
+.files svg { flex-shrink: 0; align-self: center; opacity: 0.45; }
+.path { min-width: 0; flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
+.files [data-change='removed'] .path { text-decoration: line-through; opacity: 0.55; }
+.files [data-change='outside'] .path { opacity: 0.45; }
+
+.codeTitle {
+ border-block: 1px solid hsl(var(--border));
+ padding: 0.625rem 1.25rem;
+ font: 0.625rem var(--font-mono);
+ color: hsl(var(--muted-foreground));
+}
+
+.rules { background: hsl(var(--muted) / 0.15); }
+.rules pre { background: transparent; }
+
+.legend {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.5rem 1.25rem;
+ border-top: 1px solid hsl(var(--border));
+ padding: 0.75rem 1.25rem;
+ font-size: 0.625rem;
+ color: hsl(var(--muted-foreground));
+}
+
+.legend span { display: flex; align-items: center; gap: 0.5rem; }
+.legend i { width: 0.375rem; height: 0.375rem; border-radius: 1px; }
+.amber { background: #b7791f; }
+.green { background: #059669; }
+
+.command {
+ min-width: 0;
+ padding-inline: 1.25rem;
+ border-top: 1px solid hsl(var(--border));
+}
+
+.caption {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ justify-content: space-between;
+ gap: 1rem;
+ margin-top: 1rem;
+ font-size: 0.6875rem;
+}
+
+.caption p { color: hsl(var(--muted-foreground)); }
+.caption a { display: inline-flex; align-items: center; gap: 0.5rem; font-size: 0.8125rem; font-weight: 500; }
+.caption a:hover { text-decoration: underline; text-underline-offset: 4px; }
+.caption a:focus-visible { outline: 2px solid currentColor; outline-offset: 4px; }
+
+@media (width < 48rem) {
+ .section { padding-block: 2.5rem; }
+ .tabs { grid-template-columns: repeat(2, minmax(0, 1fr)); }
+ .tab { padding: 0.875rem 0.5rem; font-size: 0.75rem; }
+ .comparison { grid-template-columns: 1fr; gap: 1rem; }
+ .pipeline { grid-template-columns: 1fr; }
+ .pane { min-height: auto; }
+ .pane + .pane { border-left: 0; border-top: 1px solid hsl(var(--border)); }
+ .files { min-height: auto; }
+}
diff --git a/docs/src/home/scenario-demo.tsx b/docs/src/home/scenario-demo.tsx
new file mode 100644
index 0000000..7a2f56e
--- /dev/null
+++ b/docs/src/home/scenario-demo.tsx
@@ -0,0 +1,144 @@
+'use client'
+
+import { useState } from 'react'
+import Link from 'next/link'
+import { Tabs, TabsList, TabsTrigger, TabsContent, Button } from 'blackwork'
+import { ArrowRight, FileCode2, FolderOpen, SlidersHorizontal } from 'lucide-react'
+import { exampleCopy } from '@/examples/copy'
+import type { ExampleFile, ExampleView, PresetView } from '@/examples/code-types'
+import { CodeView, ChangeMark } from '@/examples/code-view'
+import { CommandBlock } from '@/mdx/components/command-block'
+import styles from './scenario-demo.module.css'
+
+const ui = {
+ zh: {
+ tabs: ['模板重复维护', '复用 Skills', '提取子项目', '调整文件'],
+ source: '源文件', config: '配置差异', output: '生成结果',
+ excerpt: '关键配置节选', edition: '切换生成版本',
+ sourceHint: '琥珀色标出需要处理的内容', outputHint: '绿色标出生成时改写的行',
+ footnote: '虚构仓库演示 · 不会在浏览器中执行命令',
+ view: '查看完整示例', browse: '选择一个场景,看看文件如何变化。',
+ },
+ en: {
+ tabs: ['Template variants', 'Shared skills', 'Monorepo projects', 'File changes'],
+ source: 'Source files', config: 'Preset rules', output: 'Generated files',
+ excerpt: 'Config excerpt', edition: 'Choose an edition',
+ sourceHint: 'Amber marks content to change', outputHint: 'Green marks rewritten lines',
+ footnote: 'Fictional repository · Commands are not run in the browser',
+ view: 'Explore the example', browse: 'Choose a scenario to see what happens to the files.',
+ },
+}
+
+function FileRows({ files, locale, directory = '' }: { files: ExampleFile[]; locale: 'en' | 'zh'; directory?: string }) {
+ return (
+
+ {files.map((file) => (
+
+
+ {directory && file.path.startsWith(`${directory}/`) ? file.path.slice(directory.length + 1) : file.path}
+
+
+ ))}
+
+ )
+}
+
+function Demonstration({ data, locale }: { data: ExampleView; locale: 'en' | 'zh' }) {
+ const [selected, setSelected] = useState(0)
+ const preset: PresetView = data.presets[selected]
+ const copy = ui[locale]
+ const focusPath = data.id === 'monorepo' ? 'package.json'
+ : data.id === 'file-changes' ? '.env.example'
+ : data.id === 'skills' ? 'code-review/SKILL.md' : 'src/main.ts'
+ const sourceFile = preset.source.find((file) => file.path === `${preset.directory ? `${preset.directory}/` : ''}${focusPath}`)!
+ const outputFile = preset.output.find((file) => file.path === focusPath)!
+
+ return (
+
+
+
+
+ {data.repository}
+
+
+ {data.presets.map((edition, index) => (
+ setSelected(index)}>
+ {edition.label[locale]}
+
+ ))}
+
+
+
+
+
+ {data.repository}/
+
+ {focusPath}
+
+
+
+ 02
{copy.config}
+ {preset.id}.json / {copy.excerpt}
+
+
+
+ 03
{copy.output} {preset.output.length} {locale === 'zh' ? '个文件' : 'files'}
+ {preset.target}/
+
+ {focusPath}
+
+
+
+
+ {copy.sourceHint}
+ {copy.outputHint}
+
+
+
+
+
+ )
+}
+
+export function ScenarioDemo({ locale, examples }: { locale: 'en' | 'zh'; examples: ExampleView[] }) {
+ const copy = exampleCopy[locale]
+ const text = ui[locale]
+ const prefix = locale === 'zh' ? '/zh' : ''
+
+ return (
+
+
+
+
SOURCE → PRESET → PROJECT
+
{copy.title}
+
+
{text.browse}
+
+
+
+ {copy.scenarios.map((scenario, index) => (
+
+ 0{index + 1} {text.tabs[index]}
+
+ ))}
+
+ {copy.scenarios.map((scenario, index) => (
+
+
+
{scenario.title}
+
+
{copy.before} {scenario.before}
+
{copy.after} {scenario.after}
+
+
+
+
+
{text.footnote}
+
{text.view}
+
+
+ ))}
+
+
+ )
+}
diff --git a/docs/src/home/scenarios.tsx b/docs/src/home/scenarios.tsx
new file mode 100644
index 0000000..6a5d7c5
--- /dev/null
+++ b/docs/src/home/scenarios.tsx
@@ -0,0 +1,9 @@
+import { getExampleView } from '@/examples/highlight.server'
+import { ScenarioDemo } from './scenario-demo'
+
+export async function HomeScenarios({ locale }: { locale: 'en' | 'zh' }) {
+ const examples = await Promise.all(
+ (['variants', 'skills', 'monorepo', 'file-changes'] as const).map(getExampleView),
+ )
+ return
+}
diff --git a/docs/src/home/source-diagram.tsx b/docs/src/home/source-diagram.tsx
new file mode 100644
index 0000000..8a722c8
--- /dev/null
+++ b/docs/src/home/source-diagram.tsx
@@ -0,0 +1,38 @@
+import { FileCode2, Folder, GitBranch } from 'lucide-react'
+import styles from './preset-home.module.css'
+
+export function SourceDiagram({ locale }: { locale: 'en' | 'zh' }) {
+ const zh = locale === 'zh'
+ return (
+
+
+
starter/ {zh ? '维护这一份' : 'Maintain once'}
+
+
src/main.ts
+
src/analytics.ts
+
package.json
+
+
+
+
+
+
basic.json
+
+
{zh ? '基础版' : 'Basic edition'}
+
src/main.ts
+
src/analytics.ts −
+
+
+
+
team.json
+
+
{zh ? '团队版' : 'Team edition'}
+
src/main.ts
+
src/analytics.ts
+
+
+
+ {zh ? '公共代码改一处,版本差异留在配置里。' : 'Edit shared code once. Keep edition differences in config.'}
+
+ )
+}
diff --git a/docs/src/mdx/command-highlight.server.ts b/docs/src/mdx/command-highlight.server.ts
new file mode 100644
index 0000000..1d6054b
--- /dev/null
+++ b/docs/src/mdx/command-highlight.server.ts
@@ -0,0 +1,24 @@
+import { cache } from 'react'
+import { createHighlighter } from 'shiki'
+import type { HighlightedCommand } from './components/command-block'
+
+let highlighter: ReturnType
| undefined
+
+export const highlightCommand = cache(async (text: string): Promise => {
+ highlighter ??= createHighlighter({
+ themes: ['github-light', 'github-dark'],
+ langs: ['bash'],
+ })
+ const syntax = await highlighter
+ return {
+ text,
+ lines: syntax.codeToTokensWithThemes(text, {
+ lang: 'bash',
+ 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,
+ }))),
+ }
+})
diff --git a/docs/src/mdx/components/command-block.tsx b/docs/src/mdx/components/command-block.tsx
new file mode 100644
index 0000000..9fc0245
--- /dev/null
+++ b/docs/src/mdx/components/command-block.tsx
@@ -0,0 +1,37 @@
+'use client'
+
+import type { CSSProperties } from 'react'
+import { CodeBlock } from '@blackwork/machine/runtime'
+
+export interface HighlightedCommand {
+ text: string
+ lines: { text: string; light?: string; dark?: string }[][]
+}
+
+export function CommandBlock({ command, locale = 'en' }: {
+ command: HighlightedCommand
+ locale?: 'en' | 'zh'
+}) {
+ return (
+
+
+ {command.lines.map((line, index) => (
+
+ {index > 0 && '\n'}
+ {line.map((token, tokenIndex) => (
+ {token.text}
+ ))}
+
+ ))}
+
+
+ )
+}
diff --git a/docs/src/mdx/components/package-manager-commands.tsx b/docs/src/mdx/components/package-manager-commands.tsx
index 1f13110..631847f 100644
--- a/docs/src/mdx/components/package-manager-commands.tsx
+++ b/docs/src/mdx/components/package-manager-commands.tsx
@@ -1,6 +1,6 @@
-'use client'
-
import { Tabs, TabsContent, TabsList, TabsTrigger } from 'blackwork'
+import { highlightCommand } from '../command-highlight.server'
+import { CommandBlock } from './command-block'
const commands = {
create: [
@@ -17,13 +17,18 @@ const commands = {
],
} as const
-export function PackageManagerCommands({
+export async function PackageManagerCommands({
mode = 'create',
locale = 'en',
}: {
mode?: keyof typeof commands
locale?: 'en' | 'zh'
}) {
+ const highlightedCommands = await Promise.all(commands[mode].map(async (item) => ({
+ ...item,
+ highlighted: await highlightCommand(item.command),
+ })))
+
return (
@@ -33,11 +38,9 @@ export function PackageManagerCommands({
))}
- {commands[mode].map(({ id, command }) => (
+ {highlightedCommands.map(({ id, highlighted }) => (
-
- {command}
-
+
))}
diff --git a/docs/src/site/docs-link.test.ts b/docs/src/site/docs-link.test.ts
index 0dc5fc5..745012f 100644
--- a/docs/src/site/docs-link.test.ts
+++ b/docs/src/site/docs-link.test.ts
@@ -10,6 +10,10 @@ vi.mock('next/navigation', () => ({ usePathname }))
describe('header navigation', () => {
test.each([
+ ['/examples/variants', '/examples/variants'],
+ ['/zh/examples/skills', '/examples/variants'],
+ ['/en/examples/monorepo/', '/examples/variants'],
+ ['/zh/examples/file-changes', '/examples/variants'],
['/zh/guide/preset-configs', '/guide/preset-configs'],
['/en/guide/preset-configs/', '/guide/preset-configs'],
['/guide/preset-configs', '/guide/preset-configs'],
diff --git a/docs/src/site/navigation.ts b/docs/src/site/navigation.ts
index f960624..ba7cd1b 100644
--- a/docs/src/site/navigation.ts
+++ b/docs/src/site/navigation.ts
@@ -9,6 +9,10 @@ export const headerNavigation = [
href: '/guide/preset-configs',
label: { en: 'Preset configs', zh: 'Preset 配置' },
},
+ {
+ href: '/examples/variants',
+ label: { en: 'Examples', zh: '使用示例' },
+ },
{
href: '/showcase',
label: { en: 'Showcase', zh: '案例展示' },
@@ -31,6 +35,8 @@ export const normalizeNavigationPath = (href: string) => {
export const getCurrentHeaderHref = (pathname: string) => {
const path = normalizeNavigationPath(pathname)
+ if (path.startsWith('/examples/')) return '/examples/variants'
+
const matchingItem = headerNavigation
.filter(({ href }) => href.startsWith('/'))
.sort((a, b) => b.href.length - a.href.length)