@sarthak_krishak/inkforge 0.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/LICENSE +23 -0
- package/Readme.md +189 -0
- package/cli/index.js +243 -0
- package/package.json +42 -0
- package/src/components/ProgressBar/index.jsx +46 -0
- package/src/components/Spinner/index.jsx +48 -0
- package/src/core/colors.js +25 -0
- package/src/index.jsx +3 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
```
|
|
2
|
+
MIT License
|
|
3
|
+
|
|
4
|
+
Copyright (c) 2026 Sarthak
|
|
5
|
+
|
|
6
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
7
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
8
|
+
in the Software without restriction, including without limitation the rights
|
|
9
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
10
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
11
|
+
furnished to do so, subject to the following conditions:
|
|
12
|
+
|
|
13
|
+
The above copyright notice and this permission notice shall be included in all
|
|
14
|
+
copies or substantial portions of the Software.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
17
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
18
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
19
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
20
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
21
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
22
|
+
SOFTWARE.
|
|
23
|
+
```
|
package/Readme.md
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
# InkForge
|
|
2
|
+
|
|
3
|
+
> **Beautiful, customizable terminal UI components — installed in one command, owned forever.**
|
|
4
|
+
|
|
5
|
+
InkForge is a copy-paste component library for building Terminal UIs with [React](https://react.dev) and [Ink](https://github.com/vadimdemedes/ink). Think of it as **shadcn/ui for your terminal** — instead of installing a locked npm dependency, you get the actual source code dropped into your project. You own it. You can read it, edit it, delete it.
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npx inkforge add spinner
|
|
9
|
+
npx inkforge add progressbar
|
|
10
|
+
npx inkforge add spinner progressbar
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Why InkForge?
|
|
16
|
+
|
|
17
|
+
Modern AI coding tools (Claude Code, Aider, OpenCode) live in the terminal — but building beautiful terminal UIs from scratch is painful. Every developer reinvents spinners, progress bars, and diff viewers from raw ANSI escape codes.
|
|
18
|
+
|
|
19
|
+
InkForge solves this with a simple philosophy:
|
|
20
|
+
|
|
21
|
+
| Principle | What it means |
|
|
22
|
+
|---|---|
|
|
23
|
+
| **Own your code** | `npx inkforge add spinner` drops the actual `.jsx` file into your project — no runtime dependency |
|
|
24
|
+
| **Copy-paste first** | Every component is plain, readable JSX. No magic. No black boxes. |
|
|
25
|
+
| **AI-native by default** | Built for the components AI coding agents actually need |
|
|
26
|
+
| **Terminal-aware** | Designed for ANSI constraints and tested on real terminals |
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## Installation
|
|
31
|
+
|
|
32
|
+
InkForge has no runtime dependency. You use the CLI once to copy components into your project, then the CLI is no longer needed.
|
|
33
|
+
|
|
34
|
+
Your project needs these peer dependencies:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
npm install react ink
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## Usage
|
|
43
|
+
|
|
44
|
+
### Interactive mode (recommended)
|
|
45
|
+
|
|
46
|
+
Run with no arguments to get an interactive component selector:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
npx inkforge add
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Use `↑↓` to move, `space` to select, `enter` to confirm.
|
|
53
|
+
|
|
54
|
+
### Direct mode
|
|
55
|
+
|
|
56
|
+
Add components by name:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
npx inkforge add spinner
|
|
60
|
+
npx inkforge add progressbar
|
|
61
|
+
npx inkforge add spinner progressbar # multiple at once
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### List all components
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
npx inkforge list
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
## Components
|
|
73
|
+
|
|
74
|
+
### `Spinner`
|
|
75
|
+
|
|
76
|
+
Animated loading indicator with 5 variants and theme support.
|
|
77
|
+
|
|
78
|
+
```jsx
|
|
79
|
+
import { Spinner } from './components/inkforge/Spinner';
|
|
80
|
+
|
|
81
|
+
// Basic
|
|
82
|
+
<Spinner label="Loading..." />
|
|
83
|
+
|
|
84
|
+
// Variants: dots | line | bounce | arc | simple
|
|
85
|
+
<Spinner variant="bounce" label="Processing..." />
|
|
86
|
+
|
|
87
|
+
// Themes: default | cyberpunk
|
|
88
|
+
<Spinner variant="dots" theme="cyberpunk" label="Hacking..." />
|
|
89
|
+
|
|
90
|
+
// Done state
|
|
91
|
+
<Spinner label="Deploying..." done={isDone} doneText="✓ Deployed!" />
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
**Props**
|
|
95
|
+
|
|
96
|
+
| Prop | Type | Default | Description |
|
|
97
|
+
|---|---|---|---|
|
|
98
|
+
| `variant` | `string` | `'dots'` | Animation style: `dots`, `line`, `bounce`, `arc`, `simple` |
|
|
99
|
+
| `label` | `string` | `'Loading...'` | Text shown next to spinner |
|
|
100
|
+
| `color` | `string` | theme primary | Override spinner color (hex or named) |
|
|
101
|
+
| `theme` | `string` | `'default'` | Color theme: `default`, `cyberpunk` |
|
|
102
|
+
| `interval` | `number` | `80` | Animation speed in ms |
|
|
103
|
+
| `done` | `boolean` | `false` | Switch to completion state |
|
|
104
|
+
| `doneText` | `string` | `'✓ Done'` | Text shown when `done` is true |
|
|
105
|
+
|
|
106
|
+
---
|
|
107
|
+
|
|
108
|
+
### `ProgressBar`
|
|
109
|
+
|
|
110
|
+
Fillable progress bar with 3 visual variants.
|
|
111
|
+
|
|
112
|
+
```jsx
|
|
113
|
+
import { ProgressBar } from './components/inkforge/ProgressBar';
|
|
114
|
+
|
|
115
|
+
// Basic
|
|
116
|
+
<ProgressBar value={60} label="Build" />
|
|
117
|
+
|
|
118
|
+
// Show raw value instead of percent
|
|
119
|
+
<ProgressBar value={45} total={200} label="Files" showValue />
|
|
120
|
+
|
|
121
|
+
// Variants: default | thin | block
|
|
122
|
+
<ProgressBar value={progress} variant="thin" label="Upload" />
|
|
123
|
+
<ProgressBar value={progress} variant="block" label="Memory" />
|
|
124
|
+
|
|
125
|
+
// Custom color
|
|
126
|
+
<ProgressBar value={progress} color="#E5C07B" label="CPU" />
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
**Props**
|
|
130
|
+
|
|
131
|
+
| Prop | Type | Default | Description |
|
|
132
|
+
|---|---|---|---|
|
|
133
|
+
| `value` | `number` | `0` | Current progress value |
|
|
134
|
+
| `total` | `number` | `100` | Maximum value |
|
|
135
|
+
| `width` | `number` | `30` | Bar width in characters |
|
|
136
|
+
| `label` | `string` | `''` | Label shown before the bar |
|
|
137
|
+
| `showPercent` | `boolean` | `true` | Show `%` on the right |
|
|
138
|
+
| `showValue` | `boolean` | `false` | Show `value/total` instead |
|
|
139
|
+
| `color` | `string` | theme success | Fill color |
|
|
140
|
+
| `bgColor` | `string` | theme muted | Empty bar color |
|
|
141
|
+
| `theme` | `string` | `'default'` | Color theme: `default`, `cyberpunk` |
|
|
142
|
+
| `variant` | `string` | `'default'` | Bar style: `default`, `thin`, `block` |
|
|
143
|
+
|
|
144
|
+
---
|
|
145
|
+
|
|
146
|
+
## Themes
|
|
147
|
+
|
|
148
|
+
InkForge ships with two pre-built themes. Pass `theme` to any component:
|
|
149
|
+
|
|
150
|
+
| Theme | Description | Best for |
|
|
151
|
+
|---|---|---|
|
|
152
|
+
| `default` | Clean, professional, blue accent | Production tools |
|
|
153
|
+
| `cyberpunk` | High contrast, neon cyan/magenta | Dev tools, personal projects |
|
|
154
|
+
|
|
155
|
+
---
|
|
156
|
+
|
|
157
|
+
## Roadmap
|
|
158
|
+
|
|
159
|
+
- [x] Spinner
|
|
160
|
+
- [x] ProgressBar
|
|
161
|
+
- [ ] DiffViewer — AI-native code diff display
|
|
162
|
+
- [ ] StreamingOutput — Token-by-token streaming display
|
|
163
|
+
- [ ] PromptInput — Input with history and autocomplete
|
|
164
|
+
- [ ] Select / MultiSelect — Keyboard-navigable menus
|
|
165
|
+
- [ ] Table — Structured data display
|
|
166
|
+
- [ ] StatusBar — Footer with agent state
|
|
167
|
+
|
|
168
|
+
---
|
|
169
|
+
|
|
170
|
+
## Contributing
|
|
171
|
+
|
|
172
|
+
Contributions are welcome. Open an issue first to discuss what you'd like to add.
|
|
173
|
+
|
|
174
|
+
```bash
|
|
175
|
+
git clone https://github.com/yourusername/inkforge
|
|
176
|
+
cd inkforge
|
|
177
|
+
npm install
|
|
178
|
+
npm run demo # See components in action
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
---
|
|
182
|
+
|
|
183
|
+
## License
|
|
184
|
+
|
|
185
|
+
MIT — see [LICENSE](./LICENSE)
|
|
186
|
+
|
|
187
|
+
---
|
|
188
|
+
|
|
189
|
+
*Built for the era of AI coding agents. Inspired by [shadcn/ui](https://ui.shadcn.com).*
|
package/cli/index.js
ADDED
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// cli/index.js — inkforge CLI
|
|
4
|
+
// Usage: npx inkforge add <component>
|
|
5
|
+
// npx inkforge list
|
|
6
|
+
|
|
7
|
+
import { program } from 'commander';
|
|
8
|
+
import fs from 'fs';
|
|
9
|
+
import path from 'path';
|
|
10
|
+
import { fileURLToPath } from 'url';
|
|
11
|
+
import readline from 'readline';
|
|
12
|
+
|
|
13
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
14
|
+
|
|
15
|
+
// ─── Color helpers (no chalk dependency needed for CLI) ───────────────────────
|
|
16
|
+
const c = {
|
|
17
|
+
cyan: s => `\x1b[36m${s}\x1b[0m`,
|
|
18
|
+
green: s => `\x1b[32m${s}\x1b[0m`,
|
|
19
|
+
yellow: s => `\x1b[33m${s}\x1b[0m`,
|
|
20
|
+
red: s => `\x1b[31m${s}\x1b[0m`,
|
|
21
|
+
bold: s => `\x1b[1m${s}\x1b[0m`,
|
|
22
|
+
dim: s => `\x1b[2m${s}\x1b[0m`,
|
|
23
|
+
reset: s => `\x1b[0m${s}\x1b[0m`,
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
// ─── Component registry ───────────────────────────────────────────────────────
|
|
27
|
+
const REGISTRY = {
|
|
28
|
+
spinner: {
|
|
29
|
+
name: 'Spinner',
|
|
30
|
+
description: 'Animated loading spinner with 5 variants',
|
|
31
|
+
file: 'Spinner/index.jsx',
|
|
32
|
+
dir: 'Spinner',
|
|
33
|
+
usage: "<Spinner variant=\"dots\" label=\"Loading...\" />",
|
|
34
|
+
},
|
|
35
|
+
progressbar: {
|
|
36
|
+
name: 'ProgressBar',
|
|
37
|
+
description: 'Fillable progress bar with 3 variants',
|
|
38
|
+
file: 'ProgressBar/index.jsx',
|
|
39
|
+
dir: 'ProgressBar',
|
|
40
|
+
usage: "<ProgressBar value={60} label=\"Build\" />",
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
45
|
+
function getComponentsDir() {
|
|
46
|
+
const cwd = process.cwd();
|
|
47
|
+
// Try src/components first, then components/
|
|
48
|
+
const candidates = [
|
|
49
|
+
path.join(cwd, 'src', 'components', 'inkforge'),
|
|
50
|
+
path.join(cwd, 'components', 'inkforge'),
|
|
51
|
+
];
|
|
52
|
+
// Return the first parent that exists, defaulting to src/components/inkforge
|
|
53
|
+
if (fs.existsSync(path.join(cwd, 'src'))) return candidates[0];
|
|
54
|
+
return candidates[1];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function getTemplatesDir() {
|
|
58
|
+
return path.join(__dirname, '..', 'src', 'components');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function ensureDir(dirPath) {
|
|
62
|
+
if (!fs.existsSync(dirPath)) {
|
|
63
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function copyComponent(key) {
|
|
68
|
+
const entry = REGISTRY[key];
|
|
69
|
+
const srcDir = getTemplatesDir();
|
|
70
|
+
const destDir = getComponentsDir();
|
|
71
|
+
const srcFile = path.join(srcDir, entry.file);
|
|
72
|
+
const destFolder = path.join(destDir, entry.dir);
|
|
73
|
+
const destFile = path.join(destFolder, 'index.jsx');
|
|
74
|
+
|
|
75
|
+
// Check template exists (guards against broken installs)
|
|
76
|
+
if (!fs.existsSync(srcFile)) {
|
|
77
|
+
console.log(c.red(`✗ Template not found for ${entry.name}. Try reinstalling inkforge.`));
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Warn if already exists
|
|
82
|
+
if (fs.existsSync(destFile)) {
|
|
83
|
+
console.log(c.yellow(`⚠ ${entry.name} already exists at ${path.relative(process.cwd(), destFile)}`));
|
|
84
|
+
console.log(c.dim(' Skipping. Delete the file first if you want a fresh copy.\n'));
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
ensureDir(destFolder);
|
|
89
|
+
|
|
90
|
+
// Read template and rewrite the import path to be relative to the user's project
|
|
91
|
+
let src = fs.readFileSync(srcFile, 'utf8');
|
|
92
|
+
// Replace the internal core path with a sensible relative path
|
|
93
|
+
src = src.replace(
|
|
94
|
+
/from ['"].*?core\/colors\.js['"]/,
|
|
95
|
+
`from '../core/colors.js'`
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
fs.writeFileSync(destFile, src, 'utf8');
|
|
99
|
+
|
|
100
|
+
// Also copy core/colors.js if not present
|
|
101
|
+
ensureCoreColors(destDir);
|
|
102
|
+
|
|
103
|
+
console.log(c.green(`✓ Added ${c.bold(entry.name)}`));
|
|
104
|
+
console.log(c.dim(` → ${path.relative(process.cwd(), destFile)}\n`));
|
|
105
|
+
console.log(c.cyan(' Usage:'));
|
|
106
|
+
console.log(c.dim(` import { ${entry.name} } from './${path.relative(process.cwd(), destFolder).replace(/\\/g, '/')}';`));
|
|
107
|
+
console.log(c.dim(` ${entry.usage}\n`));
|
|
108
|
+
return true;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function ensureCoreColors(destDir) {
|
|
112
|
+
const coreDir = path.join(destDir, 'core');
|
|
113
|
+
const destFile = path.join(coreDir, 'colors.js');
|
|
114
|
+
if (fs.existsSync(destFile)) return;
|
|
115
|
+
|
|
116
|
+
const srcFile = path.join(__dirname, '..', 'src', 'core', 'colors.js');
|
|
117
|
+
if (!fs.existsSync(srcFile)) return;
|
|
118
|
+
|
|
119
|
+
ensureDir(coreDir);
|
|
120
|
+
fs.copyFileSync(srcFile, destFile);
|
|
121
|
+
console.log(c.dim(` → Also copied core/colors.js\n`));
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// ─── Interactive selector ─────────────────────────────────────────────────────
|
|
125
|
+
async function interactiveSelect() {
|
|
126
|
+
const keys = Object.keys(REGISTRY);
|
|
127
|
+
const entries = keys.map(k => REGISTRY[k]);
|
|
128
|
+
let cursor = 0;
|
|
129
|
+
const selected = new Set();
|
|
130
|
+
|
|
131
|
+
// Hide cursor, enable raw mode
|
|
132
|
+
process.stdout.write('\x1b[?25l');
|
|
133
|
+
readline.emitKeypressEvents(process.stdin);
|
|
134
|
+
if (process.stdin.isTTY) process.stdin.setRawMode(true);
|
|
135
|
+
|
|
136
|
+
function render() {
|
|
137
|
+
// Clear previous lines
|
|
138
|
+
process.stdout.write(`\x1b[${entries.length + 4}A\x1b[0J`);
|
|
139
|
+
console.log(c.bold('\n Select components to add:\n'));
|
|
140
|
+
entries.forEach((e, i) => {
|
|
141
|
+
const checked = selected.has(keys[i]) ? c.green('◉') : c.dim('○');
|
|
142
|
+
const active = i === cursor ? c.cyan('▶ ') : ' ';
|
|
143
|
+
const name = i === cursor ? c.bold(c.cyan(e.name)) : e.name;
|
|
144
|
+
const desc = c.dim(e.description);
|
|
145
|
+
console.log(` ${active}${checked} ${name.padEnd(18)}${desc}`);
|
|
146
|
+
});
|
|
147
|
+
console.log(c.dim('\n ↑↓ move space select enter confirm q quit'));
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Initial render — print blank lines first so clear works
|
|
151
|
+
console.log('\n');
|
|
152
|
+
entries.forEach(() => console.log(''));
|
|
153
|
+
console.log('\n');
|
|
154
|
+
render();
|
|
155
|
+
|
|
156
|
+
return new Promise(resolve => {
|
|
157
|
+
process.stdin.on('keypress', (str, key) => {
|
|
158
|
+
if (!key) return;
|
|
159
|
+
|
|
160
|
+
if (key.name === 'up') cursor = (cursor - 1 + keys.length) % keys.length;
|
|
161
|
+
if (key.name === 'down') cursor = (cursor + 1) % keys.length;
|
|
162
|
+
|
|
163
|
+
if (key.name === 'space') {
|
|
164
|
+
const k = keys[cursor];
|
|
165
|
+
if (selected.has(k)) selected.delete(k);
|
|
166
|
+
else selected.add(k);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (key.name === 'return') {
|
|
170
|
+
cleanup();
|
|
171
|
+
resolve([...selected]);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (key.name === 'q' || (key.ctrl && key.name === 'c')) {
|
|
175
|
+
cleanup();
|
|
176
|
+
console.log(c.dim('\n Cancelled.\n'));
|
|
177
|
+
resolve([]);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
render();
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
function cleanup() {
|
|
185
|
+
if (process.stdin.isTTY) process.stdin.setRawMode(false);
|
|
186
|
+
process.stdin.pause();
|
|
187
|
+
process.stdout.write('\x1b[?25h'); // show cursor
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// ─── Commands ─────────────────────────────────────────────────────────────────
|
|
192
|
+
program
|
|
193
|
+
.name('inkforge')
|
|
194
|
+
.description('InkForge — beautiful terminal UI components for React/Ink')
|
|
195
|
+
.version('0.1.0');
|
|
196
|
+
|
|
197
|
+
// inkforge list
|
|
198
|
+
program
|
|
199
|
+
.command('list')
|
|
200
|
+
.description('List all available components')
|
|
201
|
+
.action(() => {
|
|
202
|
+
console.log(c.bold('\n InkForge Components\n'));
|
|
203
|
+
Object.entries(REGISTRY).forEach(([key, entry]) => {
|
|
204
|
+
console.log(` ${c.cyan(entry.name.padEnd(16))} ${c.dim(entry.description)}`);
|
|
205
|
+
console.log(c.dim(` npx inkforge add ${key}\n`));
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
// inkforge add [component...]
|
|
210
|
+
program
|
|
211
|
+
.command('add [components...]')
|
|
212
|
+
.description('Add one or more components to your project')
|
|
213
|
+
.action(async (components) => {
|
|
214
|
+
console.log(c.bold(c.cyan('\n InkForge\n')));
|
|
215
|
+
|
|
216
|
+
// No args — show interactive selector
|
|
217
|
+
if (!components || components.length === 0) {
|
|
218
|
+
const chosen = await interactiveSelect();
|
|
219
|
+
if (chosen.length === 0) return;
|
|
220
|
+
console.log('');
|
|
221
|
+
chosen.forEach(key => copyComponent(key));
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Args provided — add them directly
|
|
226
|
+
let anyFailed = false;
|
|
227
|
+
for (const name of components) {
|
|
228
|
+
const key = name.toLowerCase().replace(/[^a-z]/g, '');
|
|
229
|
+
if (!REGISTRY[key]) {
|
|
230
|
+
console.log(c.red(`✗ Unknown component: "${name}"`));
|
|
231
|
+
console.log(c.dim(` Run ${c.cyan('npx inkforge list')} to see available components.\n`));
|
|
232
|
+
anyFailed = true;
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
copyComponent(key);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (!anyFailed) {
|
|
239
|
+
console.log(c.green(c.bold(' All done! Components are yours to own and edit.\n')));
|
|
240
|
+
}
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
program.parse();
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@sarthak_krishak/inkforge",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Beautiful terminal UI components for React/Ink — shadcn/ui for your terminal",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "src/index.jsx",
|
|
7
|
+
"bin": {
|
|
8
|
+
"inkforge": "./cli/index.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"src/",
|
|
12
|
+
"cli/",
|
|
13
|
+
"README.md",
|
|
14
|
+
"LICENSE"
|
|
15
|
+
],
|
|
16
|
+
"keywords": [
|
|
17
|
+
"terminal",
|
|
18
|
+
"tui",
|
|
19
|
+
"ink",
|
|
20
|
+
"cli",
|
|
21
|
+
"react",
|
|
22
|
+
"spinner",
|
|
23
|
+
"progress",
|
|
24
|
+
"shadcn"
|
|
25
|
+
],
|
|
26
|
+
"author": "Sarthak Krishak",
|
|
27
|
+
"license": "MIT",
|
|
28
|
+
"peerDependencies": {
|
|
29
|
+
"ink": ">=5.0.0",
|
|
30
|
+
"react": ">=18.0.0"
|
|
31
|
+
},
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"commander": "^12.0.0"
|
|
34
|
+
},
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"@vitejs/plugin-react": "^4.3.0",
|
|
37
|
+
"ink": "^5.0.0",
|
|
38
|
+
"react": "^18.3.0",
|
|
39
|
+
"vite": "^5.4.0",
|
|
40
|
+
"vite-node": "^2.1.0"
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Text, Box } from 'ink';
|
|
3
|
+
import { getTheme } from '../../core/colors.js';
|
|
4
|
+
|
|
5
|
+
export function ProgressBar({
|
|
6
|
+
value = 0,
|
|
7
|
+
total = 100,
|
|
8
|
+
width = 30,
|
|
9
|
+
label = '',
|
|
10
|
+
showPercent = true,
|
|
11
|
+
showValue = false,
|
|
12
|
+
color = null,
|
|
13
|
+
bgColor = null,
|
|
14
|
+
theme = 'default',
|
|
15
|
+
variant = 'default',
|
|
16
|
+
}) {
|
|
17
|
+
const palette = getTheme(theme);
|
|
18
|
+
const fillColor = color || palette.success;
|
|
19
|
+
const emptyColor = bgColor || palette.muted;
|
|
20
|
+
|
|
21
|
+
const percent = Math.min(100, Math.max(0, (value / total) * 100));
|
|
22
|
+
const filled = Math.round((percent / 100) * width);
|
|
23
|
+
const empty = width - filled;
|
|
24
|
+
|
|
25
|
+
const chars = {
|
|
26
|
+
default: { fill: '█', empty: '░' },
|
|
27
|
+
thin: { fill: '─', empty: '┄' },
|
|
28
|
+
block: { fill: '■', empty: '□' },
|
|
29
|
+
}[variant] || { fill: '█', empty: '░' };
|
|
30
|
+
|
|
31
|
+
const filledBar = chars.fill.repeat(filled);
|
|
32
|
+
const emptyBar = chars.empty.repeat(empty);
|
|
33
|
+
|
|
34
|
+
let rightLabel = '';
|
|
35
|
+
if (showValue) rightLabel = ` ${value}/${total}`;
|
|
36
|
+
else if (showPercent) rightLabel = ` ${Math.round(percent)}%`;
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<Box>
|
|
40
|
+
{label ? <Text color={palette.text}>{label} </Text> : null}
|
|
41
|
+
<Text color={fillColor}>{filledBar}</Text>
|
|
42
|
+
<Text color={emptyColor}>{emptyBar}</Text>
|
|
43
|
+
<Text color={palette.text}>{rightLabel}</Text>
|
|
44
|
+
</Box>
|
|
45
|
+
);
|
|
46
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import React, { useState, useEffect } from 'react';
|
|
2
|
+
import { Text, Box } from 'ink';
|
|
3
|
+
import { getTheme } from '../../core/colors.js';
|
|
4
|
+
|
|
5
|
+
const VARIANTS = {
|
|
6
|
+
dots: ['⠋','⠙','⠹','⠸','⠼','⠴','⠦','⠧','⠇','⠏'],
|
|
7
|
+
line: ['—','\\','|','/'],
|
|
8
|
+
bounce: ['⣾','⣽','⣻','⢿','⡿','⣟','⣯','⣷'],
|
|
9
|
+
arc: ['◜','◠','◝','◞','◡','◟'],
|
|
10
|
+
simple: ['. ','.. ','...',' '],
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export function Spinner({
|
|
14
|
+
variant = 'dots',
|
|
15
|
+
label = 'Loading...',
|
|
16
|
+
color = null,
|
|
17
|
+
theme = 'default',
|
|
18
|
+
interval = 120,
|
|
19
|
+
done = false,
|
|
20
|
+
doneText = '✓ Done',
|
|
21
|
+
}) {
|
|
22
|
+
const [frame, setFrame] = useState(0);
|
|
23
|
+
const frames = VARIANTS[variant] || VARIANTS.dots;
|
|
24
|
+
const palette = getTheme(theme);
|
|
25
|
+
const spinnerColor = color || palette.primary;
|
|
26
|
+
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
if (done) return;
|
|
29
|
+
// Use a ref-tracked interval so it doesn't chain re-renders
|
|
30
|
+
let f = 0;
|
|
31
|
+
const timer = setInterval(() => {
|
|
32
|
+
f = (f + 1) % frames.length;
|
|
33
|
+
setFrame(f);
|
|
34
|
+
}, interval);
|
|
35
|
+
return () => clearInterval(timer);
|
|
36
|
+
}, [done, variant, interval]); // only re-subscribe if these change
|
|
37
|
+
|
|
38
|
+
if (done) {
|
|
39
|
+
return <Box><Text color={palette.success}>{doneText}</Text></Box>;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<Box>
|
|
44
|
+
<Text color={spinnerColor}>{frames[frame]} </Text>
|
|
45
|
+
<Text color={palette.text}>{label}</Text>
|
|
46
|
+
</Box>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// src/core/colors.js
|
|
2
|
+
const themes = {
|
|
3
|
+
default: {
|
|
4
|
+
primary: '#61AFEF',
|
|
5
|
+
success: '#98C379',
|
|
6
|
+
warning: '#E5C07B',
|
|
7
|
+
error: '#E06C75',
|
|
8
|
+
muted: '#5C6370',
|
|
9
|
+
text: '#ABB2BF',
|
|
10
|
+
bright: '#FFFFFF',
|
|
11
|
+
},
|
|
12
|
+
cyberpunk: {
|
|
13
|
+
primary: '#00FFFF',
|
|
14
|
+
success: '#00FF88',
|
|
15
|
+
warning: '#FFD700',
|
|
16
|
+
error: '#FF5555',
|
|
17
|
+
muted: '#444466',
|
|
18
|
+
text: '#CCCCFF',
|
|
19
|
+
bright: '#FFFFFF',
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export function getTheme(name = 'default') {
|
|
24
|
+
return themes[name] || themes.default;
|
|
25
|
+
}
|
package/src/index.jsx
ADDED