frakment 0.0.0-stage → 0.0.2

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.
@@ -0,0 +1,201 @@
1
+ import { existsSync } from "node:fs";
2
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
3
+ import path from "node:path";
4
+
5
+ import * as p from "@clack/prompts";
6
+ import { execa } from "execa";
7
+ import pc from "picocolors";
8
+
9
+ import {
10
+ detectGlobalsCss,
11
+ detectPackageManager,
12
+ findProjectRoot,
13
+ } from "../utils/detect.js";
14
+
15
+ const __dirname = import.meta.dirname;
16
+ const TEMPLATES_DIR = path.resolve(__dirname, "../../templates");
17
+
18
+ const DEFAULT_REGISTRY = "https://www.arrafi.dev/frakment/r";
19
+ const CONFIG_FILE = "components.json";
20
+ const THEME_MARKER_START = "/* ==== FRAKMENT THEME START ==== */";
21
+ const THEME_MARKER_END = "/* ==== FRAKMENT THEME END ==== */";
22
+
23
+ export async function init() {
24
+ p.intro(`${pc.dim("frakment")} init`);
25
+
26
+ let cwd;
27
+ try {
28
+ cwd = findProjectRoot(process.cwd());
29
+ } catch (error) {
30
+ p.outro(pc.red("error ") + error.message);
31
+ process.exit(1);
32
+ }
33
+
34
+ const configPath = path.join(cwd, CONFIG_FILE);
35
+ const configExists = existsSync(configPath);
36
+ let shouldWriteConfig = !configExists;
37
+
38
+ if (configExists) {
39
+ const overwrite = await p.confirm({
40
+ initialValue: false,
41
+ message: `${CONFIG_FILE} already exists. Overwrite?`,
42
+ });
43
+ if (p.isCancel(overwrite)) {
44
+ p.outro(pc.dim("cancelled"));
45
+ return;
46
+ }
47
+ if (overwrite) {
48
+ shouldWriteConfig = true;
49
+ } else {
50
+ p.log.info(`keeping existing ${CONFIG_FILE}`);
51
+ }
52
+ }
53
+
54
+ const s = p.spinner();
55
+ const cssPath = detectGlobalsCss(cwd);
56
+
57
+ // 1. components.json (write only if new or user chose to overwrite)
58
+ if (shouldWriteConfig) {
59
+ s.start("writing components.json");
60
+ const template = JSON.parse(
61
+ await readFile(path.join(TEMPLATES_DIR, "components.json"), "utf-8")
62
+ );
63
+ const config = {
64
+ ...template,
65
+ registry: DEFAULT_REGISTRY,
66
+ tailwind: { ...template.tailwind, css: cssPath },
67
+ };
68
+ await writeFile(configPath, `${JSON.stringify(config, null, 2)}\n`);
69
+ s.stop(`${pc.green("✓")} components.json`);
70
+ }
71
+
72
+ // 2. theme (inline block)
73
+ const cssFullPath = path.join(cwd, cssPath);
74
+ if (!existsSync(cssFullPath)) {
75
+ p.log.error(`CSS file not found: ${cssPath}`);
76
+ p.outro(
77
+ `${pc.red(
78
+ "error "
79
+ )}Could not find globals.css. Frakment needs an existing Tailwind v4 entry file.`
80
+ );
81
+ process.exit(1);
82
+ }
83
+
84
+ let css = await readFile(cssFullPath, "utf-8");
85
+ css = css.replace(/^\uFEFF/u, "");
86
+
87
+ if (!/^@import\s+["']tailwindcss(\/|["'])/mu.test(css)) {
88
+ p.outro(
89
+ `${pc.red(
90
+ "error "
91
+ )}Frakment requires Tailwind v4. Could not find @import "tailwindcss" in ${
92
+ cssPath
93
+ }`
94
+ );
95
+ process.exit(1);
96
+ }
97
+
98
+ // Cleanup: strip any prior theme block (marker-bounded) and
99
+ // any split-approach @import "./theme.css" line.
100
+ css = css.replaceAll(
101
+ new RegExp(
102
+ `\\n*${escapeRegex(THEME_MARKER_START)}[\\s\\S]*?${escapeRegex(THEME_MARKER_END)}\\n*`,
103
+ "g"
104
+ ),
105
+ "\n"
106
+ );
107
+ css = css.replaceAll(/^@import\s+["']\.\/theme\.css["'];?\s*$\n?/gmu, "");
108
+ css = `${css.replaceAll(/\n{3,}/gu, "\n\n").trimEnd()}\n`;
109
+
110
+ // Inject fresh theme block right after the last top-level @import
111
+ s.start("injecting theme block");
112
+ const themeContent = await readFile(
113
+ path.join(TEMPLATES_DIR, "theme.css"),
114
+ "utf-8"
115
+ );
116
+ const wrapped = `${THEME_MARKER_START}\n${themeContent.trim()}\n${THEME_MARKER_END}`;
117
+
118
+ const importMatches = [...css.matchAll(/^@import\s+[^;\n]+;\s*$/gmu)];
119
+ if (importMatches.length > 0) {
120
+ const lastMatch = importMatches.at(-1);
121
+ const insertAt = lastMatch.index + lastMatch[0].length;
122
+ css = `${css.slice(0, insertAt)}\n\n${wrapped}${css.slice(insertAt)}`;
123
+ } else {
124
+ css = `${css.trimEnd()}\n\n${wrapped}\n`;
125
+ }
126
+
127
+ await writeFile(cssFullPath, css);
128
+ s.stop(`${pc.green("✓")} theme block`);
129
+
130
+ // 3. lib/utils.ts
131
+ if (!existsSync(path.join(cwd, "lib/utils.ts"))) {
132
+ s.start("creating lib/utils.ts");
133
+ await mkdir(path.join(cwd, "lib"), { recursive: true });
134
+ await writeFile(
135
+ path.join(cwd, "lib/utils.ts"),
136
+ `import { clsx, type ClassValue } from "clsx";
137
+ import { twMerge } from "tailwind-merge";
138
+
139
+ export function cn(...inputs: ClassValue[]) {
140
+ return twMerge(clsx(inputs));
141
+ }
142
+ `
143
+ );
144
+ s.stop(`${pc.green("✓")} lib/utils.ts`);
145
+ }
146
+
147
+ // 3b. lib/animation.ts
148
+ if (!existsSync(path.join(cwd, "lib/animation.ts"))) {
149
+ s.start("creating lib/animation.ts");
150
+ await mkdir(path.join(cwd, "lib"), { recursive: true });
151
+ await writeFile(
152
+ path.join(cwd, "lib/animation.ts"),
153
+ `export const SPRING_SMOOTH = { type: "spring", stiffness: 200, damping: 30 } as const
154
+
155
+ export const EASE_OUT = [0.16, 1, 0.3, 1] as const
156
+ export const EASE_SMOOTH = [0.32, 0.72, 0, 1] as const
157
+ export const EASE_IN_OUT = [0.4, 0, 0.2, 1] as const
158
+
159
+ export const DURATION_FAST = 0.2
160
+ export const DURATION_SMOOTH = 0.45
161
+ `
162
+ );
163
+ s.stop(`${pc.green("✓")} lib/animation.ts`);
164
+ }
165
+
166
+ // 4. dependencies
167
+ const pkgPath = path.join(cwd, "package.json");
168
+ const pkgRaw = await readFile(pkgPath, "utf-8");
169
+ const pkg = JSON.parse(pkgRaw.replace(/^\uFEFF/u, ""));
170
+ const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
171
+ const missing = [
172
+ "clsx",
173
+ "tailwind-merge",
174
+ "class-variance-authority",
175
+ "radix-ui",
176
+ "@phosphor-icons/react",
177
+ "motion",
178
+ ].filter((d) => !allDeps[d]);
179
+
180
+ if (missing.length > 0) {
181
+ s.start(`installing ${missing.join(", ")}`);
182
+ const pm = detectPackageManager(cwd);
183
+ try {
184
+ const args = pm === "npm" ? ["install", ...missing] : ["add", ...missing];
185
+ await execa(pm, args, { cwd, stdio: "ignore" });
186
+ s.stop(`${pc.green("✓")} installed ${missing.length} package(s)`);
187
+ } catch (error) {
188
+ s.stop(`${pc.red("✗")} failed to install dependencies`);
189
+ p.log.warn(error.message);
190
+ }
191
+ }
192
+
193
+ p.outro(
194
+ `${pc.green("✓")} frakment is ready\n\n` +
195
+ ` Next ${pc.cyan("frakment add status-chip")}`
196
+ );
197
+ }
198
+
199
+ function escapeRegex(s) {
200
+ return s.replaceAll(/[.*+?^${}()|[\]\\]/gu, "\\$&");
201
+ }
@@ -0,0 +1,61 @@
1
+ import { existsSync } from "node:fs";
2
+ import { readFile } from "node:fs/promises";
3
+ import path from "node:path";
4
+
5
+ import * as p from "@clack/prompts";
6
+ import pc from "picocolors";
7
+
8
+ import { findProjectRoot } from "../utils/detect.js";
9
+
10
+ export async function list(options) {
11
+ p.intro(`${pc.dim("frakment")} list`);
12
+
13
+ let cwd;
14
+ try {
15
+ cwd = findProjectRoot(process.cwd());
16
+ } catch (error) {
17
+ p.outro(pc.red("error ") + error.message);
18
+ process.exit(1);
19
+ }
20
+
21
+ const configPath = path.join(cwd, "components.json");
22
+ if (!existsSync(configPath)) {
23
+ p.outro(
24
+ `${pc.red("error ")}components.json not found. Run \`frakment init\` first.`
25
+ );
26
+ process.exit(1);
27
+ }
28
+
29
+ const config = JSON.parse(await readFile(configPath, "utf-8"));
30
+ const registryUrl = (options.registry ?? config.registry).replace(/\/$/u, "");
31
+ const s = p.spinner();
32
+ s.start("fetching components");
33
+
34
+ let index;
35
+ try {
36
+ const res = await fetch(`${registryUrl}/index.json`);
37
+ if (!res.ok) {
38
+ throw new Error(`HTTP ${res.status}`);
39
+ }
40
+ index = await res.json();
41
+ } catch {
42
+ s.stop(`${pc.red("✗")} failed to fetch component list`);
43
+ process.exit(1);
44
+ }
45
+ s.stop(`${pc.green("✓")} found ${index.components.length} component(s)`);
46
+
47
+ if (!index.components.length) {
48
+ p.outro(pc.dim("no components available yet"));
49
+ return;
50
+ }
51
+
52
+ const nameWidth = Math.max(...index.components.map((c) => c.name.length));
53
+ const lines = index.components.map(
54
+ (c) =>
55
+ ` ${pc.cyan(c.name.padEnd(nameWidth))} ${pc.dim(c.description ?? "")}`
56
+ );
57
+
58
+ p.outro(
59
+ `${lines.join("\n")}\n\n Add one with ${pc.cyan("frakment add <name>")}`
60
+ );
61
+ }
@@ -0,0 +1,38 @@
1
+ import { existsSync } from "node:fs";
2
+ import path from "node:path";
3
+
4
+ export function findProjectRoot(startDir) {
5
+ let dir = startDir;
6
+ while (dir !== path.dirname(dir)) {
7
+ if (existsSync(path.join(dir, "package.json"))) {
8
+ return dir;
9
+ }
10
+ dir = path.dirname(dir);
11
+ }
12
+ throw new Error("No package.json found. Are you inside a project?");
13
+ }
14
+
15
+ export function detectPackageManager(cwd) {
16
+ if (existsSync(path.join(cwd, "pnpm-lock.yaml"))) {
17
+ return "pnpm";
18
+ }
19
+ if (existsSync(path.join(cwd, "yarn.lock"))) {
20
+ return "yarn";
21
+ }
22
+ if (existsSync(path.join(cwd, "bun.lockb"))) {
23
+ return "bun";
24
+ }
25
+ return "npm";
26
+ }
27
+
28
+ export function detectGlobalsCss(cwd) {
29
+ const candidates = [
30
+ "app/globals.css",
31
+ "src/app/globals.css",
32
+ "styles/globals.css",
33
+ "src/styles/globals.css",
34
+ ];
35
+ return (
36
+ candidates.find((p) => existsSync(path.join(cwd, p))) ?? "app/globals.css"
37
+ );
38
+ }
@@ -0,0 +1,11 @@
1
+ {
2
+ "registry": "https://frakment.vercel.app/r",
3
+ "aliases": {
4
+ "components": "@/components",
5
+ "ui": "@/components/ui",
6
+ "utils": "@/lib/utils"
7
+ },
8
+ "tailwind": {
9
+ "css": "app/globals.css"
10
+ }
11
+ }
@@ -0,0 +1,12 @@
1
+ export const SPRING_SMOOTH = {
2
+ damping: 30,
3
+ stiffness: 200,
4
+ type: "spring",
5
+ } as const;
6
+
7
+ export const EASE_OUT = [0.16, 1, 0.3, 1] as const;
8
+ export const EASE_SMOOTH = [0.32, 0.72, 0, 1] as const;
9
+ export const EASE_IN_OUT = [0.4, 0, 0.2, 1] as const;
10
+
11
+ export const DURATION_FAST = 0.2;
12
+ export const DURATION_SMOOTH = 0.45;