alexui 1.0.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/README.md +57 -0
- package/components/ActionTable.tsx +307 -0
- package/components/AlertBanner.tsx +124 -0
- package/components/AnimatedAccordion.tsx +95 -0
- package/components/Autocomplete.tsx +144 -0
- package/components/Avatar.tsx +123 -0
- package/components/Badge.tsx +80 -0
- package/components/Breadcrumb.tsx +74 -0
- package/components/Calendar.tsx +340 -0
- package/components/Card3D.tsx +117 -0
- package/components/Carousel3D.tsx +193 -0
- package/components/CascadeSelect.tsx +232 -0
- package/components/ChartShowcase.tsx +700 -0
- package/components/Checkbox.tsx +212 -0
- package/components/ChipsInput.tsx +152 -0
- package/components/CircularKnob.tsx +240 -0
- package/components/CodeVisualizer.tsx +67 -0
- package/components/Collapsible.tsx +72 -0
- package/components/ColorThemeManager.tsx +458 -0
- package/components/CommandMenu.tsx +191 -0
- package/components/ConfirmDialog.tsx +152 -0
- package/components/ContextMenu.tsx +192 -0
- package/components/DashboardLayout.tsx +115 -0
- package/components/DatePicker.tsx +108 -0
- package/components/Divider.tsx +67 -0
- package/components/Dock.tsx +93 -0
- package/components/DragDropLists.tsx +160 -0
- package/components/Drawer.tsx +161 -0
- package/components/DropdownPlus.tsx +304 -0
- package/components/EmptyState.tsx +49 -0
- package/components/ErrorPage.tsx +62 -0
- package/components/FileDropzone.tsx +206 -0
- package/components/ForgotPassword.tsx +137 -0
- package/components/FormField.tsx +81 -0
- package/components/GlassButton.tsx +56 -0
- package/components/GlassCard.tsx +82 -0
- package/components/GlassInput.tsx +96 -0
- package/components/GlassmorphicModal.tsx +108 -0
- package/components/GlowInput.tsx +111 -0
- package/components/GlowSelect.tsx +203 -0
- package/components/GlowTextArea.tsx +105 -0
- package/components/HorizontalTimeline.tsx +121 -0
- package/components/HoverCard.tsx +105 -0
- package/components/ImageLightbox.tsx +259 -0
- package/components/InputGroup.tsx +118 -0
- package/components/InputOTP.tsx +147 -0
- package/components/InteractiveNavbar.tsx +266 -0
- package/components/InteractiveSidebar.tsx +211 -0
- package/components/Kbd.tsx +51 -0
- package/components/LiteYouTube.tsx +118 -0
- package/components/LoaderCollection.tsx +368 -0
- package/components/LoginForm.tsx +192 -0
- package/components/MagneticButton.tsx +101 -0
- package/components/MaskedInput.tsx +79 -0
- package/components/MentionInput.tsx +413 -0
- package/components/MorphingSwitch.tsx +86 -0
- package/components/MultiSelect.tsx +158 -0
- package/components/NumberInput.tsx +203 -0
- package/components/Panel.tsx +104 -0
- package/components/PasswordInput.tsx +203 -0
- package/components/Popover.tsx +91 -0
- package/components/PricingTable.tsx +113 -0
- package/components/ProgressBar.tsx +152 -0
- package/components/RadioButton.tsx +211 -0
- package/components/Rating.tsx +82 -0
- package/components/ResizablePanel.tsx +114 -0
- package/components/ScrollPanel.tsx +103 -0
- package/components/SettingsPage.tsx +154 -0
- package/components/SignupForm.tsx +182 -0
- package/components/Skeleton.tsx +41 -0
- package/components/Slider.tsx +95 -0
- package/components/SlidingTabs.tsx +54 -0
- package/components/SortableList.tsx +91 -0
- package/components/SpeedDial.tsx +134 -0
- package/components/Spinner.tsx +40 -0
- package/components/Stepper.tsx +124 -0
- package/components/TabMenu.tsx +72 -0
- package/components/TableControls.tsx +77 -0
- package/components/TablePagination.tsx +88 -0
- package/components/TextEditor.tsx +329 -0
- package/components/TextReveal.tsx +99 -0
- package/components/ThemeSwitcher.tsx +133 -0
- package/components/TimelineGSAP.tsx +164 -0
- package/components/ToastSystem.tsx +110 -0
- package/components/ToggleButton.tsx +79 -0
- package/components/Tooltip.tsx +121 -0
- package/components/Tree.tsx +138 -0
- package/dist/commands/add.d.ts +7 -0
- package/dist/commands/add.js +110 -0
- package/dist/commands/init.d.ts +5 -0
- package/dist/commands/init.js +76 -0
- package/dist/commands/list.d.ts +1 -0
- package/dist/commands/list.js +32 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +60 -0
- package/dist/registry.d.ts +6 -0
- package/dist/registry.js +38 -0
- package/dist/tui/browse.d.ts +3 -0
- package/dist/tui/browse.js +139 -0
- package/dist/tui/format.d.ts +11 -0
- package/dist/tui/format.js +52 -0
- package/dist/tui/main.d.ts +1 -0
- package/dist/tui/main.js +86 -0
- package/dist/tui/panels.d.ts +9 -0
- package/dist/tui/panels.js +50 -0
- package/dist/tui/theme.d.ts +28 -0
- package/dist/tui/theme.js +76 -0
- package/dist/types.d.ts +28 -0
- package/dist/types.js +1 -0
- package/dist/utils/config.d.ts +6 -0
- package/dist/utils/config.js +24 -0
- package/dist/utils/copy.d.ts +9 -0
- package/dist/utils/copy.js +43 -0
- package/dist/utils/cwd.d.ts +6 -0
- package/dist/utils/cwd.js +30 -0
- package/dist/utils/deps.d.ts +1 -0
- package/dist/utils/deps.js +19 -0
- package/dist/utils/project.d.ts +5 -0
- package/dist/utils/project.js +30 -0
- package/dist/utils/theme.d.ts +1 -0
- package/dist/utils/theme.js +24 -0
- package/package.json +52 -0
- package/registry.json +1133 -0
- package/templates/theme.css +81 -0
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import * as p from '@clack/prompts';
|
|
2
|
+
import pc from 'picocolors';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { getRegistry, resolveComponentTree } from '../registry.js';
|
|
5
|
+
import { configExists, readConfig } from '../utils/config.js';
|
|
6
|
+
import { collectNpmDependencies, copyComponents, getMissingDependencies } from '../utils/copy.js';
|
|
7
|
+
import { installPackages } from '../utils/deps.js';
|
|
8
|
+
import { runInit } from './init.js';
|
|
9
|
+
const USAGE_HINTS = {
|
|
10
|
+
toastsystem: 'Envolvé tu app en <ToastProvider> (ver ToastSystem.tsx).',
|
|
11
|
+
colorthememanager: 'Requiere las variables CSS de tema en tu index.css.',
|
|
12
|
+
themeswitcher: 'Asegurate de tener la clase .dark en <html> para modo oscuro.',
|
|
13
|
+
};
|
|
14
|
+
function cancel(message, embed) {
|
|
15
|
+
if (embed)
|
|
16
|
+
return;
|
|
17
|
+
p.cancel(message);
|
|
18
|
+
process.exit(0);
|
|
19
|
+
}
|
|
20
|
+
export async function runAdd(componentArgs, cwd = process.cwd(), options) {
|
|
21
|
+
const embed = options?.embed ?? false;
|
|
22
|
+
if (!configExists(cwd)) {
|
|
23
|
+
p.log.warn('No existe alexui.json. Ejecutá alexui init primero.');
|
|
24
|
+
const shouldInit = await p.confirm({ message: '¿Querés inicializar ahora?', initialValue: true });
|
|
25
|
+
if (p.isCancel(shouldInit) || !shouldInit) {
|
|
26
|
+
cancel('Operación cancelada.', embed);
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
await runInit(cwd, { embed: true });
|
|
30
|
+
}
|
|
31
|
+
const config = readConfig(cwd);
|
|
32
|
+
const registry = getRegistry();
|
|
33
|
+
let selectedIds = componentArgs.map((id) => id.toLowerCase());
|
|
34
|
+
if (selectedIds.length === 0) {
|
|
35
|
+
const picked = await p.multiselect({
|
|
36
|
+
message: 'Seleccioná componentes para agregar',
|
|
37
|
+
options: registry.components.map((c) => ({
|
|
38
|
+
value: c.id,
|
|
39
|
+
label: c.title,
|
|
40
|
+
hint: `${c.group} · ${c.category}`,
|
|
41
|
+
})),
|
|
42
|
+
required: true,
|
|
43
|
+
});
|
|
44
|
+
if (p.isCancel(picked)) {
|
|
45
|
+
cancel('Operación cancelada.', embed);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
selectedIds = picked;
|
|
49
|
+
}
|
|
50
|
+
const unknown = selectedIds.filter((id) => !registry.components.some((c) => c.id === id));
|
|
51
|
+
if (unknown.length > 0) {
|
|
52
|
+
p.log.error(`Componentes no encontrados: ${unknown.join(', ')}`);
|
|
53
|
+
p.log.info(`Usá "alexui list" para ver los ${registry.componentCount} disponibles.`);
|
|
54
|
+
if (!embed)
|
|
55
|
+
process.exit(1);
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
const tree = resolveComponentTree(selectedIds);
|
|
59
|
+
const targetDir = path.join(cwd, config.componentDir);
|
|
60
|
+
const internalExtras = tree.filter((c) => !selectedIds.includes(c.id));
|
|
61
|
+
if (!embed) {
|
|
62
|
+
p.intro(pc.bgCyan(pc.black(` Agregando ${selectedIds.length} componente(s) `)));
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
p.log.step(pc.cyan(`Agregando ${selectedIds.map((id) => pc.white(id)).join(pc.dim(', '))}`));
|
|
66
|
+
}
|
|
67
|
+
if (internalExtras.length > 0) {
|
|
68
|
+
p.log.info(`Dependencias internas: ${internalExtras.map((c) => pc.dim(c.id)).join(', ')}`);
|
|
69
|
+
}
|
|
70
|
+
const overwrite = options?.overwrite ?? false;
|
|
71
|
+
const spinner = p.spinner();
|
|
72
|
+
spinner.start('Copiando archivos…');
|
|
73
|
+
const result = copyComponents(tree, targetDir, overwrite);
|
|
74
|
+
spinner.stop(overwrite ? 'Archivos actualizados' : 'Archivos copiados');
|
|
75
|
+
for (const file of result.created) {
|
|
76
|
+
p.log.success(`${pc.dim(config.componentDir + '/')} ${pc.white(file)}`);
|
|
77
|
+
}
|
|
78
|
+
for (const file of result.skipped) {
|
|
79
|
+
p.log.warn(`Omitido ${pc.dim(file)} ${pc.dim('(ya existe)')}`);
|
|
80
|
+
}
|
|
81
|
+
const npmDeps = collectNpmDependencies(tree);
|
|
82
|
+
const missing = getMissingDependencies(npmDeps, cwd);
|
|
83
|
+
if (missing.length > 0) {
|
|
84
|
+
p.log.step(`Dependencias npm: ${missing.map((d) => pc.cyan(d)).join(', ')}`);
|
|
85
|
+
const shouldInstall = options?.yes ?? await p.confirm({
|
|
86
|
+
message: '¿Instalar dependencias faltantes?',
|
|
87
|
+
initialValue: true,
|
|
88
|
+
});
|
|
89
|
+
if (!p.isCancel(shouldInstall) && shouldInstall) {
|
|
90
|
+
const installSpinner = p.spinner();
|
|
91
|
+
installSpinner.start('Instalando paquetes…');
|
|
92
|
+
const ok = installPackages(missing, cwd);
|
|
93
|
+
installSpinner.stop(ok ? 'Dependencias instaladas' : 'Error al instalar');
|
|
94
|
+
if (!ok)
|
|
95
|
+
p.log.warn('Instalalas manualmente: ' + missing.join(' '));
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
const hints = selectedIds
|
|
99
|
+
.map((id) => USAGE_HINTS[id])
|
|
100
|
+
.filter(Boolean);
|
|
101
|
+
if (hints.length > 0) {
|
|
102
|
+
p.note(hints.join('\n'), 'Notas de uso');
|
|
103
|
+
}
|
|
104
|
+
if (embed) {
|
|
105
|
+
p.log.success(`${tree.length} archivo(s) en ${pc.cyan(config.componentDir)}`);
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
p.outro(pc.green(`¡Listo! ${tree.length} archivo(s) en ${config.componentDir}`));
|
|
109
|
+
}
|
|
110
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import * as p from '@clack/prompts';
|
|
2
|
+
import pc from 'picocolors';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { DEFAULT_CONFIG, writeConfig } from '../utils/config.js';
|
|
5
|
+
import { hasReactProject, ensureDir } from '../utils/project.js';
|
|
6
|
+
import { applyThemeCss } from '../utils/theme.js';
|
|
7
|
+
function cancel(message, embed) {
|
|
8
|
+
if (embed)
|
|
9
|
+
return;
|
|
10
|
+
p.cancel(message);
|
|
11
|
+
process.exit(0);
|
|
12
|
+
}
|
|
13
|
+
export async function runInit(cwd = process.cwd(), options) {
|
|
14
|
+
const embed = options?.embed ?? false;
|
|
15
|
+
if (!embed) {
|
|
16
|
+
p.intro(pc.bgCyan(pc.black(' AlexUI Init ')));
|
|
17
|
+
}
|
|
18
|
+
else {
|
|
19
|
+
p.log.step(pc.cyan('Inicializando proyecto…'));
|
|
20
|
+
}
|
|
21
|
+
if (!hasReactProject(cwd)) {
|
|
22
|
+
p.log.warn('No se detectó React en package.json. Necesitarás React + Tailwind.');
|
|
23
|
+
}
|
|
24
|
+
const componentDir = await p.text({
|
|
25
|
+
message: '¿Dónde guardar los componentes?',
|
|
26
|
+
placeholder: DEFAULT_CONFIG.componentDir,
|
|
27
|
+
defaultValue: DEFAULT_CONFIG.componentDir,
|
|
28
|
+
});
|
|
29
|
+
if (p.isCancel(componentDir)) {
|
|
30
|
+
cancel('Inicialización cancelada.', embed);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
const cssPath = await p.text({
|
|
34
|
+
message: '¿Ruta del CSS principal (Tailwind)?',
|
|
35
|
+
placeholder: DEFAULT_CONFIG.tailwind.css,
|
|
36
|
+
defaultValue: DEFAULT_CONFIG.tailwind.css,
|
|
37
|
+
});
|
|
38
|
+
if (p.isCancel(cssPath)) {
|
|
39
|
+
cancel('Inicialización cancelada.', embed);
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
const setupTheme = await p.confirm({
|
|
43
|
+
message: '¿Agregar variables de tema AlexUI al CSS?',
|
|
44
|
+
initialValue: true,
|
|
45
|
+
});
|
|
46
|
+
if (p.isCancel(setupTheme)) {
|
|
47
|
+
cancel('Inicialización cancelada.', embed);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
const config = {
|
|
51
|
+
...DEFAULT_CONFIG,
|
|
52
|
+
componentDir: String(componentDir),
|
|
53
|
+
tailwind: {
|
|
54
|
+
...DEFAULT_CONFIG.tailwind,
|
|
55
|
+
css: String(cssPath),
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
const spinner = p.spinner();
|
|
59
|
+
spinner.start('Escribiendo configuración…');
|
|
60
|
+
writeConfig(config, cwd);
|
|
61
|
+
ensureDir(path.join(cwd, config.componentDir));
|
|
62
|
+
if (setupTheme) {
|
|
63
|
+
const result = applyThemeCss(config.tailwind.css, cwd);
|
|
64
|
+
spinner.stop(result === 'created' ? 'CSS y tema creados'
|
|
65
|
+
: result === 'merged' ? 'Tema fusionado al CSS'
|
|
66
|
+
: 'Configuración guardada');
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
spinner.stop('Configuración guardada');
|
|
70
|
+
}
|
|
71
|
+
p.log.success(`alexui.json ${pc.dim('→')} ${pc.cyan(config.componentDir)}`);
|
|
72
|
+
if (!embed) {
|
|
73
|
+
p.note(`alexui --cwd ./mi-app\nalexui add dock\nalexui list`, 'Próximos pasos');
|
|
74
|
+
p.outro(pc.green('¡Proyecto listo para AlexUI!'));
|
|
75
|
+
}
|
|
76
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function runList(filter?: string): void;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import pc from 'picocolors';
|
|
2
|
+
import { getRegistry } from '../registry.js';
|
|
3
|
+
export function runList(filter) {
|
|
4
|
+
const registry = getRegistry();
|
|
5
|
+
let items = registry.components;
|
|
6
|
+
if (filter) {
|
|
7
|
+
const q = filter.toLowerCase();
|
|
8
|
+
items = items.filter((c) => c.id.includes(q) ||
|
|
9
|
+
c.title.toLowerCase().includes(q) ||
|
|
10
|
+
c.category.includes(q) ||
|
|
11
|
+
c.group.includes(q));
|
|
12
|
+
}
|
|
13
|
+
console.log(pc.bold(`\nAlexUI ${registry.version} — ${items.length} componentes\n`));
|
|
14
|
+
const grouped = new Map();
|
|
15
|
+
for (const comp of items) {
|
|
16
|
+
const key = `${comp.group} / ${comp.category}`;
|
|
17
|
+
if (!grouped.has(key))
|
|
18
|
+
grouped.set(key, []);
|
|
19
|
+
grouped.get(key).push(comp);
|
|
20
|
+
}
|
|
21
|
+
for (const [group, comps] of [...grouped.entries()].sort()) {
|
|
22
|
+
console.log(pc.cyan(group));
|
|
23
|
+
for (const comp of comps) {
|
|
24
|
+
const deps = comp.npmDependencies.length > 0 ? pc.dim(` [${comp.npmDependencies.join(', ')}]`) : '';
|
|
25
|
+
const internal = comp.internalDependencies.length > 0
|
|
26
|
+
? pc.dim(` +${comp.internalDependencies.join(', ')}`)
|
|
27
|
+
: '';
|
|
28
|
+
console.log(` ${pc.green(comp.id.padEnd(22))} ${comp.title}${deps}${internal}`);
|
|
29
|
+
}
|
|
30
|
+
console.log('');
|
|
31
|
+
}
|
|
32
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from 'commander';
|
|
3
|
+
import { runAdd } from './commands/add.js';
|
|
4
|
+
import { runInit } from './commands/init.js';
|
|
5
|
+
import { runList } from './commands/list.js';
|
|
6
|
+
import { getRegistry } from './registry.js';
|
|
7
|
+
import { runTui } from './tui/main.js';
|
|
8
|
+
import { parseCwdFromArgs, resolveProjectDir } from './utils/cwd.js';
|
|
9
|
+
const registry = getRegistry();
|
|
10
|
+
const { cwd: cwdArg, rest: args } = parseCwdFromArgs(process.argv.slice(2));
|
|
11
|
+
let projectDir;
|
|
12
|
+
try {
|
|
13
|
+
projectDir = resolveProjectDir(cwdArg);
|
|
14
|
+
}
|
|
15
|
+
catch (error) {
|
|
16
|
+
console.error(error instanceof Error ? error.message : error);
|
|
17
|
+
process.exit(1);
|
|
18
|
+
}
|
|
19
|
+
const program = new Command();
|
|
20
|
+
program
|
|
21
|
+
.name('alexui')
|
|
22
|
+
.description('CLI — AlexUI by Alexis Jardin: componentes glassmorphic para React')
|
|
23
|
+
.version(registry.version)
|
|
24
|
+
.option('-C, --cwd <path>', 'Directorio del proyecto destino (por defecto: carpeta actual)');
|
|
25
|
+
program
|
|
26
|
+
.command('tui')
|
|
27
|
+
.description('Abrir el modo interactivo (TUI)')
|
|
28
|
+
.action(async () => {
|
|
29
|
+
await runTui(projectDir);
|
|
30
|
+
});
|
|
31
|
+
program
|
|
32
|
+
.command('init')
|
|
33
|
+
.description('Inicializar AlexUI en el proyecto actual')
|
|
34
|
+
.action(async () => {
|
|
35
|
+
await runInit(projectDir);
|
|
36
|
+
});
|
|
37
|
+
program
|
|
38
|
+
.command('add')
|
|
39
|
+
.description('Agregar uno o más componentes')
|
|
40
|
+
.argument('[components...]', 'IDs de componentes (ej: dock badge loginform)')
|
|
41
|
+
.option('-y, --yes', 'Instalar dependencias sin preguntar')
|
|
42
|
+
.option('--overwrite', 'Sobrescribir archivos existentes')
|
|
43
|
+
.action(async (components, options) => {
|
|
44
|
+
await runAdd(components, projectDir, options);
|
|
45
|
+
});
|
|
46
|
+
program
|
|
47
|
+
.command('list')
|
|
48
|
+
.description('Listar componentes disponibles')
|
|
49
|
+
.argument('[filter]', 'Filtrar por id, título o categoría')
|
|
50
|
+
.action((filter) => {
|
|
51
|
+
runList(filter);
|
|
52
|
+
});
|
|
53
|
+
const knownCommands = new Set(['tui', 'init', 'add', 'list']);
|
|
54
|
+
const isCommandMode = args.some((arg) => knownCommands.has(arg) || arg === '--help' || arg === '-h' || arg === '--version' || arg === '-V');
|
|
55
|
+
if (!isCommandMode) {
|
|
56
|
+
await runTui(projectDir);
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
program.parse(['node', 'alexui', ...args], { from: 'user' });
|
|
60
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { RegistryComponent, RegistryManifest } from './types.js';
|
|
2
|
+
export declare function getRegistry(): RegistryManifest;
|
|
3
|
+
export declare function getComponentsDir(): string;
|
|
4
|
+
export declare function getTemplatesDir(): string;
|
|
5
|
+
export declare function findComponent(id: string): RegistryComponent | undefined;
|
|
6
|
+
export declare function resolveComponentTree(ids: string[]): RegistryComponent[];
|
package/dist/registry.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
const pkgRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
5
|
+
let cached = null;
|
|
6
|
+
export function getRegistry() {
|
|
7
|
+
if (!cached) {
|
|
8
|
+
const raw = fs.readFileSync(path.join(pkgRoot, 'registry.json'), 'utf8');
|
|
9
|
+
cached = JSON.parse(raw);
|
|
10
|
+
}
|
|
11
|
+
return cached;
|
|
12
|
+
}
|
|
13
|
+
export function getComponentsDir() {
|
|
14
|
+
return path.join(pkgRoot, 'components');
|
|
15
|
+
}
|
|
16
|
+
export function getTemplatesDir() {
|
|
17
|
+
return path.join(pkgRoot, 'templates');
|
|
18
|
+
}
|
|
19
|
+
export function findComponent(id) {
|
|
20
|
+
const registry = getRegistry();
|
|
21
|
+
return registry.components.find((c) => c.id === id.toLowerCase());
|
|
22
|
+
}
|
|
23
|
+
export function resolveComponentTree(ids) {
|
|
24
|
+
const registry = getRegistry();
|
|
25
|
+
const byId = new Map(registry.components.map((c) => [c.id, c]));
|
|
26
|
+
const resolved = new Map();
|
|
27
|
+
const visit = (id) => {
|
|
28
|
+
const comp = byId.get(id.toLowerCase());
|
|
29
|
+
if (!comp || resolved.has(comp.id))
|
|
30
|
+
return;
|
|
31
|
+
for (const dep of comp.internalDependencies)
|
|
32
|
+
visit(dep);
|
|
33
|
+
resolved.set(comp.id, comp);
|
|
34
|
+
};
|
|
35
|
+
for (const id of ids)
|
|
36
|
+
visit(id);
|
|
37
|
+
return [...resolved.values()];
|
|
38
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import * as p from '@clack/prompts';
|
|
2
|
+
import pc from 'picocolors';
|
|
3
|
+
import { findComponent, getRegistry } from '../registry.js';
|
|
4
|
+
import { runAdd } from '../commands/add.js';
|
|
5
|
+
import { categoryLabel, componentSelectOptions, formatCatalogGroup, formatComponentDetail, formatComponentDetailTitle, formatSearchResults, } from './format.js';
|
|
6
|
+
import { categoryIcon, icon } from './theme.js';
|
|
7
|
+
import { sectionHeader } from './panels.js';
|
|
8
|
+
function filterComponents(filter) {
|
|
9
|
+
const { components } = getRegistry();
|
|
10
|
+
if (filter.type === 'all')
|
|
11
|
+
return components;
|
|
12
|
+
if (filter.type === 'group')
|
|
13
|
+
return components.filter((c) => c.group === filter.group);
|
|
14
|
+
return components.filter((c) => c.category === filter.category);
|
|
15
|
+
}
|
|
16
|
+
function filterLabel(filter) {
|
|
17
|
+
if (filter.type === 'all')
|
|
18
|
+
return 'Todos';
|
|
19
|
+
if (filter.type === 'group')
|
|
20
|
+
return filter.group === 'completos' ? 'Completos' : 'Individuales';
|
|
21
|
+
return categoryLabel(filter.category);
|
|
22
|
+
}
|
|
23
|
+
async function pickBrowseFilter() {
|
|
24
|
+
sectionHeader('Explorar catálogo');
|
|
25
|
+
const filterType = await p.select({
|
|
26
|
+
message: 'Filtrar por',
|
|
27
|
+
options: [
|
|
28
|
+
{ value: 'all', label: `${icon.list} Todos`, hint: `${getRegistry().componentCount} componentes` },
|
|
29
|
+
{ value: 'completos', label: `${icon.complete} Completos`, hint: 'templates y compuestos' },
|
|
30
|
+
{ value: 'individuales', label: `${icon.atomic} Individuales`, hint: 'átomos y utilidades' },
|
|
31
|
+
{ value: 'category', label: `${icon.browse} Por categoría`, hint: 'entradas, tablas…' },
|
|
32
|
+
],
|
|
33
|
+
});
|
|
34
|
+
if (p.isCancel(filterType))
|
|
35
|
+
return null;
|
|
36
|
+
if (filterType === 'all')
|
|
37
|
+
return { type: 'all' };
|
|
38
|
+
if (filterType === 'completos')
|
|
39
|
+
return { type: 'group', group: 'completos' };
|
|
40
|
+
if (filterType === 'individuales')
|
|
41
|
+
return { type: 'group', group: 'individuales' };
|
|
42
|
+
const categories = [...new Set(getRegistry().components.map((c) => c.category))].sort();
|
|
43
|
+
const category = await p.select({
|
|
44
|
+
message: 'Categoría',
|
|
45
|
+
options: categories.map((cat) => ({
|
|
46
|
+
value: cat,
|
|
47
|
+
label: `${categoryIcon(cat)} ${categoryLabel(cat)}`,
|
|
48
|
+
hint: `${getRegistry().components.filter((c) => c.category === cat).length}`,
|
|
49
|
+
})),
|
|
50
|
+
});
|
|
51
|
+
if (p.isCancel(category))
|
|
52
|
+
return null;
|
|
53
|
+
return { type: 'category', category: String(category) };
|
|
54
|
+
}
|
|
55
|
+
async function showComponentDetail(comp, cwd) {
|
|
56
|
+
p.note(formatComponentDetail(comp), formatComponentDetailTitle(comp));
|
|
57
|
+
const action = await p.select({
|
|
58
|
+
message: comp.title,
|
|
59
|
+
options: [
|
|
60
|
+
{ value: 'add', label: `${icon.add} Agregar al proyecto`, hint: comp.file },
|
|
61
|
+
{ value: 'back', label: `${icon.back} Volver a la lista` },
|
|
62
|
+
{ value: 'menu', label: `${icon.exit} Menú principal` },
|
|
63
|
+
],
|
|
64
|
+
});
|
|
65
|
+
if (p.isCancel(action) || action === 'back')
|
|
66
|
+
return 'back';
|
|
67
|
+
if (action === 'menu')
|
|
68
|
+
return 'menu';
|
|
69
|
+
await runAdd([comp.id], cwd, { embed: true });
|
|
70
|
+
return 'back';
|
|
71
|
+
}
|
|
72
|
+
async function browseList(components, cwd, filterName) {
|
|
73
|
+
if (components.length === 0) {
|
|
74
|
+
p.log.warn('No hay componentes con ese filtro.');
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
p.log.info(pc.dim(`${filterName} · ${components.length} resultados`));
|
|
78
|
+
while (true) {
|
|
79
|
+
const picked = await p.select({
|
|
80
|
+
message: `Componentes`,
|
|
81
|
+
options: [
|
|
82
|
+
...componentSelectOptions(components),
|
|
83
|
+
{ value: '__back__', label: pc.dim(`${icon.back} Volver al menú`) },
|
|
84
|
+
],
|
|
85
|
+
});
|
|
86
|
+
if (p.isCancel(picked) || picked === '__back__')
|
|
87
|
+
return;
|
|
88
|
+
const comp = findComponent(String(picked));
|
|
89
|
+
if (!comp)
|
|
90
|
+
continue;
|
|
91
|
+
const next = await showComponentDetail(comp, cwd);
|
|
92
|
+
if (next === 'menu')
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
export async function browseCatalog(cwd) {
|
|
97
|
+
const filter = await pickBrowseFilter();
|
|
98
|
+
if (!filter)
|
|
99
|
+
return;
|
|
100
|
+
await browseList(filterComponents(filter), cwd, filterLabel(filter));
|
|
101
|
+
}
|
|
102
|
+
export async function searchComponents(cwd) {
|
|
103
|
+
sectionHeader('Buscar');
|
|
104
|
+
const query = await p.text({
|
|
105
|
+
message: 'Término de búsqueda',
|
|
106
|
+
placeholder: 'dock, badge, tabla, login…',
|
|
107
|
+
});
|
|
108
|
+
if (p.isCancel(query) || !String(query).trim())
|
|
109
|
+
return;
|
|
110
|
+
const q = String(query).toLowerCase();
|
|
111
|
+
const results = getRegistry().components.filter((c) => c.id.includes(q) ||
|
|
112
|
+
c.title.toLowerCase().includes(q) ||
|
|
113
|
+
c.category.includes(q) ||
|
|
114
|
+
c.group.includes(q));
|
|
115
|
+
console.log(formatSearchResults(String(query), results.length));
|
|
116
|
+
if (results.length === 0)
|
|
117
|
+
return;
|
|
118
|
+
if (results.length === 1) {
|
|
119
|
+
await showComponentDetail(results[0], cwd);
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
await browseList(results, cwd, `Búsqueda: "${query}"`);
|
|
123
|
+
}
|
|
124
|
+
export async function listCatalogInTui() {
|
|
125
|
+
sectionHeader('Catálogo completo');
|
|
126
|
+
const registry = getRegistry();
|
|
127
|
+
const grouped = new Map();
|
|
128
|
+
for (const comp of registry.components) {
|
|
129
|
+
const key = `${comp.group} / ${categoryLabel(comp.category)}`;
|
|
130
|
+
if (!grouped.has(key))
|
|
131
|
+
grouped.set(key, []);
|
|
132
|
+
grouped.get(key).push(`${categoryIcon(comp.category)} ${comp.id} — ${comp.title}`);
|
|
133
|
+
}
|
|
134
|
+
const body = [...grouped.entries()]
|
|
135
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
136
|
+
.map(([group, lines]) => formatCatalogGroup(group, lines))
|
|
137
|
+
.join('\n\n');
|
|
138
|
+
p.note(body, `${icon.list} ${registry.componentCount} componentes`);
|
|
139
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { RegistryComponent } from '../types.js';
|
|
2
|
+
export declare function categoryLabel(category: string): string;
|
|
3
|
+
export declare function formatComponentDetail(comp: RegistryComponent): string;
|
|
4
|
+
export declare function formatComponentDetailTitle(comp: RegistryComponent): string;
|
|
5
|
+
export declare function componentSelectOptions(components: RegistryComponent[]): {
|
|
6
|
+
value: string;
|
|
7
|
+
label: string;
|
|
8
|
+
hint: string;
|
|
9
|
+
}[];
|
|
10
|
+
export declare function formatCatalogGroup(group: string, lines: string[]): string;
|
|
11
|
+
export declare function formatSearchResults(query: string, count: number): string;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import pc from 'picocolors';
|
|
2
|
+
import { categoryIcon, depPills, groupBadge, icon } from './theme.js';
|
|
3
|
+
const CATEGORY_LABELS = {
|
|
4
|
+
entradas: 'Entradas',
|
|
5
|
+
estructura: 'Estructura',
|
|
6
|
+
feedback: 'Feedback',
|
|
7
|
+
multimedia: 'Multimedia',
|
|
8
|
+
animaciones: 'Animaciones',
|
|
9
|
+
tablas: 'Tablas',
|
|
10
|
+
complejos: 'Complejos',
|
|
11
|
+
};
|
|
12
|
+
export function categoryLabel(category) {
|
|
13
|
+
return CATEGORY_LABELS[category] ?? category;
|
|
14
|
+
}
|
|
15
|
+
export function formatComponentDetail(comp) {
|
|
16
|
+
const cat = categoryLabel(comp.category);
|
|
17
|
+
const lines = [
|
|
18
|
+
groupBadge(comp.group) + ' ' + pc.dim(`${categoryIcon(comp.category)} ${cat}`),
|
|
19
|
+
'',
|
|
20
|
+
pc.bold(pc.white(comp.title)),
|
|
21
|
+
pc.dim(` ${comp.id}`) + pc.dim(' · ') + pc.dim(comp.file),
|
|
22
|
+
'',
|
|
23
|
+
pc.dim(' ') + comp.description,
|
|
24
|
+
'',
|
|
25
|
+
pc.dim(' ') + pc.bold('Dependencias npm'),
|
|
26
|
+
' ' + depPills(comp.npmDependencies, 'npm'),
|
|
27
|
+
];
|
|
28
|
+
if (comp.internalDependencies.length > 0) {
|
|
29
|
+
lines.push('', pc.dim(' ') + pc.bold('Se copian automáticamente'), ' ' + depPills(comp.internalDependencies, 'internal'));
|
|
30
|
+
}
|
|
31
|
+
return lines.join('\n');
|
|
32
|
+
}
|
|
33
|
+
export function formatComponentDetailTitle(comp) {
|
|
34
|
+
return `${icon.browse} ${comp.id}`;
|
|
35
|
+
}
|
|
36
|
+
export function componentSelectOptions(components) {
|
|
37
|
+
return components.map((c) => ({
|
|
38
|
+
value: c.id,
|
|
39
|
+
label: `${categoryIcon(c.category)} ${c.title}`,
|
|
40
|
+
hint: `${c.group === 'completos' ? 'completo' : 'atómico'} · ${categoryLabel(c.category)}`,
|
|
41
|
+
}));
|
|
42
|
+
}
|
|
43
|
+
export function formatCatalogGroup(group, lines) {
|
|
44
|
+
return `${pc.magenta(icon.list)} ${pc.bold(pc.cyan(group))}\n${lines.map((l) => ` ${pc.white(l)}`).join('\n')}`;
|
|
45
|
+
}
|
|
46
|
+
export function formatSearchResults(query, count) {
|
|
47
|
+
if (count === 0)
|
|
48
|
+
return pc.yellow(` Sin resultados para "${query}"`);
|
|
49
|
+
if (count === 1)
|
|
50
|
+
return pc.green(` 1 coincidencia para "${query}"`);
|
|
51
|
+
return pc.green(` ${count} coincidencias para "${query}"`);
|
|
52
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function runTui(initialCwd?: string): Promise<void>;
|
package/dist/tui/main.js
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import * as p from '@clack/prompts';
|
|
2
|
+
import pc from 'picocolors';
|
|
3
|
+
import { getRegistry } from '../registry.js';
|
|
4
|
+
import { runAdd } from '../commands/add.js';
|
|
5
|
+
import { runInit } from '../commands/init.js';
|
|
6
|
+
import { resolveProjectDir } from '../utils/cwd.js';
|
|
7
|
+
import { browseCatalog, listCatalogInTui, searchComponents } from './browse.js';
|
|
8
|
+
import { categoryLabel } from './format.js';
|
|
9
|
+
import { menuOptions, printProjectPanel, printWelcome, sectionHeader } from './panels.js';
|
|
10
|
+
import { icon } from './theme.js';
|
|
11
|
+
async function changeProjectDir(current) {
|
|
12
|
+
sectionHeader('Cambiar proyecto');
|
|
13
|
+
const next = await p.text({
|
|
14
|
+
message: 'Ruta absoluta o relativa del proyecto',
|
|
15
|
+
placeholder: current,
|
|
16
|
+
defaultValue: current,
|
|
17
|
+
validate: (value) => {
|
|
18
|
+
if (!value?.trim())
|
|
19
|
+
return 'Ingresá una ruta';
|
|
20
|
+
try {
|
|
21
|
+
resolveProjectDir(value);
|
|
22
|
+
return undefined;
|
|
23
|
+
}
|
|
24
|
+
catch (error) {
|
|
25
|
+
return error instanceof Error ? error.message : 'Ruta inválida';
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
if (p.isCancel(next))
|
|
30
|
+
return current;
|
|
31
|
+
return resolveProjectDir(String(next));
|
|
32
|
+
}
|
|
33
|
+
async function quickAdd(cwd) {
|
|
34
|
+
sectionHeader('Agregar componentes');
|
|
35
|
+
const registry = getRegistry();
|
|
36
|
+
const picked = await p.multiselect({
|
|
37
|
+
message: 'Seleccioná uno o más',
|
|
38
|
+
options: registry.components.map((c) => ({
|
|
39
|
+
value: c.id,
|
|
40
|
+
label: c.title,
|
|
41
|
+
hint: `${c.group === 'completos' ? 'completo' : 'atómico'} · ${categoryLabel(c.category)}`,
|
|
42
|
+
})),
|
|
43
|
+
required: true,
|
|
44
|
+
});
|
|
45
|
+
if (p.isCancel(picked))
|
|
46
|
+
return;
|
|
47
|
+
await runAdd(picked, cwd, { embed: true });
|
|
48
|
+
}
|
|
49
|
+
export async function runTui(initialCwd = process.cwd()) {
|
|
50
|
+
let cwd = resolveProjectDir(initialCwd);
|
|
51
|
+
printWelcome(cwd);
|
|
52
|
+
while (true) {
|
|
53
|
+
const action = await p.select({
|
|
54
|
+
message: pc.bold('Menú principal'),
|
|
55
|
+
options: menuOptions(cwd),
|
|
56
|
+
});
|
|
57
|
+
if (p.isCancel(action) || action === 'exit')
|
|
58
|
+
break;
|
|
59
|
+
console.log('');
|
|
60
|
+
switch (action) {
|
|
61
|
+
case 'init':
|
|
62
|
+
await runInit(cwd, { embed: true });
|
|
63
|
+
printProjectPanel(cwd, `${icon.ok} Proyecto actualizado`);
|
|
64
|
+
break;
|
|
65
|
+
case 'add':
|
|
66
|
+
await quickAdd(cwd);
|
|
67
|
+
break;
|
|
68
|
+
case 'browse':
|
|
69
|
+
await browseCatalog(cwd);
|
|
70
|
+
break;
|
|
71
|
+
case 'search':
|
|
72
|
+
await searchComponents(cwd);
|
|
73
|
+
break;
|
|
74
|
+
case 'list':
|
|
75
|
+
await listCatalogInTui();
|
|
76
|
+
break;
|
|
77
|
+
case 'chdir':
|
|
78
|
+
cwd = await changeProjectDir(cwd);
|
|
79
|
+
printProjectPanel(cwd, `${icon.chdir} Nuevo destino`);
|
|
80
|
+
break;
|
|
81
|
+
}
|
|
82
|
+
console.log('');
|
|
83
|
+
}
|
|
84
|
+
const registry = getRegistry();
|
|
85
|
+
p.outro(pc.magenta(icon.logo) + pc.dim(' AlexUI') + pc.green(' · ¡Hasta luego!') + pc.dim(` v${registry.version}`));
|
|
86
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export declare function renderProjectPanel(cwd: string): string;
|
|
2
|
+
export declare function printWelcome(cwd: string): void;
|
|
3
|
+
export declare function printProjectPanel(cwd: string, title?: string): void;
|
|
4
|
+
export declare function menuOptions(cwd: string): {
|
|
5
|
+
value: string;
|
|
6
|
+
label: string;
|
|
7
|
+
hint: string;
|
|
8
|
+
}[];
|
|
9
|
+
export declare function sectionHeader(title: string): void;
|