@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 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
@@ -0,0 +1,3 @@
1
+ // src/index.jsx
2
+ export { Spinner } from './components/Spinner/index.jsx';
3
+ export { ProgressBar } from './components/ProgressBar/index.jsx';