@axzydev/axzy_ui_system 1.3.6 → 1.4.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.
@@ -0,0 +1,181 @@
1
+ import { useState } from "react";
2
+ import { FaCheck, FaExternalLinkAlt } from "react-icons/fa";
3
+ import ITBadget from "../components/atoms/badget/badget";
4
+ import ITDivider from "../components/atoms/divider/divider";
5
+ import ITFlex from "../components/atoms/flex/flex";
6
+ import ITGrid from "../components/atoms/grid/grid";
7
+ import ITStack from "../components/atoms/stack/stack";
8
+ import ITText from "../components/atoms/text/text";
9
+ import ITDialog from "../components/organisms/dialog/dialog";
10
+ import ProductoIcono from "./ProductoIcono";
11
+ import type { Producto } from "./productos";
12
+
13
+ interface Props {
14
+ /** Producto que se muestra. */
15
+ producto: Producto;
16
+ /** Cierra la pantalla de detalle. */
17
+ onClose: () => void;
18
+ }
19
+
20
+ /**
21
+ * Pantalla de detalle de un producto del portafolio.
22
+ *
23
+ * Se abre a pantalla completa sobre la home: a la izquierda la galería de
24
+ * capturas reales (con miniaturas y pie), a la derecha la ficha del producto
25
+ * (qué resuelve, stack y datos técnicos).
26
+ *
27
+ * @example
28
+ * ```tsx
29
+ * {detalle && <ProductoDetalle producto={detalle} onClose={() => setDetalle(null)} />}
30
+ * ```
31
+ */
32
+ export default function ProductoDetalle({ producto, onClose }: Props) {
33
+ const capturas = producto.capturas ?? [];
34
+ const [activa, setActiva] = useState(capturas[0]);
35
+ const hayGaleria = capturas.length > 0;
36
+
37
+ return (
38
+ <ITDialog isOpen onClose={onClose} title={producto.nombre} useFormHeader fullScreen>
39
+ <div className="mx-auto w-full max-w-6xl">
40
+ <ITGrid container spacing={8}>
41
+ {/* ─── Galería ─── */}
42
+ {hayGaleria && activa && (
43
+ <ITGrid item xs={12} lg={7}>
44
+ <ITStack spacing={3}>
45
+ <div className="overflow-hidden rounded-xl border border-slate-200 dark:border-slate-800 bg-slate-50 dark:bg-slate-950/60">
46
+ <img
47
+ src={activa.src}
48
+ alt={`${producto.nombre}: ${activa.titulo}`}
49
+ className="block w-full"
50
+ />
51
+ </div>
52
+
53
+ <ITFlex gap={2} wrap="wrap">
54
+ {capturas.map((captura) => (
55
+ <button
56
+ key={captura.src}
57
+ type="button"
58
+ title={captura.titulo}
59
+ aria-label={captura.titulo}
60
+ onClick={() => setActiva(captura)}
61
+ className={`h-14 w-20 shrink-0 overflow-hidden rounded-lg border-2 bg-slate-100 dark:bg-slate-950 transition-all ${
62
+ activa.src === captura.src
63
+ ? "border-primary-500 opacity-100"
64
+ : "border-transparent opacity-55 hover:opacity-100"
65
+ }`}
66
+ >
67
+ <img src={captura.src} alt="" className="h-full w-full object-cover object-top" />
68
+ </button>
69
+ ))}
70
+ </ITFlex>
71
+
72
+ <ITText as="p" muted className="!text-xs">
73
+ {activa.titulo}
74
+ </ITText>
75
+ </ITStack>
76
+ </ITGrid>
77
+ )}
78
+
79
+ {/* ─── Ficha ─── */}
80
+ <ITGrid item xs={12} lg={hayGaleria ? 5 : 12}>
81
+ <ITStack spacing={5}>
82
+ <ITFlex align="center" justify="between" gap={3}>
83
+ <ITFlex align="center" gap={3}>
84
+ <div className={`w-12 h-12 rounded-xl bg-gradient-to-br ${producto.gradiente} text-white flex items-center justify-center shadow-md shrink-0`}>
85
+ <ProductoIcono icono={producto.icono} size={20} />
86
+ </div>
87
+ <ITText as="h2" className="!text-xl !font-bold">
88
+ {producto.nombre}
89
+ </ITText>
90
+ </ITFlex>
91
+ <ITBadget label={producto.tipo} color="primary" variant="outlined" />
92
+ </ITFlex>
93
+
94
+ {producto.detalle && (
95
+ <ITText as="p" muted className="!text-sm !leading-relaxed">
96
+ {producto.detalle}
97
+ </ITText>
98
+ )}
99
+
100
+ {!!producto.caracteristicas?.length && (
101
+ <div>
102
+ <ITText as="h3" className="!text-sm !font-bold !mb-2.5">
103
+ Qué resuelve
104
+ </ITText>
105
+ <ITStack spacing={2}>
106
+ {producto.caracteristicas.map((caracteristica) => (
107
+ <ITFlex key={caracteristica} align="start" gap={2}>
108
+ <FaCheck size={11} className="mt-1 shrink-0 text-emerald-500" />
109
+ <ITText as="span" muted className="!text-xs !leading-relaxed">
110
+ {caracteristica}
111
+ </ITText>
112
+ </ITFlex>
113
+ ))}
114
+ </ITStack>
115
+ </div>
116
+ )}
117
+
118
+ <div>
119
+ <ITText as="h3" className="!text-sm !font-bold !mb-2.5">
120
+ Stack
121
+ </ITText>
122
+ <ITFlex gap={2} wrap="wrap">
123
+ {producto.stack.map((tecnologia) => (
124
+ <ITBadget key={tecnologia} label={tecnologia} color="gray" variant="outlined" />
125
+ ))}
126
+ </ITFlex>
127
+ </div>
128
+
129
+ {!!producto.ficha?.length && (
130
+ <>
131
+ <ITDivider />
132
+
133
+ <div>
134
+ <ITText as="h3" className="!text-sm !font-bold !mb-2.5">
135
+ Ficha técnica
136
+ </ITText>
137
+ <ITStack spacing={2.5}>
138
+ {producto.ficha.map((dato) => (
139
+ <ITGrid container spacing={2} key={dato.etiqueta}>
140
+ <ITGrid item xs={4}>
141
+ <ITText as="span" muted className="!text-xs !font-semibold">
142
+ {dato.etiqueta}
143
+ </ITText>
144
+ </ITGrid>
145
+ <ITGrid item xs={8}>
146
+ <ITText as="span" className="!text-xs !leading-relaxed">
147
+ {dato.valor}
148
+ </ITText>
149
+ </ITGrid>
150
+ </ITGrid>
151
+ ))}
152
+ </ITStack>
153
+ </div>
154
+ </>
155
+ )}
156
+
157
+ {producto.enlace && (
158
+ <a
159
+ href={producto.enlace}
160
+ {...(producto.enlace.startsWith("#")
161
+ ? {}
162
+ : { target: "_blank", rel: "noopener noreferrer" })}
163
+ className="!text-xs !font-semibold !text-primary-600 dark:!text-primary-400 inline-flex items-center gap-1.5"
164
+ >
165
+ {!producto.enlace.startsWith("#") && <FaExternalLinkAlt size={11} />}
166
+ {producto.enlaceTexto ?? "Ver el repositorio"}
167
+ </a>
168
+ )}
169
+
170
+ {producto.nota && (
171
+ <ITText as="p" muted className="!text-[11px] !leading-relaxed">
172
+ {producto.nota}
173
+ </ITText>
174
+ )}
175
+ </ITStack>
176
+ </ITGrid>
177
+ </ITGrid>
178
+ </div>
179
+ </ITDialog>
180
+ );
181
+ }
@@ -0,0 +1,25 @@
1
+ import { FaCheck, FaDesktop, FaLayerGroup, FaTerminal } from "react-icons/fa";
2
+ import type { Producto } from "./productos";
3
+
4
+ /**
5
+ * Ícono de un producto del portafolio.
6
+ *
7
+ * @example
8
+ * ```tsx
9
+ * <ProductoIcono icono="terminal" size={16} />
10
+ * ```
11
+ */
12
+ export default function ProductoIcono({
13
+ icono,
14
+ size = 18,
15
+ }: {
16
+ /** Ícono declarado en los datos del producto. */
17
+ icono: Producto["icono"];
18
+ /** Tamaño en px. @default 18 */
19
+ size?: number;
20
+ }) {
21
+ if (icono === "terminal") return <FaTerminal size={size} />;
22
+ if (icono === "layers") return <FaLayerGroup size={size} />;
23
+ if (icono === "check") return <FaCheck size={size} />;
24
+ return <FaDesktop size={size} />;
25
+ }
@@ -0,0 +1,179 @@
1
+ /**
2
+ * Productos propios que se muestran en «Mis Productos» del portafolio
3
+ * (`HomeShowcase`), con su pantalla de detalle (`ProductoDetalle`).
4
+ *
5
+ * La tarjeta muestra solo lo esencial (portada, nombre, una descripción corta y
6
+ * el stack); lo demás vive en el detalle. Los productos con `capturas` abren
7
+ * esa pantalla y las imágenes salen de `public/productos/`, tomadas de las apps
8
+ * reales (ver `scripts/capturas/README.md`).
9
+ */
10
+
11
+ /** Una pantalla del producto, con el texto que la describe. */
12
+ export interface CapturaProducto {
13
+ /** Ruta pública de la imagen. */
14
+ src: string;
15
+ /** Qué muestra esa pantalla (se usa como pie en la pantalla de detalle). */
16
+ titulo: string;
17
+ }
18
+
19
+ /** Fila de la ficha técnica: etiqueta y valor. */
20
+ export interface DatoTecnico {
21
+ /** Nombre del dato (p. ej. «Plataformas»). */
22
+ etiqueta: string;
23
+ /** Valor del dato. */
24
+ valor: string;
25
+ }
26
+
27
+ /** Producto del portafolio. */
28
+ export interface Producto {
29
+ /** Identificador estable (clave de React y ancla). */
30
+ id: string;
31
+ /** Nombre del producto. */
32
+ nombre: string;
33
+ /** Etiqueta corta: tipo de producto. */
34
+ tipo: string;
35
+ /** Descripción corta, para la tarjeta. */
36
+ descripcion: string;
37
+ /** Descripción larga, para la pantalla de detalle. */
38
+ detalle?: string;
39
+ /** Qué resuelve, en puntos (solo en el detalle). */
40
+ caracteristicas?: string[];
41
+ /** Tecnologías principales (se muestran en la tarjeta). */
42
+ stack: string[];
43
+ /** Datos técnicos, solo en el detalle. */
44
+ ficha?: DatoTecnico[];
45
+ /** Enlace público. Se omite en productos cuyo repositorio no es público. */
46
+ enlace?: string;
47
+ /** Texto del enlace. */
48
+ enlaceTexto?: string;
49
+ /** Nota al pie del detalle (p. ej. de dónde salen las capturas). */
50
+ nota?: string;
51
+ /** Clases Tailwind del degradado (literales, para que Tailwind las vea). */
52
+ gradiente: string;
53
+ /** Ícono del producto. */
54
+ icono: "desktop" | "terminal" | "layers" | "check";
55
+ /** Capturas de la app real. Sin capturas no hay pantalla de detalle. */
56
+ capturas?: CapturaProducto[];
57
+ }
58
+
59
+ export const PRODUCTOS: Producto[] = [
60
+ {
61
+ id: "axzy-ui-system",
62
+ nombre: "AXZY UI System",
63
+ tipo: "Librería",
64
+ descripcion:
65
+ "Librería de componentes React con Tailwind CSS v4. Más de 50 componentes enterprise con theming en runtime, TypeScript nativo y accesibilidad.",
66
+ stack: ["React", "TypeScript", "Tailwind CSS v4", "npm"],
67
+ enlace: "#ui-system",
68
+ enlaceTexto: "Ver proyecto",
69
+ gradiente: "from-primary-500 to-purple-500",
70
+ icono: "layers",
71
+ },
72
+ {
73
+ id: "checkapp",
74
+ nombre: "CheckApp",
75
+ tipo: "App web",
76
+ descripcion:
77
+ "Panel administrativo para la operación de seguridad: clientes, guardias, puntos de control con QR, rondas, incidencias y mantenimiento, con analítica de desempeño.",
78
+ detalle:
79
+ "Cubre la operación diaria de una empresa de seguridad: alta de clientes con sus ubicaciones, puntos de control con QR —se imprimen en lote por cliente y zona—, asignación de guardias, rutas programadas y supervisión de las rondas. Cada recorrido queda registrado con su cronología, y sobre esos datos se calcula la analítica de desempeño por rango de fechas: eventos e incidencias, puntos escaneados, rondas incompletas y puntos omitidos. Suma kardex, mantenimiento, incidencias y la configuración del sistema.",
80
+ caracteristicas: [
81
+ "Directorio de clientes con sus ubicaciones, contactos y estados (activos e inactivos).",
82
+ "Puntos de control con QR: impresión masiva filtrando por cliente y zona.",
83
+ "Supervisión de rondas: filtros por cliente, guardia y fecha, con la cronología de cada recorrido y su estado.",
84
+ "Analítica de seguridad: eventos/incidencias, puntos escaneados, rondas incompletas y puntos omitidos.",
85
+ "Módulos de guardias, rutas programadas, incidencias, kardex, mantenimiento, usuarios y configuración.",
86
+ ],
87
+ stack: ["Node.js", "React", "Deploy"],
88
+ ficha: [
89
+ { etiqueta: "Módulos", valor: "Inicio, clientes, guardias, ubicaciones, rutas, rondas, incidencias, kardex, mantenimiento, programación, usuarios y sistema" },
90
+ { etiqueta: "Puntos de control", valor: "Cada ubicación tiene su QR, con impresión masiva por cliente y zona" },
91
+ { etiqueta: "Supervisión", valor: "Historial de rondas con cronología por guardia y estado (activas / historial)" },
92
+ { etiqueta: "Analítica", valor: "Eventos, puntos escaneados, rondas incompletas y puntos omitidos por rango de fechas" },
93
+ { etiqueta: "Acceso", valor: "Login con sesión y usuario administrador" },
94
+ { etiqueta: "Estado", valor: "En línea en checkapp.axzy.dev" },
95
+ ],
96
+ nota: "Capturas del panel en línea: analítica de seguridad, historial de rondas e impresión de QRs.",
97
+ enlace: "https://checkapp.axzy.dev/",
98
+ enlaceTexto: "Abrir CheckApp",
99
+ gradiente: "from-emerald-500 to-cyan-500",
100
+ icono: "check",
101
+ capturas: [
102
+ { src: "/productos/checkapp-analitica.webp", titulo: "Analítica de seguridad: indicadores y gráficas por rango de fechas" },
103
+ { src: "/productos/checkapp-rondas.webp", titulo: "Historial de rondas: cronología, estado y control por guardia" },
104
+ { src: "/productos/checkapp-qrs.webp", titulo: "Impresión masiva de QRs de los puntos de control" },
105
+ ],
106
+ },
107
+ {
108
+ id: "agente-puerto-nuevo",
109
+ nombre: "Agente Puerto Nuevo",
110
+ tipo: "App de escritorio",
111
+ descripcion:
112
+ "App de escritorio que instala, actualiza y respalda Puerto Nuevo con clics: revisa Git y Docker, arma el .env y vigila el sistema desde la bandeja.",
113
+ detalle:
114
+ "Acompaña al sistema Puerto Nuevo en la computadora del cliente. En la primera ejecución revisa que estén Git, Docker, Docker Compose y curl (y WSL en Windows), avisa qué descargar si falta algo, baja el repositorio, genera el .env con las claves y corre la instalación mostrando el avance paso a paso. Después queda en la bandeja del sistema: si el API deja de responder avisa, y si el equipo arrancó con la sesión lo reinicia solo. Está pensada para gente que no usa la terminal.",
115
+ caracteristicas: [
116
+ "Asistente de 4 pasos: herramientas, descarga, configuración e instalación.",
117
+ "Panel con actualizar, abrir la web, direcciones de red con QR y espacio en disco.",
118
+ "Bandeja del sistema con el estado en vivo (/api/v1/health cada 15 s) y arranque con la sesión.",
119
+ "Respaldos de la base de datos y paquete de soporte en un .zip.",
120
+ "Instaladores .exe, .dmg y .AppImage; los pasos viven en los scripts del repo, así que un git pull los actualiza sin reinstalar la app.",
121
+ ],
122
+ stack: ["Electron", "React", "TypeScript", "Tailwind CSS v4", "AXZY UI System"],
123
+ ficha: [
124
+ { etiqueta: "Plataformas", valor: "Windows, macOS y Linux" },
125
+ { etiqueta: "Instaladores", valor: ".exe (NSIS x64), .dmg (arm64 y x64) y .AppImage (x64)" },
126
+ { etiqueta: "Versión", valor: "1.1.0" },
127
+ { etiqueta: "Base", valor: "Electron + React 19 + Tailwind CSS v4" },
128
+ { etiqueta: "Interfaz", valor: "Componentes de AXZY UI System" },
129
+ { etiqueta: "Monitoreo", valor: "Consulta /api/v1/health cada 15 s desde la bandeja" },
130
+ { etiqueta: "Lógica", valor: "Scripts del repo (actualizar.sh, respaldar.sh, operar.sh): un git pull actualiza los pasos" },
131
+ { etiqueta: "Alcance", valor: "Instalación, actualización, respaldos, relojes checadores y soporte" },
132
+ ],
133
+ nota: "Capturas de la aplicación en ejecución: el asistente de instalación, el panel de inicio y los ajustes.",
134
+ gradiente: "from-primary-500 to-cyan-500",
135
+ icono: "desktop",
136
+ capturas: [
137
+ { src: "/productos/agente-asistente.webp", titulo: "Asistente: revisa las herramientas y dice qué falta" },
138
+ { src: "/productos/agente-inicio.webp", titulo: "Inicio: actualizar, abrir la web y direcciones de red" },
139
+ { src: "/productos/agente-ajustes.webp", titulo: "Ajustes: herramientas, configuración y soporte" },
140
+ ],
141
+ },
142
+ {
143
+ id: "axzy-http-cli",
144
+ nombre: "axzy",
145
+ tipo: "CLI + TUI",
146
+ descripcion:
147
+ "Cliente HTTP para la terminal: una alternativa a Postman que guarda los requests como YAML versionable. Un binario en Rust con TUI y comandos para CI.",
148
+ detalle:
149
+ "Pensado para vivir en la terminal, al lado del código. Cada request es un archivo YAML dentro de .http/, así que se versiona en git y se revisa como cualquier otro archivo; la TUI de tres paneles sirve para explorar, editar y ejecutar, y los mismos comandos funcionan en CI: si un assert falla, el proceso termina con exit code 1. Es un único binario en Rust, sin runtime ni dependencias extra.",
150
+ caracteristicas: [
151
+ "Requests en YAML dentro de .http/, uno por archivo y organizados en carpetas: se revisan en git como cualquier código.",
152
+ "Ambientes con variables {{var}} y secretos guardados en el keychain del sistema.",
153
+ "Asserts, capturas con JSONPath y runner de carpetas; exit code 1 si algo falla, ideal para CI.",
154
+ "Import desde Swagger/OpenAPI (incluso desde la página de Swagger UI), Postman, Insomnia y curl.",
155
+ "TUI de tres paneles (Collections · Request · Response) con navegación estilo vim y 9 temas.",
156
+ ],
157
+ stack: ["Rust", "ratatui", "crossterm", "reqwest", "GitHub Actions"],
158
+ ficha: [
159
+ { etiqueta: "Binario", valor: "Único, en Rust (rust-version 1.88)" },
160
+ { etiqueta: "Instalación", valor: "curl | sh, PowerShell o cargo install" },
161
+ { etiqueta: "Requests", valor: "YAML en .http/, uno por archivo" },
162
+ { etiqueta: "Ambientes", valor: "Variables {{var}} y secretos en el keychain del sistema" },
163
+ { etiqueta: "TUI", valor: "ratatui + crossterm: 3 paneles, comandos estilo vim y 9 temas" },
164
+ { etiqueta: "CI", valor: "Runner de carpetas con exit code 1 si un assert falla" },
165
+ { etiqueta: "Import", valor: "Swagger/OpenAPI, Postman, Insomnia y curl" },
166
+ { etiqueta: "Licencia", valor: "MIT" },
167
+ ],
168
+ nota: "Capturas de la TUI y la CLI reales, ejecutando un request contra una API local.",
169
+ enlace: "https://github.com/maag070208/AXZY_HTTP_CLIENT_TERMINAL",
170
+ enlaceTexto: "Ver el repositorio",
171
+ gradiente: "from-purple-500 to-fuchsia-500",
172
+ icono: "terminal",
173
+ capturas: [
174
+ { src: "/productos/axzy-tui.webp", titulo: "TUI: colección, request y respuesta JSON en tres paneles" },
175
+ { src: "/productos/axzy-auth.webp", titulo: "Auth y respuesta en la misma pantalla" },
176
+ { src: "/productos/axzy-cli.webp", titulo: "CLI: corre el request y reporta cada assert" },
177
+ ],
178
+ },
179
+ ];
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Local SVG icons for the portfolio tech grids.
3
+ *
4
+ * Every icon lives in `public/tech-icons/` (fetched from the Devicon set) so
5
+ * the page never depends on third-party CDNs at runtime. Reference icons
6
+ * through {@link techIcon} instead of hardcoding URLs, so a missing asset is a
7
+ * type error rather than a broken image.
8
+ *
9
+ * @example
10
+ * ```tsx
11
+ * const icon = techIcon("typescript");
12
+ * <img src={icon.src} alt="TypeScript" className={icon.mono ? "dark:invert" : ""} />
13
+ * ```
14
+ */
15
+
16
+ /** A tech icon: its public URL plus presentation hints. */
17
+ export interface TechIconEntry {
18
+ /** Public path of the SVG asset. */
19
+ src: string;
20
+ /**
21
+ * True for monochrome marks (GitHub, Express). The showcase inverts them in
22
+ * dark mode, where a black glyph would disappear on the dark card.
23
+ */
24
+ mono?: boolean;
25
+ }
26
+
27
+ const ICONS = {
28
+ // Languages
29
+ typescript: { src: "/tech-icons/typescript.svg" },
30
+ javascript: { src: "/tech-icons/javascript.svg" },
31
+ csharp: { src: "/tech-icons/csharp.svg" },
32
+ python: { src: "/tech-icons/python.svg" },
33
+ kotlin: { src: "/tech-icons/kotlin.svg" },
34
+ dart: { src: "/tech-icons/dart.svg" },
35
+ // Frameworks & runtimes
36
+ angular: { src: "/tech-icons/angular.svg" },
37
+ react: { src: "/tech-icons/react.svg" },
38
+ ionic: { src: "/tech-icons/ionic.svg" },
39
+ dotnet: { src: "/tech-icons/dotnet.svg" },
40
+ nodejs: { src: "/tech-icons/nodejs.svg" },
41
+ flutter: { src: "/tech-icons/flutter.svg" },
42
+ express: { src: "/tech-icons/express.svg", mono: true },
43
+ electron: { src: "/tech-icons/electron.svg" },
44
+ redux: { src: "/tech-icons/redux.svg" },
45
+ // Markup, styles & data
46
+ html5: { src: "/tech-icons/html5.svg" },
47
+ css3: { src: "/tech-icons/css3.svg" },
48
+ sass: { src: "/tech-icons/sass.svg" },
49
+ mongodb: { src: "/tech-icons/mongodb.svg" },
50
+ mysql: { src: "/tech-icons/mysql.svg" },
51
+ // Mobile & platform
52
+ android: { src: "/tech-icons/android.svg" },
53
+ // Tooling & DevOps
54
+ docker: { src: "/tech-icons/docker.svg" },
55
+ git: { src: "/tech-icons/git.svg" },
56
+ github: { src: "/tech-icons/github.svg", mono: true },
57
+ gitlab: { src: "/tech-icons/gitlab.svg" },
58
+ npm: { src: "/tech-icons/npm.svg" },
59
+ visualstudio: { src: "/tech-icons/visualstudio.svg" },
60
+ jest: { src: "/tech-icons/jest.svg" },
61
+ } as const satisfies Record<string, TechIconEntry>;
62
+
63
+ /** Name of an available local tech icon. */
64
+ export type TechIconName = keyof typeof ICONS;
65
+
66
+ /** Resolves a tech icon name to its local asset descriptor. */
67
+ export const techIcon = (name: TechIconName): TechIconEntry => ICONS[name];