tweenlabs 0.1.3 → 0.1.6
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/README.md +118 -3
- package/bin/cli.js +303 -90
- package/package.json +62 -60
package/README.md
CHANGED
|
@@ -7,15 +7,15 @@ keywords: TweenLabs, GSAP component library, GSAP Next.js, GSAP animations React
|
|
|
7
7
|
|
|
8
8
|
# <img src="https://raw.githubusercontent.com/TweenLabs/TweenLabs/master/public/logo.svg" alt="TweenLabs Logo" width="40" height="40" align="center" /> TweenLabs
|
|
9
9
|
|
|
10
|
-
|
|
11
10
|
> The open-source **GSAP animation component library** for Next.js developers — learn, copy, and contribute modern web animation patterns built with **GSAP 3.15**, **Next.js 16**, and **Lenis**.
|
|
12
11
|
|
|
13
|
-
**[Live Demo](https://tweenlabs.xyz)** • **[Contributing Guide](#contributing)** • **[Roadmap](#roadmap)**
|
|
12
|
+
**[Live Demo](https://tweenlabs.xyz)** • **[Contributing Guide](#contributing)** • **[Roadmap](#roadmap)**
|
|
14
13
|
|
|
15
14
|

|
|
16
15
|

|
|
17
16
|

|
|
18
17
|

|
|
18
|
+

|
|
19
19
|
|
|
20
20
|
<!-- [Contributors](https://img.shields.io/github/contributors/TweenLabs/TweenLabs?style=flat-square) -->
|
|
21
21
|
|
|
@@ -31,6 +31,114 @@ No paid plugins. No locked content. Just clean, modern animation patterns anyone
|
|
|
31
31
|
|
|
32
32
|
---
|
|
33
33
|
|
|
34
|
+
## ⚡ CLI — Install Components Instantly
|
|
35
|
+
|
|
36
|
+
TweenLabs ships with a **zero-dependency CLI** that lets you pull components directly into your codebase — no copy-paste required.
|
|
37
|
+
|
|
38
|
+
### Initialize configuration
|
|
39
|
+
|
|
40
|
+
Run the `init` command to configure your preferred installation path. This creates a `tweenlabs.config.json` file in the root of your project, meaning you won't be prompted for the path when adding components in the future.
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
npx tweenlabs@latest init
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
By default, it will detect your project setup and suggest `./src/components/tweenlabs` or `./components/tweenlabs`.
|
|
47
|
+
|
|
48
|
+
### Add a component
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
npx tweenlabs@latest add <component-slug>
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
This will:
|
|
55
|
+
1. Fetch the component files from the TweenLabs registry
|
|
56
|
+
2. Detect your project layout (supports `src/` and non-`src/` setups)
|
|
57
|
+
3. Automatically resolve the install path to `src/components/tweenlabs/`
|
|
58
|
+
4. Create the directory if it doesn't exist
|
|
59
|
+
5. Detect and install any missing npm dependencies
|
|
60
|
+
|
|
61
|
+
### Browse & install interactively
|
|
62
|
+
|
|
63
|
+
Run `add` without a slug to get an interactive picker:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
npx tweenlabs@latest add
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
```
|
|
70
|
+
▲ tweenlabs v0.1.6
|
|
71
|
+
|
|
72
|
+
Select a component to install:
|
|
73
|
+
|
|
74
|
+
[1] . All Components
|
|
75
|
+
[2] gravity-drop Physics-based falling animations with realistic bounce
|
|
76
|
+
[3] scroll-assembly Content reveals synced with scroll position
|
|
77
|
+
[4] border-reveal Inward/outward border animations
|
|
78
|
+
...
|
|
79
|
+
|
|
80
|
+
👉 Enter the number of the component to add (1-8):
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### List all available components
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
npx tweenlabs@latest list
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### Install all components at once
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
npx tweenlabs@latest add .
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
## 📁 Output Structure
|
|
98
|
+
|
|
99
|
+
After installation, components land here by default:
|
|
100
|
+
|
|
101
|
+
```
|
|
102
|
+
your-project/
|
|
103
|
+
└── src/
|
|
104
|
+
└── components/
|
|
105
|
+
└── tweenlabs/
|
|
106
|
+
├── GravityDrop.tsx
|
|
107
|
+
├── BorderReveal.tsx
|
|
108
|
+
└── ...
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
> **tweenlabs.config.json:** If a `tweenlabs.config.json` exists in your project's root, the CLI reads the `path` option and installs components there. This takes highest precedence.
|
|
112
|
+
>
|
|
113
|
+
> **shadcn UI users:** If no `tweenlabs.config.json` exists but your project has a `components.json`, TweenLabs reads the `aliases.components` field and installs into the matching directory under a `tweenlabs/` subfolder automatically.
|
|
114
|
+
|
|
115
|
+
---
|
|
116
|
+
|
|
117
|
+
## 🚩 CLI Flags
|
|
118
|
+
|
|
119
|
+
| Flag | Short | Description |
|
|
120
|
+
|------|-------|-------------|
|
|
121
|
+
| `--yes` | `-y` | Skip all prompts; accept all defaults and auto-install dependencies |
|
|
122
|
+
| `--path <dir>` | `-p` | Override the install directory |
|
|
123
|
+
| `--overwrite` | `-o` | Overwrite existing files without prompting |
|
|
124
|
+
| `--help` | `-h` | Show help |
|
|
125
|
+
| `--version` | `-v` | Show CLI version |
|
|
126
|
+
|
|
127
|
+
### Examples
|
|
128
|
+
|
|
129
|
+
```bash
|
|
130
|
+
# Install with a custom path
|
|
131
|
+
npx tweenlabs@latest add gravity-drop --path src/ui/animations
|
|
132
|
+
|
|
133
|
+
# Install all, skip all prompts, overwrite existing files
|
|
134
|
+
npx tweenlabs@latest add . --yes --overwrite
|
|
135
|
+
|
|
136
|
+
# Install with pnpm dlx
|
|
137
|
+
pnpm dlx tweenlabs add border-reveal
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
---
|
|
141
|
+
|
|
34
142
|
## ✨ Animation Components
|
|
35
143
|
|
|
36
144
|
| Component | Description | GSAP Features Used |
|
|
@@ -57,7 +165,9 @@ No paid plugins. No locked content. Just clean, modern animation patterns anyone
|
|
|
57
165
|
|
|
58
166
|
---
|
|
59
167
|
|
|
60
|
-
## 🚀 Quick Start
|
|
168
|
+
## 🚀 Quick Start (Playground)
|
|
169
|
+
|
|
170
|
+
Want to run the full TweenLabs playground locally?
|
|
61
171
|
|
|
62
172
|
```bash
|
|
63
173
|
# Install pnpm if you don't have it
|
|
@@ -123,6 +233,11 @@ git push origin feat/your-animation-name
|
|
|
123
233
|
|
|
124
234
|
## 🗺 Roadmap
|
|
125
235
|
|
|
236
|
+
- [x] CLI — install components via `npx tweenlabs add`
|
|
237
|
+
- [x] Auto-detect package manager (npm, pnpm, yarn, bun)
|
|
238
|
+
- [x] Auto-install missing dependencies
|
|
239
|
+
- [x] Interactive component picker
|
|
240
|
+
- [x] `components.json` / path resolution
|
|
126
241
|
- [ ] Export as npm package (`@tweenlabs/components`)
|
|
127
242
|
- [ ] Storybook integration for isolated component previews
|
|
128
243
|
- [ ] Unit tests for animation logic
|
package/bin/cli.js
CHANGED
|
@@ -22,13 +22,14 @@ const helpText = `
|
|
|
22
22
|
${colors.bold}${colors.cyan}TweenLabs CLI${colors.reset} - Install premium GSAP components directly into your codebase.
|
|
23
23
|
|
|
24
24
|
${colors.bold}Usage:${colors.reset}
|
|
25
|
+
npx tweenlabs init Initialize configuration file
|
|
25
26
|
npx tweenlabs list List all available components
|
|
26
27
|
npx tweenlabs add <component-slug> Install a specific component
|
|
27
28
|
|
|
28
29
|
${colors.bold}Options:${colors.reset}
|
|
29
30
|
-y, --yes Skip all prompts (auto-accept defaults & install dependencies)
|
|
30
31
|
-p, --path <path> Specify a custom directory to install the component
|
|
31
|
-
-o, --overwrite Overwrite existing component files without prompting
|
|
32
|
+
-o, --overwrite Overwrite existing component files/configuration without prompting
|
|
32
33
|
-h, --help Show this help message
|
|
33
34
|
-v, --version Show version
|
|
34
35
|
`;
|
|
@@ -79,6 +80,31 @@ function askQuestion(query) {
|
|
|
79
80
|
);
|
|
80
81
|
}
|
|
81
82
|
|
|
83
|
+
// Levenshtein distance helper for spelling suggestions
|
|
84
|
+
function getLevenshteinDistance(a, b) {
|
|
85
|
+
const tmp = [];
|
|
86
|
+
let i;
|
|
87
|
+
let j;
|
|
88
|
+
const alen = a.length;
|
|
89
|
+
const blen = b.length;
|
|
90
|
+
if (alen === 0) return blen;
|
|
91
|
+
if (blen === 0) return alen;
|
|
92
|
+
for (i = 0; i <= alen; i++) {
|
|
93
|
+
tmp[i] = [i];
|
|
94
|
+
}
|
|
95
|
+
for (j = 0; j <= blen; j++) {
|
|
96
|
+
tmp[0][j] = j;
|
|
97
|
+
}
|
|
98
|
+
for (i = 1; i <= alen; i++) {
|
|
99
|
+
for (j = 1; j <= blen; j++) {
|
|
100
|
+
tmp[i][j] = a.charAt(i - 1) === b.charAt(j - 1)
|
|
101
|
+
? tmp[i - 1][j - 1]
|
|
102
|
+
: Math.min(tmp[i - 1][j - 1] + 1, Math.min(tmp[i][j - 1] + 1, tmp[i - 1][j] + 1));
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return tmp[alen][blen];
|
|
106
|
+
}
|
|
107
|
+
|
|
82
108
|
// Detect client's package manager
|
|
83
109
|
function detectPackageManager() {
|
|
84
110
|
if (fs.existsSync(path.join(process.cwd(), "pnpm-lock.yaml"))) return "pnpm";
|
|
@@ -95,16 +121,22 @@ async function main() {
|
|
|
95
121
|
process.exit(0);
|
|
96
122
|
}
|
|
97
123
|
|
|
124
|
+
let version = "0.1.6";
|
|
125
|
+
try {
|
|
126
|
+
const pkg = require("../package.json");
|
|
127
|
+
version = pkg.version;
|
|
128
|
+
} catch (_e) {}
|
|
129
|
+
|
|
98
130
|
if (args.includes("--version") || args.includes("-v")) {
|
|
99
|
-
|
|
100
|
-
const pkg = require("../package.json");
|
|
101
|
-
console.log(pkg.version);
|
|
102
|
-
} catch (_err) {
|
|
103
|
-
console.log("0.1.1");
|
|
104
|
-
}
|
|
131
|
+
console.log(version);
|
|
105
132
|
process.exit(0);
|
|
106
133
|
}
|
|
107
134
|
|
|
135
|
+
// Print CLI Header Banner
|
|
136
|
+
console.log(
|
|
137
|
+
`\n${colors.bold}${colors.cyan}▲ tweenlabs${colors.reset} ${colors.gray}v${version}${colors.reset}\n`,
|
|
138
|
+
);
|
|
139
|
+
|
|
108
140
|
// Parse flags
|
|
109
141
|
const isYes = args.includes("-y") || args.includes("--yes");
|
|
110
142
|
const isOverwrite = args.includes("-o") || args.includes("--overwrite");
|
|
@@ -139,29 +171,86 @@ async function main() {
|
|
|
139
171
|
process.exit(0);
|
|
140
172
|
}
|
|
141
173
|
|
|
174
|
+
if (cleanArgs[0] === "init") {
|
|
175
|
+
const configPath = path.join(process.cwd(), "tweenlabs.config.json");
|
|
176
|
+
if (fs.existsSync(configPath) && !isOverwrite && !isYes) {
|
|
177
|
+
console.log(
|
|
178
|
+
`${colors.yellow}! tweenlabs.config.json already exists.${colors.reset}`
|
|
179
|
+
);
|
|
180
|
+
const overwriteConfirm = await askQuestion(
|
|
181
|
+
`Overwrite configuration file? (y/n) ${colors.gray}[y]${colors.reset}: `
|
|
182
|
+
);
|
|
183
|
+
if (
|
|
184
|
+
overwriteConfirm &&
|
|
185
|
+
overwriteConfirm.toLowerCase() !== "y" &&
|
|
186
|
+
overwriteConfirm.toLowerCase() !== "yes"
|
|
187
|
+
) {
|
|
188
|
+
console.log(`${colors.yellow}! Configuration initialization cancelled.${colors.reset}`);
|
|
189
|
+
process.exit(0);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
let defaultDir = "";
|
|
194
|
+
if (fs.existsSync(path.join(process.cwd(), "src"))) {
|
|
195
|
+
defaultDir = "./src/components/tweenlabs";
|
|
196
|
+
} else {
|
|
197
|
+
defaultDir = "./components/tweenlabs";
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
let targetPath = defaultDir;
|
|
201
|
+
if (!isYes) {
|
|
202
|
+
const inputPath = await askQuestion(
|
|
203
|
+
`Configure component installation path (${defaultDir}): `
|
|
204
|
+
);
|
|
205
|
+
if (inputPath) {
|
|
206
|
+
targetPath = inputPath;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
try {
|
|
211
|
+
const configData = {
|
|
212
|
+
path: targetPath,
|
|
213
|
+
};
|
|
214
|
+
fs.writeFileSync(configPath, JSON.stringify(configData, null, 2), "utf-8");
|
|
215
|
+
console.log(
|
|
216
|
+
`\n${colors.green}✔ Created tweenlabs.config.json with path: ${colors.bold}${targetPath}${colors.reset}\n`
|
|
217
|
+
);
|
|
218
|
+
process.exit(0);
|
|
219
|
+
} catch (err) {
|
|
220
|
+
console.error(
|
|
221
|
+
`${colors.red}Error: Failed to create tweenlabs.config.json.${colors.reset}`
|
|
222
|
+
);
|
|
223
|
+
console.error(`${colors.gray}Details: ${err.message}${colors.reset}`);
|
|
224
|
+
process.exit(1);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
142
228
|
if (cleanArgs[0] === "list") {
|
|
143
|
-
console.log(
|
|
144
|
-
`\n${colors.cyan}🔍 Fetching available components list...${colors.reset}`,
|
|
145
|
-
);
|
|
229
|
+
console.log(`${colors.cyan}Fetching registry...${colors.reset}`);
|
|
146
230
|
const domain =
|
|
147
231
|
process.env.TWEENLABS_REGISTRY_URL || "https://tweenlabs.xyz";
|
|
148
232
|
const url = `${domain}/api/registry/list`;
|
|
149
233
|
try {
|
|
150
234
|
const data = await fetchJson(url);
|
|
151
235
|
console.log(
|
|
152
|
-
`\n${colors.bold}${colors.green}Available
|
|
236
|
+
`\n${colors.bold}${colors.green}Available Components:${colors.reset}\n`,
|
|
153
237
|
);
|
|
154
|
-
|
|
238
|
+
const rows = data.components.map((comp) => ({
|
|
239
|
+
slug: comp.cleanSlug || comp.slug,
|
|
240
|
+
desc: comp.description || "",
|
|
241
|
+
}));
|
|
242
|
+
const maxSlugLen = Math.max(...rows.map((r) => r.slug.length), 10);
|
|
243
|
+
for (const row of rows) {
|
|
244
|
+
const paddedSlug = row.slug.padEnd(maxSlugLen + 4, " ");
|
|
155
245
|
console.log(
|
|
156
|
-
` ${colors.
|
|
246
|
+
` ${colors.cyan}${paddedSlug}${colors.reset}${colors.gray}${row.desc}${colors.reset}`,
|
|
157
247
|
);
|
|
158
|
-
console.log(` Slug: ${comp.slug} (or ${comp.cleanSlug})`);
|
|
159
|
-
console.log(` Desc: ${comp.description}\n`);
|
|
160
248
|
}
|
|
249
|
+
console.log("");
|
|
161
250
|
process.exit(0);
|
|
162
251
|
} catch (err) {
|
|
163
252
|
console.error(
|
|
164
|
-
|
|
253
|
+
`${colors.red}Error: Failed to fetch components list.${colors.reset}`,
|
|
165
254
|
);
|
|
166
255
|
console.error(`${colors.gray}Details: ${err.message}${colors.reset}`);
|
|
167
256
|
process.exit(1);
|
|
@@ -170,81 +259,123 @@ async function main() {
|
|
|
170
259
|
|
|
171
260
|
if (cleanArgs[0] !== "add") {
|
|
172
261
|
console.log(
|
|
173
|
-
`${colors.red}Error: Unknown command "${cleanArgs[0]}". Did you mean "add" or "
|
|
262
|
+
`${colors.red}Error: Unknown command "${cleanArgs[0]}". Did you mean "add", "list" or "init"?${colors.reset}`,
|
|
174
263
|
);
|
|
175
264
|
console.log(helpText);
|
|
176
265
|
process.exit(1);
|
|
177
266
|
}
|
|
178
267
|
|
|
179
|
-
|
|
268
|
+
let componentSlug = cleanArgs[1];
|
|
180
269
|
if (!componentSlug) {
|
|
270
|
+
console.log(`${colors.cyan}Fetching registry...${colors.reset}`);
|
|
271
|
+
const domain =
|
|
272
|
+
process.env.TWEENLABS_REGISTRY_URL || "https://tweenlabs.xyz";
|
|
273
|
+
const listUrl = `${domain}/api/registry/list`;
|
|
274
|
+
let listData;
|
|
275
|
+
try {
|
|
276
|
+
listData = await fetchJson(listUrl);
|
|
277
|
+
} catch (err) {
|
|
278
|
+
console.error(
|
|
279
|
+
`${colors.red}Error: Failed to fetch components list.${colors.reset}`,
|
|
280
|
+
);
|
|
281
|
+
console.error(`${colors.gray}Details: ${err.message}${colors.reset}`);
|
|
282
|
+
process.exit(1);
|
|
283
|
+
}
|
|
284
|
+
|
|
181
285
|
console.log(
|
|
182
|
-
`${colors.
|
|
286
|
+
`${colors.bold}${colors.green}Select a component to install:${colors.reset}\n`,
|
|
183
287
|
);
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
288
|
+
const components = listData.components;
|
|
289
|
+
const maxIndexLen = String(components.length + 1).length;
|
|
290
|
+
const maxSlugLen = Math.max(
|
|
291
|
+
...components.map((c) => (c.cleanSlug || c.slug).length),
|
|
292
|
+
10,
|
|
188
293
|
);
|
|
189
|
-
process.exit(1);
|
|
190
|
-
}
|
|
191
294
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
295
|
+
// Print "All Components" option first
|
|
296
|
+
const allIndexStr = "[1]".padStart(maxIndexLen + 2, " ");
|
|
297
|
+
const allSlugStr = ".".padEnd(maxSlugLen + 4, " ");
|
|
298
|
+
console.log(
|
|
299
|
+
` ${colors.bold}${colors.cyan}${allIndexStr}${colors.reset} ${colors.cyan}${allSlugStr}${colors.reset}${colors.gray}All Components${colors.reset}`,
|
|
300
|
+
);
|
|
195
301
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
302
|
+
for (let i = 0; i < components.length; i++) {
|
|
303
|
+
const indexStr = `[${i + 2}]`.padStart(maxIndexLen + 2, " ");
|
|
304
|
+
const slugStr = (components[i].cleanSlug || components[i].slug).padEnd(
|
|
305
|
+
maxSlugLen + 4,
|
|
306
|
+
" ",
|
|
307
|
+
);
|
|
308
|
+
console.log(
|
|
309
|
+
` ${colors.bold}${colors.cyan}${indexStr}${colors.reset} ${colors.cyan}${slugStr}${colors.reset}${colors.gray}${components[i].name}${colors.reset}`,
|
|
310
|
+
);
|
|
311
|
+
}
|
|
312
|
+
console.log("");
|
|
199
313
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
componentData = await fetchJson(url);
|
|
203
|
-
} catch (err) {
|
|
204
|
-
console.error(
|
|
205
|
-
`\n${colors.red}❌ Failed to fetch component. Make sure the slug is correct and the server is running.${colors.reset}`,
|
|
314
|
+
const choiceStr = await askQuestion(
|
|
315
|
+
`? Enter the number of the component to add (1-${components.length + 1}): `,
|
|
206
316
|
);
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
317
|
+
const choice = parseInt(choiceStr, 10);
|
|
318
|
+
if (Number.isNaN(choice) || choice < 1 || choice > components.length + 1) {
|
|
319
|
+
console.log(`${colors.red}Error: Invalid choice. Exiting.${colors.reset}`);
|
|
320
|
+
process.exit(1);
|
|
321
|
+
}
|
|
210
322
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
323
|
+
if (choice === 1) {
|
|
324
|
+
componentSlug = ".";
|
|
325
|
+
} else {
|
|
326
|
+
componentSlug =
|
|
327
|
+
components[choice - 2].cleanSlug || components[choice - 2].slug;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
214
330
|
|
|
215
331
|
// Resolve target directory
|
|
216
332
|
let targetDir = "";
|
|
217
333
|
if (customPath) {
|
|
218
334
|
targetDir = path.resolve(process.cwd(), customPath);
|
|
219
335
|
} else {
|
|
220
|
-
// 1. Try to read
|
|
221
|
-
const
|
|
222
|
-
if (fs.existsSync(
|
|
336
|
+
// 1. Try to read tweenlabs.config.json (our config)
|
|
337
|
+
const tweenlabsConfigPath = path.join(process.cwd(), "tweenlabs.config.json");
|
|
338
|
+
if (fs.existsSync(tweenlabsConfigPath)) {
|
|
223
339
|
try {
|
|
224
|
-
const config = JSON.parse(fs.readFileSync(
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
const cleanAlias = compAlias.replace(/^[@~]\//, "");
|
|
228
|
-
if (
|
|
229
|
-
fs.existsSync(path.join(process.cwd(), "src")) &&
|
|
230
|
-
!cleanAlias.startsWith("src/")
|
|
231
|
-
) {
|
|
232
|
-
targetDir = path.join(
|
|
233
|
-
process.cwd(),
|
|
234
|
-
"src",
|
|
235
|
-
cleanAlias,
|
|
236
|
-
"tweenlabs",
|
|
237
|
-
);
|
|
238
|
-
} else {
|
|
239
|
-
targetDir = path.join(process.cwd(), cleanAlias, "tweenlabs");
|
|
240
|
-
}
|
|
340
|
+
const config = JSON.parse(fs.readFileSync(tweenlabsConfigPath, "utf-8"));
|
|
341
|
+
if (config.path) {
|
|
342
|
+
targetDir = path.resolve(process.cwd(), config.path);
|
|
241
343
|
}
|
|
242
344
|
} catch (_err) {
|
|
243
345
|
// Ignore JSON parse errors
|
|
244
346
|
}
|
|
245
347
|
}
|
|
246
348
|
|
|
247
|
-
// 2.
|
|
349
|
+
// 2. Try to read components.json (shadcn configuration) if still not resolved
|
|
350
|
+
if (!targetDir) {
|
|
351
|
+
const componentsJsonPath = path.join(process.cwd(), "components.json");
|
|
352
|
+
if (fs.existsSync(componentsJsonPath)) {
|
|
353
|
+
try {
|
|
354
|
+
const config = JSON.parse(fs.readFileSync(componentsJsonPath, "utf-8"));
|
|
355
|
+
const compAlias = config.aliases?.components;
|
|
356
|
+
if (compAlias) {
|
|
357
|
+
const cleanAlias = compAlias.replace(/^[@~]\//, "");
|
|
358
|
+
if (
|
|
359
|
+
fs.existsSync(path.join(process.cwd(), "src")) &&
|
|
360
|
+
!cleanAlias.startsWith("src/")
|
|
361
|
+
) {
|
|
362
|
+
targetDir = path.join(
|
|
363
|
+
process.cwd(),
|
|
364
|
+
"src",
|
|
365
|
+
cleanAlias,
|
|
366
|
+
"tweenlabs",
|
|
367
|
+
);
|
|
368
|
+
} else {
|
|
369
|
+
targetDir = path.join(process.cwd(), cleanAlias, "tweenlabs");
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
} catch (_err) {
|
|
373
|
+
// Ignore JSON parse errors
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// 3. Fallback if targetDir is still not resolved
|
|
248
379
|
if (!targetDir) {
|
|
249
380
|
if (fs.existsSync(path.join(process.cwd(), "src"))) {
|
|
250
381
|
targetDir = path.join(process.cwd(), "src", "components", "tweenlabs");
|
|
@@ -255,29 +386,111 @@ async function main() {
|
|
|
255
386
|
}
|
|
256
387
|
|
|
257
388
|
console.log(
|
|
258
|
-
|
|
389
|
+
`Target directory: ${colors.bold}${path.relative(process.cwd(), targetDir)}${colors.reset}`,
|
|
259
390
|
);
|
|
260
391
|
|
|
261
|
-
|
|
262
|
-
let
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
392
|
+
const domain = process.env.TWEENLABS_REGISTRY_URL || "https://tweenlabs.xyz";
|
|
393
|
+
let slugsToInstall = [];
|
|
394
|
+
if (componentSlug === "." || componentSlug === "all") {
|
|
395
|
+
console.log(
|
|
396
|
+
`${colors.cyan}Fetching all components list...${colors.reset}`,
|
|
397
|
+
);
|
|
398
|
+
const listUrl = `${domain}/api/registry/list`;
|
|
399
|
+
try {
|
|
400
|
+
const listData = await fetchJson(listUrl);
|
|
401
|
+
slugsToInstall = listData.components.map((c) => c.cleanSlug || c.slug);
|
|
402
|
+
} catch (err) {
|
|
403
|
+
console.error(
|
|
404
|
+
`${colors.red}Error: Failed to fetch components list.${colors.reset}`,
|
|
405
|
+
);
|
|
406
|
+
console.error(`${colors.gray}Details: ${err.message}${colors.reset}`);
|
|
407
|
+
process.exit(1);
|
|
268
408
|
}
|
|
409
|
+
} else {
|
|
410
|
+
slugsToInstall = [componentSlug];
|
|
269
411
|
}
|
|
270
412
|
|
|
271
|
-
|
|
413
|
+
// Gather files and check conflicts
|
|
414
|
+
const filesToWrite = [];
|
|
415
|
+
const conflicts = [];
|
|
416
|
+
const allRequiredDeps = new Set();
|
|
417
|
+
|
|
418
|
+
for (const slug of slugsToInstall) {
|
|
419
|
+
console.log(
|
|
420
|
+
`${colors.cyan}Fetching ${colors.bold}${slug}${colors.reset}${colors.cyan} registry data...${colors.reset}`,
|
|
421
|
+
);
|
|
422
|
+
const url = `${domain}/api/registry/${slug}`;
|
|
423
|
+
try {
|
|
424
|
+
const componentData = await fetchJson(url);
|
|
425
|
+
for (const file of componentData.files) {
|
|
426
|
+
const filePath = path.join(targetDir, file.name);
|
|
427
|
+
if (fs.existsSync(filePath)) {
|
|
428
|
+
conflicts.push(path.relative(process.cwd(), filePath));
|
|
429
|
+
}
|
|
430
|
+
filesToWrite.push({ path: filePath, content: file.content });
|
|
431
|
+
}
|
|
432
|
+
const dependencies = componentData.dependencies || [];
|
|
433
|
+
for (const dep of dependencies) {
|
|
434
|
+
allRequiredDeps.add(dep);
|
|
435
|
+
}
|
|
436
|
+
} catch (_err) {
|
|
437
|
+
console.error(
|
|
438
|
+
`${colors.red}Error: Failed to fetch component "${slug}". Skipping.${colors.reset}`,
|
|
439
|
+
);
|
|
440
|
+
// Fetch registry list to suggest closest matches
|
|
441
|
+
let suggestions = [];
|
|
442
|
+
try {
|
|
443
|
+
const listData = await fetchJson(`${domain}/api/registry/list`);
|
|
444
|
+
const validSlugs = listData.components.map((c) => c.cleanSlug || c.slug);
|
|
445
|
+
|
|
446
|
+
// Find slugs with low Levenshtein distance
|
|
447
|
+
const matches = validSlugs.map((validSlug) => {
|
|
448
|
+
return {
|
|
449
|
+
slug: validSlug,
|
|
450
|
+
distance: getLevenshteinDistance(slug, validSlug)
|
|
451
|
+
};
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
// Sort by distance ascending
|
|
455
|
+
matches.sort((a, b) => a.distance - b.distance);
|
|
456
|
+
|
|
457
|
+
// Threshold: distance <= 3 or distance <= half of query length
|
|
458
|
+
suggestions = matches
|
|
459
|
+
.filter((m) => m.distance <= 3 || m.distance <= Math.round(slug.length / 2))
|
|
460
|
+
.map((m) => m.slug);
|
|
461
|
+
} catch (_e) {
|
|
462
|
+
// If list fetch fails, we just don't show suggestions
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
if (suggestions.length > 0) {
|
|
466
|
+
console.error(
|
|
467
|
+
`${colors.yellow}Did you mean: ${suggestions.map(s => `${colors.bold}${s}${colors.reset}`).join(", ")}?${colors.reset}\n`
|
|
468
|
+
);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
if (filesToWrite.length === 0) {
|
|
474
|
+
console.log(`${colors.red}Error: No files to install. Exiting.${colors.reset}`);
|
|
475
|
+
process.exit(1);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
if (conflicts.length > 0 && !isOverwrite && !isYes) {
|
|
479
|
+
console.log(
|
|
480
|
+
`\n${colors.yellow}! The following files already exist:${colors.reset}`,
|
|
481
|
+
);
|
|
482
|
+
for (const conflict of conflicts) {
|
|
483
|
+
console.log(` → ${conflict}`);
|
|
484
|
+
}
|
|
272
485
|
const overwriteConfirm = await askQuestion(
|
|
273
|
-
|
|
486
|
+
`\nOverwrite these files? (y/n) ${colors.gray}[y]${colors.reset}: `,
|
|
274
487
|
);
|
|
275
488
|
if (
|
|
276
489
|
overwriteConfirm &&
|
|
277
490
|
overwriteConfirm.toLowerCase() !== "y" &&
|
|
278
491
|
overwriteConfirm.toLowerCase() !== "yes"
|
|
279
492
|
) {
|
|
280
|
-
console.log(`${colors.yellow}Installation cancelled.${colors.reset}`);
|
|
493
|
+
console.log(`${colors.yellow}! Installation cancelled.${colors.reset}`);
|
|
281
494
|
process.exit(0);
|
|
282
495
|
}
|
|
283
496
|
}
|
|
@@ -288,19 +501,17 @@ async function main() {
|
|
|
288
501
|
}
|
|
289
502
|
|
|
290
503
|
// Write component files
|
|
291
|
-
console.log(`\n${colors.
|
|
292
|
-
for (const file of
|
|
293
|
-
|
|
294
|
-
fs.writeFileSync(filePath, file.content, "utf-8");
|
|
504
|
+
console.log(`\n${colors.bold}Writing component files...${colors.reset}`);
|
|
505
|
+
for (const file of filesToWrite) {
|
|
506
|
+
fs.writeFileSync(file.path, file.content, "utf-8");
|
|
295
507
|
console.log(
|
|
296
|
-
|
|
508
|
+
` ${colors.green}✔${colors.reset} Created ${colors.bold}${path.relative(process.cwd(), file.path)}${colors.reset}`,
|
|
297
509
|
);
|
|
298
510
|
}
|
|
299
511
|
|
|
300
512
|
// Check and install dependencies
|
|
301
|
-
const dependencies =
|
|
513
|
+
const dependencies = Array.from(allRequiredDeps);
|
|
302
514
|
if (dependencies.length > 0) {
|
|
303
|
-
// Read package.json to see if dependencies are already installed
|
|
304
515
|
let pkgJson = {};
|
|
305
516
|
try {
|
|
306
517
|
pkgJson = JSON.parse(
|
|
@@ -316,8 +527,12 @@ async function main() {
|
|
|
316
527
|
if (missingDeps.length > 0) {
|
|
317
528
|
const pm = detectPackageManager();
|
|
318
529
|
console.log(
|
|
319
|
-
`\n${colors.
|
|
530
|
+
`\n${colors.bold}Installing missing dependencies using ${pm}...${colors.reset}`,
|
|
320
531
|
);
|
|
532
|
+
for (const dep of missingDeps) {
|
|
533
|
+
console.log(` → ${dep}`);
|
|
534
|
+
}
|
|
535
|
+
console.log("");
|
|
321
536
|
|
|
322
537
|
let installCmd = "";
|
|
323
538
|
if (pm === "pnpm") installCmd = `pnpm add ${missingDeps.join(" ")}`;
|
|
@@ -328,26 +543,24 @@ async function main() {
|
|
|
328
543
|
try {
|
|
329
544
|
execSync(installCmd, { stdio: "inherit" });
|
|
330
545
|
console.log(
|
|
331
|
-
|
|
546
|
+
`\n${colors.green}✔ Dependencies installed successfully!${colors.reset}`,
|
|
332
547
|
);
|
|
333
548
|
} catch (_err) {
|
|
334
549
|
console.error(
|
|
335
|
-
|
|
550
|
+
`\n${colors.red}Error: Failed to install dependencies. Please run "${installCmd}" manually.${colors.reset}`,
|
|
336
551
|
);
|
|
337
552
|
}
|
|
338
553
|
} else {
|
|
339
554
|
console.log(
|
|
340
|
-
`\n${colors.green}
|
|
555
|
+
`\n${colors.green}✔ All dependencies (${dependencies.join(", ")}) already installed.${colors.reset}`,
|
|
341
556
|
);
|
|
342
557
|
}
|
|
343
558
|
}
|
|
344
559
|
|
|
345
560
|
console.log(
|
|
346
|
-
`\n${colors.
|
|
347
|
-
);
|
|
348
|
-
console.log(
|
|
349
|
-
`You can now import and use the ${colors.bold}${componentData.className}${colors.reset} component in your project.\n`,
|
|
561
|
+
`\n${colors.bold}${colors.green}✔ Done! All requested components installed successfully.${colors.reset}`,
|
|
350
562
|
);
|
|
563
|
+
console.log(`You can now import and use them in your project.\n`);
|
|
351
564
|
}
|
|
352
565
|
|
|
353
566
|
main().catch((err) => {
|
package/package.json
CHANGED
|
@@ -1,60 +1,62 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "tweenlabs",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
"
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
"
|
|
25
|
-
"
|
|
26
|
-
"
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
"better-auth": "^
|
|
32
|
-
"
|
|
33
|
-
"
|
|
34
|
-
"
|
|
35
|
-
"
|
|
36
|
-
"
|
|
37
|
-
"
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
"@
|
|
43
|
-
"@
|
|
44
|
-
"@types/
|
|
45
|
-
"
|
|
46
|
-
"
|
|
47
|
-
"
|
|
48
|
-
"
|
|
49
|
-
"
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "tweenlabs",
|
|
3
|
+
"version": "0.1.6",
|
|
4
|
+
"description": "Zero-dependency CLI to install premium GSAP animation components directly into your Next.js or React project.",
|
|
5
|
+
"keywords": ["gsap", "animation", "nextjs", "react", "components", "cli", "tweenlabs", "scrolltrigger", "lenis"],
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/TweenLabs/TweenLabs.git"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://tweenlabs.xyz",
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/TweenLabs/TweenLabs/issues"
|
|
14
|
+
},
|
|
15
|
+
"bin": {
|
|
16
|
+
"tweenlabs": "bin/cli.js"
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"bin",
|
|
20
|
+
"README.md",
|
|
21
|
+
"LICENSE"
|
|
22
|
+
],
|
|
23
|
+
"scripts": {
|
|
24
|
+
"dev": "next dev",
|
|
25
|
+
"build": "node scripts/generate-llms-full.js && next build",
|
|
26
|
+
"vercel-build": "convex deploy --cmd \"node scripts/generate-llms-full.js && next build\"",
|
|
27
|
+
"start": "next start",
|
|
28
|
+
"lint": "eslint"
|
|
29
|
+
},
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"@convex-dev/better-auth": "^0.12.4",
|
|
32
|
+
"@gsap/react": "^2.1.2",
|
|
33
|
+
"better-auth": "^1.6.19",
|
|
34
|
+
"convex": "^1.41.0",
|
|
35
|
+
"gsap": "^3.15.0",
|
|
36
|
+
"lenis": "^1.3.23",
|
|
37
|
+
"next": "16.2.7",
|
|
38
|
+
"react": "19.2.4",
|
|
39
|
+
"react-dom": "19.2.4"
|
|
40
|
+
},
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"@biomejs/biome": "^2.5.0",
|
|
43
|
+
"@tailwindcss/postcss": "^4",
|
|
44
|
+
"@types/node": "^20",
|
|
45
|
+
"@types/react": "^19",
|
|
46
|
+
"@types/react-dom": "^19",
|
|
47
|
+
"eslint": "^9",
|
|
48
|
+
"eslint-config-next": "16.2.7",
|
|
49
|
+
"sharp": "^0.35.1",
|
|
50
|
+
"tailwindcss": "^4",
|
|
51
|
+
"typescript": "^5"
|
|
52
|
+
},
|
|
53
|
+
"pnpm": {
|
|
54
|
+
"ignoredBuiltDependencies": [
|
|
55
|
+
"sharp",
|
|
56
|
+
"unrs-resolver"
|
|
57
|
+
],
|
|
58
|
+
"overrides": {
|
|
59
|
+
"postcss": "^8.5.10"
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|