@liu_jimmy/create-fe-kit 0.1.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.js +30 -25
- package/package.json +4 -1
- package/templates/react/.cursor/rules/fe-kit-style.mdc +9 -1
- package/templates/react/AGENTS.md +15 -1
- package/templates/react/src/App.tsx +16 -13
- package/templates/react/src/pages/Landing.tsx +101 -0
- package/templates/react/vite.config.ts +7 -0
- package/templates/svelte/.cursor/rules/fe-kit-style.mdc +8 -0
- package/templates/svelte/AGENTS.md +9 -1
- package/templates/svelte/src/App.svelte +14 -11
- package/templates/svelte/src/pages/Landing.svelte +95 -0
- package/templates/svelte/vite.config.ts +7 -0
- package/templates/vue/.cursor/rules/fe-kit-style.mdc +5 -0
- package/templates/vue/AGENTS.md +15 -0
- package/templates/vue/src/App.vue +12 -11
- package/templates/vue/src/pages/Landing.vue +102 -0
- package/templates/vue/vite.config.ts +7 -0
package/index.js
CHANGED
|
@@ -2,26 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* create-fe-kit
|
|
5
|
-
* Usage: pnpm create @liu_jimmy/fe-kit or npx @liu_jimmy/create-fe-kit
|
|
5
|
+
* Usage: pnpm create @liu_jimmy/create-fe-kit or npx @liu_jimmy/create-fe-kit
|
|
6
6
|
* Prompts for project name + framework (Vue / React / Svelte), then copies template.
|
|
7
7
|
*/
|
|
8
|
-
import {
|
|
9
|
-
import { createWriteStream, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from "fs";
|
|
8
|
+
import { mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from "fs";
|
|
10
9
|
import { dirname, join } from "path";
|
|
11
10
|
import { fileURLToPath } from "url";
|
|
11
|
+
import prompts from "prompts";
|
|
12
12
|
|
|
13
13
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
14
14
|
const TEMPLATES_DIR = join(__dirname, "templates");
|
|
15
15
|
|
|
16
|
-
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
17
|
-
|
|
18
|
-
function ask(question, defaultAnswer = "") {
|
|
19
|
-
return new Promise((resolve) => {
|
|
20
|
-
const prompt = defaultAnswer ? `${question} (${defaultAnswer}): ` : `${question}: `;
|
|
21
|
-
rl.question(prompt, (answer) => resolve(answer.trim() || defaultAnswer));
|
|
22
|
-
});
|
|
23
|
-
}
|
|
24
|
-
|
|
25
16
|
function copyRecursive(src, dest, vars = {}) {
|
|
26
17
|
mkdirSync(dest, { recursive: true });
|
|
27
18
|
for (const name of readdirSync(src)) {
|
|
@@ -44,21 +35,37 @@ function copyRecursive(src, dest, vars = {}) {
|
|
|
44
35
|
async function main() {
|
|
45
36
|
console.log("\n create-fe-kit — Vue / React / Svelte + Vite + fe-kit\n");
|
|
46
37
|
|
|
47
|
-
const projectName = await
|
|
38
|
+
const { projectName } = await prompts({
|
|
39
|
+
type: "text",
|
|
40
|
+
name: "projectName",
|
|
41
|
+
message: "Project name",
|
|
42
|
+
initial: "my-fe-kit-app",
|
|
43
|
+
});
|
|
44
|
+
if (projectName == null) {
|
|
45
|
+
process.exit(0);
|
|
46
|
+
}
|
|
48
47
|
const nameSafe = projectName.replace(/\s+/g, "-").toLowerCase();
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
48
|
+
|
|
49
|
+
const { framework } = await prompts({
|
|
50
|
+
type: "select",
|
|
51
|
+
name: "framework",
|
|
52
|
+
message: "Framework",
|
|
53
|
+
choices: [
|
|
54
|
+
{ title: "Vue", value: "vue" },
|
|
55
|
+
{ title: "React", value: "react" },
|
|
56
|
+
{ title: "Svelte", value: "svelte" },
|
|
57
|
+
],
|
|
58
|
+
initial: 0,
|
|
59
|
+
});
|
|
60
|
+
if (framework == null) {
|
|
61
|
+
process.exit(0);
|
|
62
|
+
}
|
|
55
63
|
|
|
56
64
|
const templatePath = join(TEMPLATES_DIR, framework);
|
|
57
65
|
try {
|
|
58
66
|
statSync(templatePath);
|
|
59
67
|
} catch {
|
|
60
|
-
console.error(`\nTemplate "${framework}" not found
|
|
61
|
-
rl.close();
|
|
68
|
+
console.error(`\nTemplate "${framework}" not found.`);
|
|
62
69
|
process.exit(1);
|
|
63
70
|
}
|
|
64
71
|
|
|
@@ -66,7 +73,6 @@ async function main() {
|
|
|
66
73
|
try {
|
|
67
74
|
statSync(targetDir);
|
|
68
75
|
console.error(`\nDirectory already exists: ${targetDir}`);
|
|
69
|
-
rl.close();
|
|
70
76
|
process.exit(1);
|
|
71
77
|
} catch {}
|
|
72
78
|
|
|
@@ -78,15 +84,14 @@ async function main() {
|
|
|
78
84
|
console.log(`\nCreating ${nameSafe} with ${framework}...`);
|
|
79
85
|
copyRecursive(templatePath, targetDir, vars);
|
|
80
86
|
|
|
81
|
-
console.log("\nDone.
|
|
87
|
+
console.log("\nDone. Your project includes a landing page (hero + FAQ + footer).\n");
|
|
88
|
+
console.log("Next steps:\n");
|
|
82
89
|
console.log(` cd ${nameSafe}`);
|
|
83
90
|
console.log(" pnpm install");
|
|
84
91
|
console.log(" pnpm dev\n");
|
|
85
|
-
rl.close();
|
|
86
92
|
}
|
|
87
93
|
|
|
88
94
|
main().catch((err) => {
|
|
89
95
|
console.error(err);
|
|
90
|
-
rl.close();
|
|
91
96
|
process.exit(1);
|
|
92
97
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@liu_jimmy/create-fe-kit",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "Create a new project with fe-kit (Vue / React / Svelte + Vite + Tailwind v3 + ESLint)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -26,5 +26,8 @@
|
|
|
26
26
|
},
|
|
27
27
|
"engines": {
|
|
28
28
|
"node": ">=18"
|
|
29
|
+
},
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"prompts": "^2.4.2"
|
|
29
32
|
}
|
|
30
33
|
}
|
|
@@ -20,4 +20,12 @@ alwaysApply: true
|
|
|
20
20
|
|
|
21
21
|
- 目录 kebab-case;组件文件 PascalCase;非组件文件 kebab-case;变量 camelCase;常量 UPPER_SNAKE_CASE;类型 PascalCase;函数动词开头。
|
|
22
22
|
|
|
23
|
-
##
|
|
23
|
+
## 三、React 组件写法
|
|
24
|
+
|
|
25
|
+
- **由 props 推导的 className**:用 `useMemo(..., [依赖])` 或 JSX 内联,禁止 `const c = f(props.x)` 只算一次。示例:`const cn = useMemo(() => base + ' ' + variants[variant], [variant]);`
|
|
26
|
+
|
|
27
|
+
## 四、生成后应通过 `pnpm lint`。
|
|
28
|
+
|
|
29
|
+
## 五、请求统计(ai record)
|
|
30
|
+
|
|
31
|
+
- 回复末尾提示用户执行:命中缓存则 `ai record --request --cache-hit --tokens-saved <估算>`,否则 `ai record --request --tokens-used <估算>`。仅作记录用。
|
|
@@ -1,3 +1,17 @@
|
|
|
1
1
|
# AI 生成代码约束(fe-kit)
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
生成或修改前端代码时必须遵守以下约束。
|
|
4
|
+
|
|
5
|
+
## 核心原则
|
|
6
|
+
|
|
7
|
+
1. **样式**:只使用 fe-kit 语义 class(`bg-bg`、`text-fg`、`border-border`、`bg-primary`、`rounded-xl` 等),禁止内联颜色/任意 Tailwind 颜色或任意值。
|
|
8
|
+
2. **命名**:目录 kebab-case、组件文件 PascalCase、变量 camelCase、常量 UPPER_SNAKE_CASE、类型 PascalCase;生成后应通过 `pnpm lint`。
|
|
9
|
+
|
|
10
|
+
## 组件写法(React)
|
|
11
|
+
|
|
12
|
+
- **由 props 推导的 className 或样式**:用 `useMemo(() => ..., [props.xxx])` 或模板内联表达式,禁止用只执行一次的变量(否则 props 变化时不会更新)。
|
|
13
|
+
- 示例:带 `variant` 的按钮,`const buttonClass = useMemo(() => baseClass + ' ' + variantClass[variant], [variant]);` 或 JSX 内联 `className={baseClass + ' ' + variantClass[variant]}`。
|
|
14
|
+
|
|
15
|
+
## AI 请求统计(fe-kit-cli)
|
|
16
|
+
|
|
17
|
+
完成本次回复后,在回复**末尾**提示用户执行一次:若用到 block 缓存则 `ai record --request --cache-hit --tokens-saved <估算>`,否则 `ai record --request --tokens-used <估算>`。供 `ai stats` 仅作记录用。
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { useState } from "react";
|
|
2
|
+
import { Landing } from "./pages/Landing";
|
|
2
3
|
|
|
3
4
|
type ThemeId = "theme-blue" | "theme-purple" | "theme-green";
|
|
4
5
|
|
|
6
|
+
const PROJECT_TITLE = "{{TITLE}}";
|
|
7
|
+
|
|
5
8
|
const themes: { id: ThemeId; label: string }[] = [
|
|
6
9
|
{ id: "theme-blue", label: "蓝" },
|
|
7
10
|
{ id: "theme-purple", label: "紫" },
|
|
@@ -23,26 +26,22 @@ function App() {
|
|
|
23
26
|
}
|
|
24
27
|
|
|
25
28
|
function toggleDark() {
|
|
26
|
-
|
|
27
|
-
|
|
29
|
+
const next = !isDark;
|
|
30
|
+
setIsDark(next);
|
|
31
|
+
document.documentElement.classList.toggle("dark", next);
|
|
28
32
|
}
|
|
29
33
|
|
|
30
34
|
return (
|
|
31
35
|
<div className="min-h-screen bg-bg text-fg font-sans">
|
|
32
36
|
<header className="border-b border-border bg-bg-elevated px-6 py-4 shadow-sm">
|
|
33
|
-
<
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
<main className="mx-auto max-w-2xl space-y-8 p-6">
|
|
38
|
-
<section className="rounded-xl border border-border bg-bg-elevated p-6 shadow-md">
|
|
39
|
-
<h2 className="text-lg font-medium text-fg">主题</h2>
|
|
40
|
-
<div className="mt-4 flex flex-wrap gap-2">
|
|
37
|
+
<div className="mx-auto flex max-w-2xl items-center justify-between">
|
|
38
|
+
<h1 className="text-2xl font-semibold text-fg">{PROJECT_TITLE}</h1>
|
|
39
|
+
<div className="flex items-center gap-2">
|
|
41
40
|
{themes.map((t) => (
|
|
42
41
|
<button
|
|
43
42
|
key={t.id}
|
|
44
43
|
type="button"
|
|
45
|
-
className={`rounded-lg px-
|
|
44
|
+
className={`rounded-lg px-3 py-1.5 text-sm font-medium transition-colors ${
|
|
46
45
|
theme === t.id ? "bg-primary text-primary-fg" : "bg-primary-muted text-fg hover:bg-primary hover:text-primary-fg"
|
|
47
46
|
}`}
|
|
48
47
|
onClick={() => handleSetTheme(t.id)}
|
|
@@ -52,7 +51,7 @@ function App() {
|
|
|
52
51
|
))}
|
|
53
52
|
<button
|
|
54
53
|
type="button"
|
|
55
|
-
className={`rounded-lg border px-
|
|
54
|
+
className={`rounded-lg border px-3 py-1.5 text-sm font-medium ${
|
|
56
55
|
isDark ? "border-fg bg-fg text-bg" : "border-border bg-bg-muted text-fg"
|
|
57
56
|
}`}
|
|
58
57
|
onClick={toggleDark}
|
|
@@ -60,7 +59,11 @@ function App() {
|
|
|
60
59
|
{isDark ? "浅色" : "深色"}
|
|
61
60
|
</button>
|
|
62
61
|
</div>
|
|
63
|
-
</
|
|
62
|
+
</div>
|
|
63
|
+
</header>
|
|
64
|
+
|
|
65
|
+
<main>
|
|
66
|
+
<Landing />
|
|
64
67
|
</main>
|
|
65
68
|
</div>
|
|
66
69
|
);
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
|
|
3
|
+
const heroTitle = "Build faster";
|
|
4
|
+
const heroSubtitle = "Ship modern web apps with fe-kit";
|
|
5
|
+
const primaryAction = { label: "Get started", href: "#" };
|
|
6
|
+
const secondaryAction = { label: "Learn more", href: "#" };
|
|
7
|
+
|
|
8
|
+
const faqTitle = "Frequently asked questions";
|
|
9
|
+
const faqItems = [
|
|
10
|
+
{
|
|
11
|
+
question: "What is fe-kit?",
|
|
12
|
+
answer:
|
|
13
|
+
"A cross-framework design system with tokens, Tailwind preset and BlockSpec for AI code cache.",
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
question: "Which frameworks?",
|
|
17
|
+
answer: "Vue, React and Svelte with the same tokens and conventions.",
|
|
18
|
+
},
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
const copyright = "© 2025 fe-kit";
|
|
22
|
+
const footerLinks = [
|
|
23
|
+
{ label: "Docs", href: "#" },
|
|
24
|
+
{ label: "GitHub", href: "#" },
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
export function Landing() {
|
|
28
|
+
const [openIndex, setOpenIndex] = useState<number | null>(0);
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<div>
|
|
32
|
+
{/* Hero */}
|
|
33
|
+
<section className="border-b border-border bg-bg-elevated py-16">
|
|
34
|
+
<div className="mx-auto max-w-2xl px-6 text-center">
|
|
35
|
+
<h1 className="text-3xl font-bold text-fg md:text-4xl">{heroTitle}</h1>
|
|
36
|
+
<p className="mt-4 text-lg text-fg-muted">{heroSubtitle}</p>
|
|
37
|
+
<div className="mt-8 flex flex-wrap justify-center gap-4">
|
|
38
|
+
<a
|
|
39
|
+
href={primaryAction.href}
|
|
40
|
+
className="rounded-lg bg-primary px-5 py-2.5 text-sm font-medium text-primary-fg hover:bg-primary-hover"
|
|
41
|
+
>
|
|
42
|
+
{primaryAction.label}
|
|
43
|
+
</a>
|
|
44
|
+
<a
|
|
45
|
+
href={secondaryAction.href}
|
|
46
|
+
className="rounded-lg border border-border bg-bg px-5 py-2.5 text-sm font-medium text-fg hover:bg-bg-muted"
|
|
47
|
+
>
|
|
48
|
+
{secondaryAction.label}
|
|
49
|
+
</a>
|
|
50
|
+
</div>
|
|
51
|
+
</div>
|
|
52
|
+
</section>
|
|
53
|
+
|
|
54
|
+
{/* FAQ */}
|
|
55
|
+
<section className="border-b border-border bg-bg py-16">
|
|
56
|
+
<div className="mx-auto max-w-2xl px-6">
|
|
57
|
+
<h2 className="text-2xl font-semibold text-fg">{faqTitle}</h2>
|
|
58
|
+
<ul className="mt-6 space-y-2">
|
|
59
|
+
{faqItems.map((item, i) => (
|
|
60
|
+
<li
|
|
61
|
+
key={i}
|
|
62
|
+
className="rounded-xl border border-border bg-bg-elevated"
|
|
63
|
+
>
|
|
64
|
+
<button
|
|
65
|
+
type="button"
|
|
66
|
+
className="flex w-full items-center justify-between px-4 py-3 text-left text-fg"
|
|
67
|
+
onClick={() => setOpenIndex(openIndex === i ? null : i)}
|
|
68
|
+
>
|
|
69
|
+
<span className="font-medium">{item.question}</span>
|
|
70
|
+
</button>
|
|
71
|
+
{openIndex === i && (
|
|
72
|
+
<div className="border-t border-border px-4 py-3 text-fg-muted">
|
|
73
|
+
{item.answer}
|
|
74
|
+
</div>
|
|
75
|
+
)}
|
|
76
|
+
</li>
|
|
77
|
+
))}
|
|
78
|
+
</ul>
|
|
79
|
+
</div>
|
|
80
|
+
</section>
|
|
81
|
+
|
|
82
|
+
{/* Footer */}
|
|
83
|
+
<footer className="border-t border-border bg-bg-muted py-8">
|
|
84
|
+
<div className="mx-auto flex max-w-2xl flex-col items-center justify-between gap-4 px-6 sm:flex-row">
|
|
85
|
+
<p className="text-sm text-fg-muted">{copyright}</p>
|
|
86
|
+
<nav className="flex gap-6">
|
|
87
|
+
{footerLinks.map((link, i) => (
|
|
88
|
+
<a
|
|
89
|
+
key={i}
|
|
90
|
+
href={link.href}
|
|
91
|
+
className="text-sm text-fg-muted hover:text-fg"
|
|
92
|
+
>
|
|
93
|
+
{link.label}
|
|
94
|
+
</a>
|
|
95
|
+
))}
|
|
96
|
+
</nav>
|
|
97
|
+
</div>
|
|
98
|
+
</footer>
|
|
99
|
+
</div>
|
|
100
|
+
);
|
|
101
|
+
}
|
|
@@ -1,6 +1,13 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
import { fileURLToPath } from "url";
|
|
1
3
|
import { defineConfig } from "vite";
|
|
2
4
|
import react from "@vitejs/plugin-react";
|
|
3
5
|
|
|
6
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
|
|
4
8
|
export default defineConfig({
|
|
5
9
|
plugins: [react()],
|
|
10
|
+
resolve: {
|
|
11
|
+
alias: { "@": path.resolve(__dirname, "src") },
|
|
12
|
+
},
|
|
6
13
|
});
|
|
@@ -6,3 +6,11 @@ alwaysApply: true
|
|
|
6
6
|
# fe-kit 风格约束
|
|
7
7
|
|
|
8
8
|
只使用 fe-kit 语义 class(bg-bg、text-fg、border-border、bg-primary、rounded-xl 等);禁止内联颜色、任意 Tailwind 颜色或任意值;命名:目录 kebab-case、组件 PascalCase、变量 camelCase、常量 UPPER_SNAKE_CASE;生成后应通过 pnpm lint。
|
|
9
|
+
|
|
10
|
+
## 组件写法(Svelte)
|
|
11
|
+
|
|
12
|
+
- **由 props 推导的 class**:用响应式语句 `$: className = base + ' ' + map[prop];`,禁止只赋值一次的 `let className = ...`。
|
|
13
|
+
|
|
14
|
+
## 请求统计(ai record)
|
|
15
|
+
|
|
16
|
+
- 回复末尾提示用户执行:命中缓存则 `ai record --request --cache-hit --tokens-saved <估算>`,否则 `ai record --request --tokens-used <估算>`。仅作记录用。
|
|
@@ -1,3 +1,11 @@
|
|
|
1
1
|
# AI 生成代码约束(fe-kit)
|
|
2
2
|
|
|
3
|
-
只使用 fe-kit 语义 class(bg-bg、text-fg、border-border、bg-primary、rounded-xl 等);禁止内联颜色与任意 Tailwind 颜色;命名按
|
|
3
|
+
只使用 fe-kit 语义 class(bg-bg、text-fg、border-border、bg-primary、rounded-xl 等);禁止内联颜色与任意 Tailwind 颜色;命名按 kebab-case 目录、PascalCase 组件、camelCase 变量;生成后应通过 pnpm lint。
|
|
4
|
+
|
|
5
|
+
## 组件写法(Svelte)
|
|
6
|
+
|
|
7
|
+
- **由 props 或状态推导的 class**:用响应式语句 `$: buttonClass = baseClass + ' ' + variantClass[variant];`,禁止只赋值一次的 `let buttonClass = ...`(否则 variant 变化时不会更新)。
|
|
8
|
+
|
|
9
|
+
## AI 请求统计(fe-kit-cli)
|
|
10
|
+
|
|
11
|
+
完成本次回复后,在回复**末尾**提示用户执行一次:若用到 block 缓存则 `ai record --request --cache-hit --tokens-saved <估算>`,否则 `ai record --request --tokens-used <估算>`。供 `ai stats` 仅作记录用。
|
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import Landing from "./pages/Landing.svelte";
|
|
3
|
+
|
|
4
|
+
const projectTitle = "{{TITLE}}";
|
|
5
|
+
|
|
2
6
|
type ThemeId = "theme-blue" | "theme-purple" | "theme-green";
|
|
3
7
|
|
|
4
8
|
let theme: ThemeId = "theme-blue";
|
|
@@ -24,18 +28,13 @@
|
|
|
24
28
|
|
|
25
29
|
<main class="min-h-screen bg-bg text-fg font-sans">
|
|
26
30
|
<header class="border-b border-border bg-bg-elevated px-6 py-4 shadow-sm">
|
|
27
|
-
<
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
<div class="mx-auto max-w-2xl space-y-8 p-6">
|
|
32
|
-
<section class="rounded-xl border border-border bg-bg-elevated p-6 shadow-md">
|
|
33
|
-
<h2 class="text-lg font-medium text-fg">主题</h2>
|
|
34
|
-
<div class="mt-4 flex flex-wrap gap-2">
|
|
31
|
+
<div class="mx-auto flex max-w-2xl items-center justify-between">
|
|
32
|
+
<h1 class="text-2xl font-semibold text-fg">{projectTitle}</h1>
|
|
33
|
+
<div class="flex items-center gap-2">
|
|
35
34
|
{#each themes as t}
|
|
36
35
|
<button
|
|
37
36
|
type="button"
|
|
38
|
-
class="rounded-lg px-
|
|
37
|
+
class="rounded-lg px-3 py-1.5 text-sm font-medium transition-colors {theme === t.id
|
|
39
38
|
? 'bg-primary text-primary-fg'
|
|
40
39
|
: 'bg-primary-muted text-fg hover:bg-primary hover:text-primary-fg'}"
|
|
41
40
|
on:click={() => setTheme(t.id)}
|
|
@@ -45,12 +44,16 @@
|
|
|
45
44
|
{/each}
|
|
46
45
|
<button
|
|
47
46
|
type="button"
|
|
48
|
-
class="rounded-lg border px-
|
|
47
|
+
class="rounded-lg border px-3 py-1.5 text-sm font-medium {isDark ? 'border-fg bg-fg text-bg' : 'border-border bg-bg-muted text-fg'}"
|
|
49
48
|
on:click={toggleDark}
|
|
50
49
|
>
|
|
51
50
|
{isDark ? "浅色" : "深色"}
|
|
52
51
|
</button>
|
|
53
52
|
</div>
|
|
54
|
-
</
|
|
53
|
+
</div>
|
|
54
|
+
</header>
|
|
55
|
+
|
|
56
|
+
<div>
|
|
57
|
+
<Landing />
|
|
55
58
|
</div>
|
|
56
59
|
</main>
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
const heroTitle = "Build faster";
|
|
3
|
+
const heroSubtitle = "Ship modern web apps with fe-kit";
|
|
4
|
+
const primaryAction = { label: "Get started", href: "#" };
|
|
5
|
+
const secondaryAction = { label: "Learn more", href: "#" };
|
|
6
|
+
|
|
7
|
+
const faqTitle = "Frequently asked questions";
|
|
8
|
+
const faqItems: { question: string; answer: string }[] = [
|
|
9
|
+
{
|
|
10
|
+
question: "What is fe-kit?",
|
|
11
|
+
answer:
|
|
12
|
+
"A cross-framework design system with tokens, Tailwind preset and BlockSpec for AI code cache.",
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
question: "Which frameworks?",
|
|
16
|
+
answer: "Vue, React and Svelte with the same tokens and conventions.",
|
|
17
|
+
},
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
let openIndex: number | null = 0;
|
|
21
|
+
|
|
22
|
+
const copyright = "© 2025 fe-kit";
|
|
23
|
+
const footerLinks: { label: string; href: string }[] = [
|
|
24
|
+
{ label: "Docs", href: "#" },
|
|
25
|
+
{ label: "GitHub", href: "#" },
|
|
26
|
+
];
|
|
27
|
+
</script>
|
|
28
|
+
|
|
29
|
+
<div>
|
|
30
|
+
<!-- Hero -->
|
|
31
|
+
<section class="border-b border-border bg-bg-elevated py-16">
|
|
32
|
+
<div class="mx-auto max-w-2xl px-6 text-center">
|
|
33
|
+
<h1 class="text-3xl font-bold text-fg md:text-4xl">{heroTitle}</h1>
|
|
34
|
+
<p class="mt-4 text-lg text-fg-muted">{heroSubtitle}</p>
|
|
35
|
+
<div class="mt-8 flex flex-wrap justify-center gap-4">
|
|
36
|
+
<a
|
|
37
|
+
href={primaryAction.href}
|
|
38
|
+
class="rounded-lg bg-primary px-5 py-2.5 text-sm font-medium text-primary-fg hover:bg-primary-hover"
|
|
39
|
+
>
|
|
40
|
+
{primaryAction.label}
|
|
41
|
+
</a>
|
|
42
|
+
<a
|
|
43
|
+
href={secondaryAction.href}
|
|
44
|
+
class="rounded-lg border border-border bg-bg px-5 py-2.5 text-sm font-medium text-fg hover:bg-bg-muted"
|
|
45
|
+
>
|
|
46
|
+
{secondaryAction.label}
|
|
47
|
+
</a>
|
|
48
|
+
</div>
|
|
49
|
+
</div>
|
|
50
|
+
</section>
|
|
51
|
+
|
|
52
|
+
<!-- FAQ -->
|
|
53
|
+
<section class="border-b border-border bg-bg py-16">
|
|
54
|
+
<div class="mx-auto max-w-2xl px-6">
|
|
55
|
+
<h2 class="text-2xl font-semibold text-fg">{faqTitle}</h2>
|
|
56
|
+
<ul class="mt-6 space-y-2">
|
|
57
|
+
{#each faqItems as item, i}
|
|
58
|
+
<li class="rounded-xl border border-border bg-bg-elevated">
|
|
59
|
+
<button
|
|
60
|
+
type="button"
|
|
61
|
+
class="flex w-full items-center justify-between px-4 py-3 text-left text-fg"
|
|
62
|
+
on:click={() => (openIndex = openIndex === i ? null : i)}
|
|
63
|
+
>
|
|
64
|
+
<span class="font-medium">{item.question}</span>
|
|
65
|
+
</button>
|
|
66
|
+
{#if openIndex === i}
|
|
67
|
+
<div class="border-t border-border px-4 py-3 text-fg-muted">
|
|
68
|
+
{item.answer}
|
|
69
|
+
</div>
|
|
70
|
+
{/if}
|
|
71
|
+
</li>
|
|
72
|
+
{/each}
|
|
73
|
+
</ul>
|
|
74
|
+
</div>
|
|
75
|
+
</section>
|
|
76
|
+
|
|
77
|
+
<!-- Footer -->
|
|
78
|
+
<footer class="border-t border-border bg-bg-muted py-8">
|
|
79
|
+
<div
|
|
80
|
+
class="mx-auto flex max-w-2xl flex-col items-center justify-between gap-4 px-6 sm:flex-row"
|
|
81
|
+
>
|
|
82
|
+
<p class="text-sm text-fg-muted">{copyright}</p>
|
|
83
|
+
<nav class="flex gap-6">
|
|
84
|
+
{#each footerLinks as link}
|
|
85
|
+
<a
|
|
86
|
+
href={link.href}
|
|
87
|
+
class="text-sm text-fg-muted hover:text-fg"
|
|
88
|
+
>
|
|
89
|
+
{link.label}
|
|
90
|
+
</a>
|
|
91
|
+
{/each}
|
|
92
|
+
</nav>
|
|
93
|
+
</div>
|
|
94
|
+
</footer>
|
|
95
|
+
</div>
|
|
@@ -1,6 +1,13 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
import { fileURLToPath } from "url";
|
|
1
3
|
import { defineConfig } from "vite";
|
|
2
4
|
import { svelte } from "@sveltejs/vite-plugin-svelte";
|
|
3
5
|
|
|
6
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
|
|
4
8
|
export default defineConfig({
|
|
5
9
|
plugins: [svelte()],
|
|
10
|
+
resolve: {
|
|
11
|
+
alias: { "@": path.resolve(__dirname, "src") },
|
|
12
|
+
},
|
|
6
13
|
});
|
|
@@ -45,7 +45,12 @@ alwaysApply: true
|
|
|
45
45
|
## 四、Vue 组件
|
|
46
46
|
|
|
47
47
|
- 模板中组件名 PascalCase;块顺序:script → template → style
|
|
48
|
+
- **由 props 推导的 class**:必须用 `computed()`,禁止 `const x = f(props.xxx)`(不会随 props 更新)。示例:`const buttonClass = computed(() => baseClass + ' ' + variantClass[props.variant]);`
|
|
48
49
|
|
|
49
50
|
## 五、生成后自检
|
|
50
51
|
|
|
51
52
|
- 代码应能通过 `pnpm lint`;若有报错只做最小修复。
|
|
53
|
+
|
|
54
|
+
## 六、请求统计(ai record)
|
|
55
|
+
|
|
56
|
+
- 本次回复结束后,在末尾提示用户执行一次:若命中缓存则 `ai record --request --cache-hit --tokens-saved <估算>`,否则 `ai record --request --tokens-used <估算>`。供 `ai stats` 仅作记录用。
|
package/templates/vue/AGENTS.md
CHANGED
|
@@ -20,3 +20,18 @@
|
|
|
20
20
|
| 阴影 | `shadow-sm`~`shadow-xl` |
|
|
21
21
|
|
|
22
22
|
主题:根节点 `theme-blue` / `theme-purple` / `theme-green`;深色加 `dark`。
|
|
23
|
+
|
|
24
|
+
## 组件写法(Vue)
|
|
25
|
+
|
|
26
|
+
- **由 props 推导的 class 或样式**:必须用 `computed()`,禁止用一次性 `const`(否则 props 变化时不会更新)。
|
|
27
|
+
- 示例:带 `variant` 的按钮,class 应写为
|
|
28
|
+
`const buttonClass = computed(() => baseClass + ' ' + variantClass[props.variant]);`
|
|
29
|
+
模板中 `:class="buttonClass"`。
|
|
30
|
+
- 组件名用 `defineOptions({ name: "PascalName" })`;props 用 `defineProps` + `withDefaults`,类型用 TypeScript。
|
|
31
|
+
|
|
32
|
+
## AI 请求统计(fe-kit-cli)
|
|
33
|
+
|
|
34
|
+
完成本次回复后,在回复**末尾**提示用户执行一次统计记录(项目已安装 `@liu_jimmy/fe-kit-cli` 时):
|
|
35
|
+
- 若本次用到了 block 缓存或等价复用:提示运行 `ai record --request --cache-hit --tokens-saved <估算值>`。
|
|
36
|
+
- 若本次为普通生成/修改代码:提示运行 `ai record --request --tokens-used <估算值>`。
|
|
37
|
+
用户复制到终端执行即可,用于 `ai stats` 的统计与 Cost saved 仅作记录参考。
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { ref } from "vue";
|
|
3
|
+
import Landing from "./pages/Landing.vue";
|
|
3
4
|
|
|
5
|
+
const projectTitle = "{{TITLE}}";
|
|
4
6
|
const theme = ref<"theme-blue" | "theme-purple" | "theme-green">("theme-blue");
|
|
5
7
|
const isDark = ref(false);
|
|
6
8
|
|
|
@@ -25,20 +27,15 @@ function toggleDark() {
|
|
|
25
27
|
<template>
|
|
26
28
|
<div class="min-h-screen bg-bg text-fg font-sans">
|
|
27
29
|
<header class="border-b border-border bg-bg-elevated px-6 py-4 shadow-sm">
|
|
28
|
-
<
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
<main class="mx-auto max-w-2xl space-y-8 p-6">
|
|
33
|
-
<section class="rounded-xl border border-border bg-bg-elevated p-6 shadow-md">
|
|
34
|
-
<h2 class="text-lg font-medium text-fg">主题</h2>
|
|
35
|
-
<div class="mt-4 flex flex-wrap gap-2">
|
|
30
|
+
<div class="mx-auto flex max-w-2xl items-center justify-between">
|
|
31
|
+
<h1 class="text-2xl font-semibold text-fg">{{ projectTitle }}</h1>
|
|
32
|
+
<div class="flex items-center gap-2">
|
|
36
33
|
<button
|
|
37
34
|
v-for="t in themes"
|
|
38
35
|
:key="t.id"
|
|
39
36
|
type="button"
|
|
40
37
|
:class="[
|
|
41
|
-
'rounded-lg px-
|
|
38
|
+
'rounded-lg px-3 py-1.5 text-sm font-medium transition-colors',
|
|
42
39
|
theme === t.id ? 'bg-primary text-primary-fg' : 'bg-primary-muted text-fg hover:bg-primary hover:text-primary-fg',
|
|
43
40
|
]"
|
|
44
41
|
@click="setTheme(t.id)"
|
|
@@ -47,13 +44,17 @@ function toggleDark() {
|
|
|
47
44
|
</button>
|
|
48
45
|
<button
|
|
49
46
|
type="button"
|
|
50
|
-
:class="['rounded-lg border px-
|
|
47
|
+
:class="['rounded-lg border px-3 py-1.5 text-sm font-medium', isDark ? 'border-fg bg-fg text-bg' : 'border-border bg-bg-muted text-fg']"
|
|
51
48
|
@click="toggleDark"
|
|
52
49
|
>
|
|
53
50
|
{{ isDark ? "浅色" : "深色" }}
|
|
54
51
|
</button>
|
|
55
52
|
</div>
|
|
56
|
-
</
|
|
53
|
+
</div>
|
|
54
|
+
</header>
|
|
55
|
+
|
|
56
|
+
<main>
|
|
57
|
+
<Landing />
|
|
57
58
|
</main>
|
|
58
59
|
</div>
|
|
59
60
|
</template>
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { ref } from "vue";
|
|
3
|
+
|
|
4
|
+
const heroTitle = "Build faster";
|
|
5
|
+
const heroSubtitle = "Ship modern web apps with fe-kit";
|
|
6
|
+
const primaryAction = { label: "Get started", href: "#" };
|
|
7
|
+
const secondaryAction = { label: "Learn more", href: "#" };
|
|
8
|
+
|
|
9
|
+
const faqTitle = "Frequently asked questions";
|
|
10
|
+
const faqItems = [
|
|
11
|
+
{
|
|
12
|
+
question: "What is fe-kit?",
|
|
13
|
+
answer:
|
|
14
|
+
"A cross-framework design system with tokens, Tailwind preset and BlockSpec for AI code cache.",
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
question: "Which frameworks?",
|
|
18
|
+
answer: "Vue, React and Svelte with the same tokens and conventions.",
|
|
19
|
+
},
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
const openIndex = ref<number | null>(0);
|
|
23
|
+
|
|
24
|
+
const copyright = "© 2025 fe-kit";
|
|
25
|
+
const footerLinks = [
|
|
26
|
+
{ label: "Docs", href: "#" },
|
|
27
|
+
{ label: "GitHub", href: "#" },
|
|
28
|
+
];
|
|
29
|
+
</script>
|
|
30
|
+
|
|
31
|
+
<template>
|
|
32
|
+
<div>
|
|
33
|
+
<!-- Hero -->
|
|
34
|
+
<section class="border-b border-border bg-bg-elevated py-16">
|
|
35
|
+
<div class="mx-auto max-w-2xl px-6 text-center">
|
|
36
|
+
<h1 class="text-3xl font-bold text-fg md:text-4xl">{{ heroTitle }}</h1>
|
|
37
|
+
<p class="mt-4 text-lg text-fg-muted">{{ heroSubtitle }}</p>
|
|
38
|
+
<div class="mt-8 flex flex-wrap justify-center gap-4">
|
|
39
|
+
<a
|
|
40
|
+
:href="primaryAction.href"
|
|
41
|
+
class="rounded-lg bg-primary px-5 py-2.5 text-sm font-medium text-primary-fg hover:bg-primary-hover"
|
|
42
|
+
>
|
|
43
|
+
{{ primaryAction.label }}
|
|
44
|
+
</a>
|
|
45
|
+
<a
|
|
46
|
+
:href="secondaryAction.href"
|
|
47
|
+
class="rounded-lg border border-border bg-bg px-5 py-2.5 text-sm font-medium text-fg hover:bg-bg-muted"
|
|
48
|
+
>
|
|
49
|
+
{{ secondaryAction.label }}
|
|
50
|
+
</a>
|
|
51
|
+
</div>
|
|
52
|
+
</div>
|
|
53
|
+
</section>
|
|
54
|
+
|
|
55
|
+
<!-- FAQ -->
|
|
56
|
+
<section class="border-b border-border bg-bg py-16">
|
|
57
|
+
<div class="mx-auto max-w-2xl px-6">
|
|
58
|
+
<h2 class="text-2xl font-semibold text-fg">{{ faqTitle }}</h2>
|
|
59
|
+
<ul class="mt-6 space-y-2">
|
|
60
|
+
<li
|
|
61
|
+
v-for="(item, i) in faqItems"
|
|
62
|
+
:key="i"
|
|
63
|
+
class="rounded-xl border border-border bg-bg-elevated"
|
|
64
|
+
>
|
|
65
|
+
<button
|
|
66
|
+
type="button"
|
|
67
|
+
class="flex w-full items-center justify-between px-4 py-3 text-left text-fg"
|
|
68
|
+
@click="openIndex = openIndex === i ? null : i"
|
|
69
|
+
>
|
|
70
|
+
<span class="font-medium">{{ item.question }}</span>
|
|
71
|
+
</button>
|
|
72
|
+
<div
|
|
73
|
+
v-show="openIndex === i"
|
|
74
|
+
class="border-t border-border px-4 py-3 text-fg-muted"
|
|
75
|
+
>
|
|
76
|
+
{{ item.answer }}
|
|
77
|
+
</div>
|
|
78
|
+
</li>
|
|
79
|
+
</ul>
|
|
80
|
+
</div>
|
|
81
|
+
</section>
|
|
82
|
+
|
|
83
|
+
<!-- Footer -->
|
|
84
|
+
<footer class="border-t border-border bg-bg-muted py-8">
|
|
85
|
+
<div
|
|
86
|
+
class="mx-auto flex max-w-2xl flex-col items-center justify-between gap-4 px-6 sm:flex-row"
|
|
87
|
+
>
|
|
88
|
+
<p class="text-sm text-fg-muted">{{ copyright }}</p>
|
|
89
|
+
<nav class="flex gap-6">
|
|
90
|
+
<a
|
|
91
|
+
v-for="(link, i) in footerLinks"
|
|
92
|
+
:key="i"
|
|
93
|
+
:href="link.href"
|
|
94
|
+
class="text-sm text-fg-muted hover:text-fg"
|
|
95
|
+
>
|
|
96
|
+
{{ link.label }}
|
|
97
|
+
</a>
|
|
98
|
+
</nav>
|
|
99
|
+
</div>
|
|
100
|
+
</footer>
|
|
101
|
+
</div>
|
|
102
|
+
</template>
|
|
@@ -1,6 +1,13 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
import { fileURLToPath } from "url";
|
|
1
3
|
import { defineConfig } from "vite";
|
|
2
4
|
import vue from "@vitejs/plugin-vue";
|
|
3
5
|
|
|
6
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
|
|
4
8
|
export default defineConfig({
|
|
5
9
|
plugins: [vue()],
|
|
10
|
+
resolve: {
|
|
11
|
+
alias: { "@": path.resolve(__dirname, "src") },
|
|
12
|
+
},
|
|
6
13
|
});
|