@liu_jimmy/create-fe-kit 0.1.0 → 1.1.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 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 { createInterface } from "readline";
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 ask("Project name", "my-fe-kit-app");
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
- console.log("\nFramework:");
50
- console.log(" 1) Vue");
51
- console.log(" 2) React");
52
- console.log(" 3) Svelte");
53
- const choice = await ask("Choose (1/2/3)", "1");
54
- const framework = { "1": "vue", "2": "react", "3": "svelte" }[choice] || "vue";
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. Use Vue (1) for now.`);
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. Next steps:\n");
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": "0.1.0",
3
+ "version": "1.1.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
  }
@@ -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
- setIsDark((v) => !v);
27
- document.documentElement.classList.toggle("dark", !isDark);
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
- <h1 className="text-2xl font-semibold text-fg">{{TITLE}}</h1>
34
- <p className="mt-1 text-sm text-fg-muted">fe-kit + React + Vite</p>
35
- </header>
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-4 py-2 text-sm font-medium transition-colors ${
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-4 py-2 text-sm font-medium ${
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
- </section>
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,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
- <h1 class="text-2xl font-semibold text-fg">{{TITLE}}</h1>
28
- <p class="mt-1 text-sm text-fg-muted">fe-kit + Svelte + Vite</p>
29
- </header>
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-4 py-2 text-sm font-medium transition-colors {theme === t.id
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-4 py-2 text-sm font-medium {isDark ? 'border-fg bg-fg text-bg' : 'border-border bg-bg-muted text-fg'}"
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
- </section>
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,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
- <h1 class="text-2xl font-semibold text-fg">{{TITLE}}</h1>
29
- <p class="mt-1 text-sm text-fg-muted">fe-kit + Vue + Vite</p>
30
- </header>
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-4 py-2 text-sm font-medium transition-colors',
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-4 py-2 text-sm font-medium', isDark ? 'border-fg bg-fg text-bg' : 'border-border bg-bg-muted text-fg']"
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
- </section>
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>