Componentes

This commit is contained in:
jorgemike
2025-03-22 18:08:10 -05:00
parent 3d5f888dcb
commit 8ab8d6d771
43 changed files with 2281 additions and 458 deletions
+307 -1
View File
@@ -12,19 +12,33 @@
"bootstrap-icons": "^1.11.3",
"next": "15.2.3",
"react": "^19.0.0",
"react-dom": "^19.0.0"
"react-dom": "^19.0.0",
"react-syntax-highlighter": "^15.6.1"
},
"devDependencies": {
"@eslint/eslintrc": "^3",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"@types/react-syntax-highlighter": "^15.5.13",
"eslint": "^9",
"eslint-config-next": "15.2.3",
"sass": "^1.86.0",
"typescript": "^5"
}
},
"node_modules/@babel/runtime": {
"version": "7.26.10",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.10.tgz",
"integrity": "sha512-2WJMeRQPHKSPemqk/awGrAiuFfzBmOIPXKizAsVhWH9YJqLZ0H+HS4c8loHGgW6utJ3E/ejXQUsiGaQy2NZ9Fw==",
"license": "MIT",
"dependencies": {
"regenerator-runtime": "^0.14.0"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@emnapi/core": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.3.1.tgz",
@@ -1210,6 +1224,15 @@
"dev": true,
"license": "MIT"
},
"node_modules/@types/hast": {
"version": "2.3.10",
"resolved": "https://registry.npmjs.org/@types/hast/-/hast-2.3.10.tgz",
"integrity": "sha512-McWspRw8xx8J9HurkVBfYj0xKoE25tOFlHGdx4MJ5xORQrMGZNqJhVQWaIbm6Oyla5kYOXtDiopzKRJzEOkwJw==",
"license": "MIT",
"dependencies": {
"@types/unist": "^2"
}
},
"node_modules/@types/json-schema": {
"version": "7.0.15",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
@@ -1254,6 +1277,22 @@
"@types/react": "^19.0.0"
}
},
"node_modules/@types/react-syntax-highlighter": {
"version": "15.5.13",
"resolved": "https://registry.npmjs.org/@types/react-syntax-highlighter/-/react-syntax-highlighter-15.5.13.tgz",
"integrity": "sha512-uLGJ87j6Sz8UaBAooU0T6lWJ0dBmjZgN1PZTrj05TNql2/XpC6+4HhMT5syIdFUUt+FASfCeLLv4kBygNU+8qA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/react": "*"
}
},
"node_modules/@types/unist": {
"version": "2.0.11",
"resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz",
"integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==",
"license": "MIT"
},
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.27.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.27.0.tgz",
@@ -2105,6 +2144,36 @@
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
"node_modules/character-entities": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/character-entities/-/character-entities-1.2.4.tgz",
"integrity": "sha512-iBMyeEHxfVnIakwOuDXpVkc54HijNgCyQB2w0VfGQThle6NXn50zU6V/u+LDhxHcDUPojn6Kpga3PTAD8W1bQw==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/character-entities-legacy": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-1.1.4.tgz",
"integrity": "sha512-3Xnr+7ZFS1uxeiUDvV02wQ+QDbc55o97tIV5zHScSPJpcLm/r0DFPcoY3tYRp+VZukxuMeKgXYmsXQHO05zQeA==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/character-reference-invalid": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-1.1.4.tgz",
"integrity": "sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/chokidar": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
@@ -2172,6 +2241,16 @@
"simple-swizzle": "^0.2.2"
}
},
"node_modules/comma-separated-tokens": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-1.0.8.tgz",
"integrity": "sha512-GHuDRO12Sypu2cV70d1dkA2EUmXHgntrzbpvOB+Qy+49ypNfGgFQIC2fhhXbnyrJRynDCAARsT7Ou0M6hirpfw==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
@@ -3042,6 +3121,19 @@
"reusify": "^1.0.4"
}
},
"node_modules/fault": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/fault/-/fault-1.0.4.tgz",
"integrity": "sha512-CJ0HCB5tL5fYTEA7ToAq5+kTwd++Borf1/bifxd9iT70QcXr4MRrO3Llf8Ifs70q+SJcGHFtnIE/Nw6giCtECA==",
"license": "MIT",
"dependencies": {
"format": "^0.2.0"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/file-entry-cache": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
@@ -3122,6 +3214,14 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/format": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/format/-/format-0.2.2.tgz",
"integrity": "sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==",
"engines": {
"node": ">=0.4.x"
}
},
"node_modules/function-bind": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
@@ -3390,6 +3490,48 @@
"node": ">= 0.4"
}
},
"node_modules/hast-util-parse-selector": {
"version": "2.2.5",
"resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-2.2.5.tgz",
"integrity": "sha512-7j6mrk/qqkSehsM92wQjdIgWM2/BW61u/53G6xmC8i1OmEdKLHbk419QKQUjz6LglWsfqoiHmyMRkP1BGjecNQ==",
"license": "MIT",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/hastscript": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/hastscript/-/hastscript-6.0.0.tgz",
"integrity": "sha512-nDM6bvd7lIqDUiYEiu5Sl/+6ReP0BMk/2f4U/Rooccxkj0P5nm+acM5PrGJ/t5I8qPGiqZSE6hVAwZEdZIvP4w==",
"license": "MIT",
"dependencies": {
"@types/hast": "^2.0.0",
"comma-separated-tokens": "^1.0.0",
"hast-util-parse-selector": "^2.0.0",
"property-information": "^5.0.0",
"space-separated-tokens": "^1.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/highlight.js": {
"version": "10.7.3",
"resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz",
"integrity": "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==",
"license": "BSD-3-Clause",
"engines": {
"node": "*"
}
},
"node_modules/highlightjs-vue": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/highlightjs-vue/-/highlightjs-vue-1.0.0.tgz",
"integrity": "sha512-PDEfEF102G23vHmPhLyPboFCD+BkMGu+GuJe2d9/eH4FsCwvgBpnc9n0pGE+ffKdph38s6foEZiEjdgHdzp+IA==",
"license": "CC0-1.0"
},
"node_modules/ignore": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
@@ -3449,6 +3591,30 @@
"node": ">= 0.4"
}
},
"node_modules/is-alphabetical": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-1.0.4.tgz",
"integrity": "sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/is-alphanumerical": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-1.0.4.tgz",
"integrity": "sha512-UzoZUr+XfVz3t3v4KyGEniVL9BDRoQtY7tOyrRybkVNjDFWyo1yhXNGrrBTQxp3ib9BLAWs7k2YKBQsFRkZG9A==",
"license": "MIT",
"dependencies": {
"is-alphabetical": "^1.0.0",
"is-decimal": "^1.0.0"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/is-array-buffer": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz",
@@ -3601,6 +3767,16 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/is-decimal": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-1.0.4.tgz",
"integrity": "sha512-RGdriMmQQvZ2aqaQq3awNA6dCGtKpiDFcOzrTWrDAT2MiWrKQVPmxLGHl7Y2nNu6led0kEyoX0enY0qXYsv9zw==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/is-extglob": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
@@ -3659,6 +3835,16 @@
"node": ">=0.10.0"
}
},
"node_modules/is-hexadecimal": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-1.0.4.tgz",
"integrity": "sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/is-map": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz",
@@ -4026,6 +4212,20 @@
"loose-envify": "cli.js"
}
},
"node_modules/lowlight": {
"version": "1.20.0",
"resolved": "https://registry.npmjs.org/lowlight/-/lowlight-1.20.0.tgz",
"integrity": "sha512-8Ktj+prEb1RoCPkEOrPMYUN/nCggB7qAWe3a7OpMjWQkh3l2RD5wKRQ+o8Q8YuI9RG/xs95waaI/E6ym/7NsTw==",
"license": "MIT",
"dependencies": {
"fault": "^1.0.0",
"highlight.js": "~10.7.0"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
@@ -4381,6 +4581,24 @@
"node": ">=6"
}
},
"node_modules/parse-entities": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-2.0.0.tgz",
"integrity": "sha512-kkywGpCcRYhqQIchaWqZ875wzpS/bMKhz5HnN3p7wveJTkTtyAB/AlnS0f8DFSqYW1T82t6yEAkEcB+A1I3MbQ==",
"license": "MIT",
"dependencies": {
"character-entities": "^1.0.0",
"character-entities-legacy": "^1.0.0",
"character-reference-invalid": "^1.0.0",
"is-alphanumerical": "^1.0.0",
"is-decimal": "^1.0.0",
"is-hexadecimal": "^1.0.0"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/path-exists": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
@@ -4475,6 +4693,15 @@
"node": ">= 0.8.0"
}
},
"node_modules/prismjs": {
"version": "1.30.0",
"resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.30.0.tgz",
"integrity": "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/prop-types": {
"version": "15.8.1",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
@@ -4487,6 +4714,19 @@
"react-is": "^16.13.1"
}
},
"node_modules/property-information": {
"version": "5.6.0",
"resolved": "https://registry.npmjs.org/property-information/-/property-information-5.6.0.tgz",
"integrity": "sha512-YUHSPk+A30YPv+0Qf8i9Mbfe/C0hdPXk1s1jPVToV8pk8BQtpw10ct89Eo7OWkutrwqvT0eicAxlOg3dOAu8JA==",
"license": "MIT",
"dependencies": {
"xtend": "^4.0.0"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/punycode": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
@@ -4546,6 +4786,23 @@
"dev": true,
"license": "MIT"
},
"node_modules/react-syntax-highlighter": {
"version": "15.6.1",
"resolved": "https://registry.npmjs.org/react-syntax-highlighter/-/react-syntax-highlighter-15.6.1.tgz",
"integrity": "sha512-OqJ2/vL7lEeV5zTJyG7kmARppUjiB9h9udl4qHQjjgEos66z00Ia0OckwYfRxCSFrW8RJIBnsBwQsHZbVPspqg==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.3.1",
"highlight.js": "^10.4.1",
"highlightjs-vue": "^1.0.0",
"lowlight": "^1.17.0",
"prismjs": "^1.27.0",
"refractor": "^3.6.0"
},
"peerDependencies": {
"react": ">= 0.14.0"
}
},
"node_modules/readdirp": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
@@ -4583,6 +4840,36 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/refractor": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/refractor/-/refractor-3.6.0.tgz",
"integrity": "sha512-MY9W41IOWxxk31o+YvFCNyNzdkc9M20NoZK5vq6jkv4I/uh2zkWcfudj0Q1fovjUQJrNewS9NMzeTtqPf+n5EA==",
"license": "MIT",
"dependencies": {
"hastscript": "^6.0.0",
"parse-entities": "^2.0.0",
"prismjs": "~1.27.0"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/refractor/node_modules/prismjs": {
"version": "1.27.0",
"resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.27.0.tgz",
"integrity": "sha512-t13BGPUlFDR7wRB5kQDG4jjl7XeuH6jbJGt11JHPL96qwsEHNX2+68tFXqc1/k+/jALsbSWJKUOT/hcYAZ5LkA==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/regenerator-runtime": {
"version": "0.14.1",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz",
"integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==",
"license": "MIT"
},
"node_modules/regexp.prototype.flags": {
"version": "1.5.4",
"resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz",
@@ -5005,6 +5292,16 @@
"node": ">=0.10.0"
}
},
"node_modules/space-separated-tokens": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-1.1.5.tgz",
"integrity": "sha512-q/JSVd1Lptzhf5bkYm4ob4iWPjx0KiRe3sRFBNrVqbJkFaBm5vbbowy1mymoPNLRa52+oadOhJ+K49wsSeSjTA==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/stable-hash": {
"version": "0.0.5",
"resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.5.tgz",
@@ -5551,6 +5848,15 @@
"node": ">=0.10.0"
}
},
"node_modules/xtend": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
"integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
"license": "MIT",
"engines": {
"node": ">=0.4"
}
},
"node_modules/yocto-queue": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
+3 -1
View File
@@ -13,13 +13,15 @@
"bootstrap-icons": "^1.11.3",
"next": "15.2.3",
"react": "^19.0.0",
"react-dom": "^19.0.0"
"react-dom": "^19.0.0",
"react-syntax-highlighter": "^15.6.1"
},
"devDependencies": {
"@eslint/eslintrc": "^3",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"@types/react-syntax-highlighter": "^15.5.13",
"eslint": "^9",
"eslint-config-next": "15.2.3",
"sass": "^1.86.0",
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 44 KiB

+96
View File
@@ -0,0 +1,96 @@
"use client";
import IndiceContenido from "@/components/content-index";
import VistaComponente from "@/components/vista-componente";
import Button from "@/components/button";
export default function Page() {
return (
<>
<IndiceContenido
items={[
{ id: "button-basico", label: "Botón básico" },
{ id: "button-icono", label: "Botón con ícono" },
{ id: "button-props", label: "Props del componente" },
]}
/>
<div className="my-5">
<h1 className="mb-4 display-5">
🧩 Componente: <code>Button</code>
</h1>
<p className="mb-4 text-muted">
El componente <code>&lt;Button /&gt;</code> permite crear botones con
estilos de Bootstrap, variantes personalizables, íconos opcionales y
soporte completo para props de HTML estándar.
</p>
<hr className="my-5" />
<VistaComponente
id="button-basico"
title="🚀 Botón básico"
description="Un botón con texto y variante primaria (por defecto)."
code={`<Button onClick={() => alert("¡Hola mundo!")}>
Clic aquí
</Button>`}
>
<Button onClick={() => alert("¡Hola mundo!")} className="mb-3">Clic aquí</Button>
</VistaComponente>
<VistaComponente
id="button-icono"
title="🔔 Botón con ícono"
description="Puedes agregar un ícono Bootstrap usando el nombre del ícono o pasar un componente."
code={`<Button icon="check-circle" variant="success">
Guardar
</Button>`}
>
<Button icon="check-circle" variant="success" className="mb-3">
Guardar
</Button>
</VistaComponente>
<section id="button-props" className="mb-5">
<h2 className="h5"> Props del componente</h2>
<ul>
<li>
<code>variant?: string</code> Clase base del botón. Ej:{" "}
<code>"primary"</code>, <code>"danger"</code>.
</li>
<li>
<code>outline?: boolean</code> Si el botón debe usar estilo{" "}
<code>outline</code>.
</li>
<li>
<code>icon?: string | ReactNode</code> Ícono Bootstrap (por
nombre) o componente React.
</li>
<li>
<code>size?: "sm" | "lg"</code> Tamaño del botón.
</li>
<li>
<code>className?: string</code> Clases adicionales para el
botón.
</li>
<li>
<code>type?: "button" | "submit" | "reset"</code> Tipo de botón.
</li>
<li>
<code>disabled?: boolean</code> Desactiva el botón.
</li>
<li>
<code>children: ReactNode</code> Contenido del botón.
</li>
<li>
<code>...rest</code> Cualquier otra prop válida de{" "}
<code>&lt;button&gt;</code>.
</li>
</ul>
</section>
</div>
</>
);
}
+187
View File
@@ -0,0 +1,187 @@
"use client";
import IndiceContenido from "@/components/content-index";
import Input from "@/components/input";
import VistaComponente from "@/components/vista-componente";
import { inputBasicExample, inputPasswordExample, inputStylesExample } from "@/snippets/input/input-examples";
import { useState } from "react";
export default function Page() {
const [value, setValue] = useState("");
const [password, setPassword] = useState("");
return (
<>
<IndiceContenido
items={[
{ id: "input-basico", label: "Input básico" },
{ id: "input-password", label: "Input de contraseña" },
{ id: "input-estilos", label: "Personalización de estilos" },
{ id: "input-props", label: "Props del componente" },
]}
/>
<div className="my-5">
<h1 className="mb-4 display-5">
🔤 Componente: <code>Input</code>
</h1>
<p className="mb-4 text-muted">
El componente <code>&lt;Input /&gt;</code> es un campo de entrada
reutilizable con soporte para etiquetas, tipos como{" "}
<code>password</code>, accesibilidad mejorada, y personalización de
estilos. Además, incluye un botón para mostrar/ocultar contraseñas
cuando el tipo es <code>password</code>.
</p>
<hr className="my-5" />
<VistaComponente
id="input-basico"
title="Input básico"
description="Un input controlado con una etiqueta visible. Puedes personalizar su valor y manejar los cambios mediante `useState`."
code={inputBasicExample}
>
<Input
label="Nombre"
placeholder="Ingresa tu nombre"
value={value}
onChange={(e) => setValue(e.target.value)}
/>
</VistaComponente>
<VistaComponente
id="input-password"
title="Input de contraseña"
description="Cuando el tipo es `password`, aparece un botón para mostrar/ocultar el valor del campo."
code={inputPasswordExample}
>
<Input
type="password"
label="Contraseña"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
</VistaComponente>
<VistaComponente
id="input-estilos"
title="Personalización de estilos"
description="Puedes aplicar clases personalizadas a los elementos internos del input."
code={inputStylesExample}
>
<Input
label="Correo"
placeholder="ejemplo@correo.com"
className={{
container: "mb-4",
label: "text-primary fw-bold",
input: "border-success",
}}
/>
</VistaComponente>
<section className="mb-5" id="input-props">
<h2 className="h5 mb-3">Props del componente</h2>
<table className="table table-bordered table-striped align-middle">
<thead>
<tr>
<th scope="col">Prop</th>
<th scope="col">Tipo</th>
<th scope="col">Descripción</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<code>label?</code>
</td>
<td>
<code>string</code>
</td>
<td>Etiqueta asociada al campo de entrada.</td>
</tr>
<tr>
<td>
<code>type?</code>
</td>
<td>
<code>string</code>
</td>
<td>
Tipo del input. Soporta cualquier tipo válido de HTML como{" "}
<code>text</code>, <code>password</code>, <code>email</code>,
etc.
</td>
</tr>
<tr>
<td>
<code>value?</code>
</td>
<td>
<code>string</code>
</td>
<td>Valor del input en modo controlado.</td>
</tr>
<tr>
<td>
<code>onChange?</code>
</td>
<td>
<code>
(e: React.ChangeEvent&lt;HTMLInputElement&gt;) =&gt; void
</code>
</td>
<td>Función que se ejecuta al cambiar el valor del input.</td>
</tr>
<tr>
<td>
<code>name?</code>
</td>
<td>
<code>string</code>
</td>
<td>Nombre del campo, útil en formularios.</td>
</tr>
<tr>
<td>
<code>id?</code>
</td>
<td>
<code>string</code>
</td>
<td>
ID personalizado para el input. Si no se proporciona, se
genera automáticamente.
</td>
</tr>
<tr>
<td>
<code>className?</code>
</td>
<td>
<code>{`{ container?, label?, input? }`}</code>
</td>
<td>
Permite personalizar los estilos del contenedor, la etiqueta y
el input.
</td>
</tr>
<tr>
<td>
<code>...rest</code>
</td>
<td>
<code>InputHTMLAttributes&lt;HTMLInputElement&gt;</code>
</td>
<td>
Cualquier otra prop válida de un <code>&lt;input /&gt;</code>{" "}
estándar.
</td>
</tr>
</tbody>
</table>
</section>
</div>
</>
);
}
+13
View File
@@ -0,0 +1,13 @@
import React from "react";
export default function ComponentsLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<div className="container-xxl bd-layout mt-4">
{children}
</div>
);
}
+200
View File
@@ -0,0 +1,200 @@
"use client";
import React, { useState } from "react";
import RadioOptionGroup, { RadioOption } from "@/components/radio-option-group";
import CheckboxOptionGroup, {
CheckboxOption,
} from "@/components/checkbox-option-group";
import VistaComponente from "@/components/vista-componente";
import { CodeBlock } from "@/components/code-block";
import {
checkboxBasicExample,
checkboxLimitExample,
radioBasicExample,
radioDisabledExample,
} from "@/snippets/options/option-examples";
import IndiceContenido from "@/components/content-index";
export default function RadioCheckboxDocs() {
const [selectedRadio, setSelectedRadio] = useState("op1");
const [checkboxBasicSelected, setCheckboxBasicSelected] = useState<string[]>([
"a",
]);
const [checkboxLimitSelected, setCheckboxLimitSelected] = useState<string[]>(
[]
);
const radioOptions: RadioOption<string>[] = [
{ label: "Opción 1", value: "op1" },
{ label: "Opción 2", value: "op2" },
{ label: "Opción 3", value: "op3" },
];
const checkboxOptions: CheckboxOption<string>[] = [
{ label: "Activo", value: "a" },
{ label: "Inactivo", value: "b" },
{ label: "Pendiente", value: "c" },
];
const handleCheckboxChange = (
value: string,
checked: boolean,
setValues: React.Dispatch<React.SetStateAction<string[]>>
) => {
setValues((prev) =>
checked ? [...prev, value] : prev.filter((v) => v !== value)
);
};
return (
<>
<IndiceContenido
items={[
{ id: "uso-basico", label: "Uso básico" },
{ id: "opcion-deshabilitada", label: "Opción deshabilitada" },
{ id: "checkbox-basico", label: "Checkbox: Uso básico" },
{ id: "checkbox-limite", label: "Checkbox: Límite de selección" },
]}
/>
<div className="my-5">
<h1 className="mb-4 display-5">
Componentes: <code>RadioOptionGroup</code> y{" "}
<code>CheckboxOptionGroup</code>
</h1>
<p className="mb-4 text-muted">
Estos componentes permiten mostrar grupos de opciones radio o
checkbox, para seleccionar una o varias opciones respectivamente. Son
útiles en formularios, encuestas o filtros. Los estilos de estos
componentes se basan en Bootstrap y se encuentran el el archivo{" "}
<code>@/styles/sass/_extneds.scss</code> y es el selector{" "}
<code>.option</code>.
</p>
<CodeBlock
code={`import RadioOptionGroup, { RadioOption } from "@/components/radio-option-group";
import CheckboxOptionGroup, { CheckboxOption } from "@/components/checkbox-option-group";`}
/>
<hr className="my-5" />
<VistaComponente
title="Radio: Uso básico"
description="Grupo de opciones radio controladas por estado."
code={radioBasicExample}
id="uso-basico"
>
<RadioOptionGroup
name="grupo-radio"
options={radioOptions}
selectedValue={selectedRadio}
onChange={setSelectedRadio}
/>
</VistaComponente>
<VistaComponente
title="Radio: Opción deshabilitada"
description="Se pueden deshabilitar opciones individualmente."
code={radioDisabledExample}
id="opcion-deshabilitada"
>
<RadioOptionGroup
name="grupo-radio-disabled"
options={[
{ label: "Activo", value: "a" },
{ label: "Inactivo", value: "b", disabled: true },
]}
selectedValue="a"
onChange={() => {}}
/>
</VistaComponente>
<VistaComponente
title="Checkbox: Uso básico"
description="Grupo de opciones múltiples con control de estado."
code={checkboxBasicExample}
id="checkbox-basico"
>
<CheckboxOptionGroup
name="grupo-checkbox"
options={checkboxOptions}
selectedValues={checkboxBasicSelected}
onChange={(value, checked) =>
handleCheckboxChange(value, checked, setCheckboxBasicSelected)
}
/>
</VistaComponente>
<VistaComponente
title="Checkbox: Límite de selección"
description="Usa `maxSelected` para limitar la cantidad de opciones seleccionables."
code={checkboxLimitExample}
id="checkbox-limite"
>
<CheckboxOptionGroup
name="grupo-checkbox-limit"
options={checkboxOptions}
selectedValues={checkboxLimitSelected}
onChange={(value, checked) =>
handleCheckboxChange(value, checked, setCheckboxLimitSelected)
}
maxSelected={2}
/>
</VistaComponente>
<section className="mb-5 mt-5">
<h2 className="h5">Props comunes</h2>
<ul>
<li>
<code>name</code>: Nombre del grupo de inputs.
</li>
<li>
<code>options</code>: Lista de opciones con <code>label</code>,{" "}
<code>value</code> y opcionalmente <code>disabled</code>.
</li>
<li>
<code>className?</code>: Clases CSS adicionales.
</li>
</ul>
<h2 className="h5 mt-4">
Props de <code>RadioOptionGroup</code>
</h2>
<ul>
<li>
<code>selectedValue?</code>: Valor seleccionado actualmente.
</li>
<li>
<code>onChange</code>: Función que se ejecuta al cambiar de
opción.
</li>
<li>
<code>disabled?</code>: Deshabilita todo el grupo.
</li>
</ul>
<h2 className="h5 mt-4">
Props de <code>CheckboxOptionGroup</code>
</h2>
<ul>
<li>
<code>selectedValues</code>: Lista de valores seleccionados.
</li>
<li>
<code>onChange</code>: Función que se ejecuta al marcar o
desmarcar una opción.
</li>
<li>
<code>maxSelected?</code>: Número máximo de opciones
seleccionables.
</li>
<li>
<code>disabled?</code>: Deshabilita todo el grupo.
</li>
</ul>
</section>
</div>
</>
);
}
+107
View File
@@ -0,0 +1,107 @@
"use client";
import React, { useState } from "react";
import VistaComponente from "@/components/vista-componente";
import { CodeBlock } from "@/components/code-block";
import Pagination from "@/components/pagination";
import {
paginationBasic,
paginationWithDelta,
} from "@/snippets/pagination/pagination-example";
import IndiceContenido from "@/components/content-index";
export default function Page() {
const [currentPage1, setCurrentPage1] = useState(1);
const [currentPage2, setCurrentPage2] = useState(6);
return (
<>
<IndiceContenido
items={[
{ id: "uso-basico", label: "Uso básico" },
{
id: "control-con-delta",
label: "Control de visibilidad con `delta`",
},
{ id: "pagination-props", label: "Props del componente" },
]}
/>
<div className="my-5">
<h1 className="mb-4 display-5">
📄 Componente: <code>Pagination</code>
</h1>
<p className="mb-4 text-muted">
El componente <code>&lt;Pagination /&gt;</code> permite navegar entre
páginas de forma controlada. Es completamente reutilizable y permite
mostrar botones de anterior/siguiente y paginación con saltos.
</p>
<CodeBlock code={`import Pagination from "@/components/pagination";`} />
<hr className="my-5" />
{/* Ejemplo básico */}
<VistaComponente
title="Uso básico"
description="Muestra los botones de página y permite cambiar de página externamente con `onPageChange`."
code={paginationBasic}
id="uso-basico"
>
<Pagination
totalPages={5}
currentPage={currentPage1}
onPageChange={setCurrentPage1}
/>
<p className="mt-2 text-sm text-muted">
Página actual: {currentPage1}
</p>
</VistaComponente>
{/* Ejemplo con delta */}
<VistaComponente
title="🔍 Control de visibilidad con `delta`"
description="Usa la prop `delta` para definir cuántas páginas cercanas a la actual deben mostrarse."
code={paginationWithDelta}
id="control-con-delta"
>
<Pagination
totalPages={20}
currentPage={currentPage2}
onPageChange={setCurrentPage2}
delta={2}
/>
<p className="mt-2 text-sm text-muted">
Página actual: {currentPage2}
</p>
</VistaComponente>
{/* Props */}
<section className="mb-5 mt-5">
<h2 className="h5"> Props del componente</h2>
<ul>
<li>
<code>totalPages</code>: <em>number</em> Total de páginas
disponibles.
</li>
<li>
<code>currentPage</code>: <em>number</em> Página actual.
</li>
<li>
<code>onPageChange</code>: <em>(page: number) =&gt; void</em>
Callback que se ejecuta al cambiar de página.
</li>
<li>
<code>delta?</code>: <em>number</em> Número de páginas vecinas
visibles (por defecto: 1).
</li>
<li>
<code>className?</code>: <em>string</em> Clases CSS
personalizadas para el contenedor.
</li>
</ul>
</section>
</div>
</>
);
}
+123
View File
@@ -0,0 +1,123 @@
"use client";
import IndiceContenido from "@/components/content-index";
import Select from "@/components/select";
import VistaComponente from "@/components/vista-componente";
import { countrySelectSnippet, languageSelectSnippet } from "@/snippets/select/select";
import { useState } from "react";
export default function Page() {
const [pais, setPais] = useState("");
const opcionesPais = [
{ value: "mx", label: "México" },
{ value: "us", label: "Estados Unidos" },
{ value: "ca", label: "Canadá" },
];
return (
<>
<IndiceContenido
items={[
{ id: "select-basico", label: "Select básico" },
{ id: "select-estilos", label: "Personalización de estilos" },
{ id: "select-props", label: "Props del componente" },
]}
/>
<div className="my-5">
<h1 className="mb-4 display-5">
📑 Componente: <code>Select</code>
</h1>
<p className="mb-4 text-muted">
El componente <code>&lt;Select /&gt;</code> permite mostrar una lista
desplegable con opciones personalizables, soporta etiquetas, estilos
por clase, y es compatible con formularios.
</p>
<hr className="my-5" />
<VistaComponente
title="🌍 Select básico"
description="Un select controlado con opciones simples y un placeholder deshabilitado."
code={countrySelectSnippet}
id="select-basico"
>
<Select
label="País"
name="pais"
placeholder="Selecciona un país"
options={opcionesPais}
value={pais}
onChange={(e) => setPais(e.target.value)}
/>
</VistaComponente>
<VistaComponente
title="🎨 Personalización de estilos"
description="Puedes aplicar clases personalizadas al contenedor, etiqueta o al select."
code={languageSelectSnippet}
id="select-estilos"
>
<Select
label="Idioma"
name="idioma"
placeholder="Selecciona un idioma"
options={[
{ value: "es", label: "Español" },
{ value: "en", label: "Inglés" },
{ value: "fr", label: "Francés" },
]}
className={{
container: "mb-4",
label: "text-primary fw-bold",
input: "border border-success",
}}
/>
</VistaComponente>
<section className="mb-5">
<h2 className="h5"> Props del componente</h2>
<ul>
<li>
<code>label: string</code> Etiqueta visible del campo.
</li>
<li>
<code>
options: &#123; value: string &#124; number; label: string
&#125;[]
</code>{" "}
Opciones a mostrar.
</li>
<li>
<code>placeholder?: string</code> Texto inicial deshabilitado.
</li>
<li>
<code>name?: string</code> Nombre del campo.
</li>
<li>
<code>id?: string</code> ID del campo (se genera si no se
proporciona).
</li>
<li>
<code>defaultValue?: string | number</code> Valor por defecto
(modo no controlado).
</li>
<li>
<code>value?: string | number</code> Valor actual del select
(modo controlado).
</li>
<li>
<code>className?: &#123; container?, label?, input? &#125;</code>{" "}
Clases personalizadas.
</li>
<li>
<code>...rest</code> Cualquier otra prop válida de{" "}
<code>&lt;select&gt;</code>.
</li>
</ul>
</section>
</div>
</>
);
}
+325
View File
@@ -0,0 +1,325 @@
"use client";
import React, { useState } from "react";
import Table, { Header, TableSkeleton } from "@/components/table";
import VistaComponente from "@/components/vista-componente";
import {
tableBasicExample,
tableSkeletonExample,
} from "@/snippets/table/table-basic";
import { tableClickable } from "@/snippets/table/table-clickable";
import { tableAutoPagination } from "@/snippets/table/table-auto-pagination";
import { tableCustomPagination } from "@/snippets/table/table-custom-pagination";
import { tableCustomCellRender } from "@/snippets/table/table-custom-cell-render";
import { tableColumnWidth } from "@/snippets/table/table-column-width";
import { CodeBlock } from "@/components/code-block";
import Input from "@/components/input";
import { tableFilter } from "@/snippets/table/table-filter";
import IndiceContenido from "@/components/content-index";
export default function Page() {
const headers: Header<{
name: string;
age: number;
email: string;
acciones?: null;
}>[] = [
{ key: "name", label: "Nombre", width: "30%" },
{ key: "age", label: "Edad", width: "15%" },
{ key: "email", label: "Correo", width: "40%" },
];
const baseData = [
{ name: "Juan", age: 20, email: "juan@example.com" },
{ name: "María", age: 25, email: "maria@example.com" },
{ name: "Pedro", age: 30, email: "pedro@example.com" },
{ name: "Luisa", age: 28, email: "luisa@example.com" },
];
const [currentPage, setCurrentPage] = useState(1);
const [search, setSearch] = useState("");
const filteredData = baseData.filter((item) =>
item.name.toLowerCase().includes(search.toLowerCase())
);
return (
<>
<IndiceContenido
items={[
{ id: "uso-basico", label: "Uso básico" },
{ id: "tabla-en-estado-de-carga", label: "Tabla en estado de carga" },
{ id: "filas-clicables", label: "Filas clicables" },
{ id: "paginacion-personalizada", label: "Paginación personalizada" },
{
id: "paginacion-controlada-externamente",
label: "Paginación controlada externamente",
},
{
id: "render-personalizado-por-celda",
label: "Render personalizado por celda",
},
{
id: "personalizacion-del-tamano-de-columnas",
label: "Personalización del tamaño de columnas",
},
{
id: "filtro-simple-con-input-externo",
label: "Filtro simple con input externo",
},
{
id: "usar-identificadores-unicos-con-rowkey",
label: "Usar identificadores únicos con `rowKey`",
},
{ id: "props-del-componente", label: "Props del componente" },
]}
/>
<div className="my-5">
<h1 className="mb-4 display-5">
Componente: <code>Table</code>
</h1>
<p className="mb-4 text-muted">
El componente <code>&lt;Table /&gt;</code> permite mostrar datos en
forma tabular, con funcionalidades como ordenamiento, paginación,
renderizado personalizado, clic en filas y un estado de carga
esqueleto. Los estilos de esta tabla se encuentran en{" "}
<code>@/styles/sass/_table.scss</code>
</p>
<CodeBlock
code={
'import Table, { Header, TablaSkeleton } from "@/components/table";'
}
/>
<hr className="my-5" />
<VistaComponente
title="Uso básico"
description="Tabla simple con headers y datos sin configuración adicional. el ancho de las columnas se ajusta automáticamente a no ser que se especifique."
code={tableBasicExample}
id="uso-basico"
>
<Table headers={headers} data={baseData} />
</VistaComponente>
{/* SKELETON */}
<VistaComponente
title="Tabla en estado de carga"
description="El componente `TablaSkeleton` simula una tabla vacía mientras se cargan los datos."
code={tableSkeletonExample}
id="tabla-en-estado-de-carga"
>
<TableSkeleton rows={3} columns={4} />
</VistaComponente>
<VistaComponente
title="Filas clicables"
description="Usa `onRowClick` para ejecutar acciones al hacer clic en una fila."
code={tableClickable}
id="filas-clicables"
>
<Table
headers={headers}
data={baseData}
onRowClick={(row) => alert(`Seleccionaste a ${row.name}`)}
/>
</VistaComponente>
<VistaComponente
title="Paginación personalizada"
description="Controla cuántas filas se muestran por página con `rowsPerPage`."
code={tableAutoPagination}
id="paginacion-personalizada"
>
<Table headers={headers} data={baseData} rowsPerPage={2} />
</VistaComponente>
<VistaComponente
title="Paginación controlada externamente"
description="Usa `controlledPage` y `onPageChange` para controlar la página actual desde el estado del componente padre."
code={tableCustomPagination}
id="paginacion-controlada-externamente"
>
<Table
headers={headers}
data={baseData}
rowsPerPage={2}
controlledPage={currentPage}
onPageChange={setCurrentPage}
/>
</VistaComponente>
<VistaComponente
title="Render personalizado por celda"
description="Personaliza cómo se muestran ciertas columnas usando la propiedad `render`."
code={tableCustomCellRender}
id="render-personalizado-por-celda"
>
<Table
headers={[
...headers,
{
key: "acciones",
label: "Acciones",
width: "15%",
render: (_, row) => (
<button
className="btn btn-sm btn-primary"
onClick={() => alert(row.email)}
>
Ver correo
</button>
),
},
]}
data={baseData.map((d) => ({ ...d, acciones: null }))}
/>
</VistaComponente>
<VistaComponente
title="Personalización del tamaño de columnas"
description="Puedes controlar el ancho de cada columna usando la propiedad `width` dentro de cada header."
code={tableColumnWidth}
id="personalizacion-del-tamano-de-columnas"
>
<Table
headers={[
{ key: "name", label: "Nombre", width: "60%" },
{ key: "age", label: "Edad", width: "15%" },
{ key: "email", label: "Correo electrónico", width: "35%" },
]}
data={baseData}
/>
</VistaComponente>
{/* FILTRO */}
<VistaComponente
title="🔍 Filtro simple con input externo"
description="Filtrar los datos de la tabla con un input externo."
code={tableFilter}
id="filtro-simple-con-input-externo"
>
<Input
placeholder="Buscar por nombre..."
value={search}
onChange={(e) => setSearch(e.target.value)}
/>
<Table headers={headers} data={filteredData} />
</VistaComponente>
<VistaComponente
title="🆔 Usar identificadores únicos con `rowKey`"
description="Proporciona una función para identificar cada fila de forma única."
id="usar-identificadores-unicos-con-rowkey"
>
<Table
headers={headers}
data={baseData.map((item, index) => ({ ...item, id: index }))}
rowKey={(row) => row.email}
/>
</VistaComponente>
{/* PROPS */}
<section className="mb-5" id="props-del-componente">
<h2 className="h5 mb-3">Props del componente</h2>
<table className="table table-bordered table-striped align-middle">
<thead>
<tr>
<th scope="col">Prop</th>
<th scope="col">Tipo</th>
<th scope="col">Descripción</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<code>headers</code>
</td>
<td>
<code>Header&lt;T&gt;[]</code>
</td>
<td>
Columnas de la tabla. Cada header puede tener key, label,
width, y una función <code>render</code>.
</td>
</tr>
<tr>
<td>
<code>data</code>
</td>
<td>
<code>T[]</code>
</td>
<td>Datos a mostrar en la tabla.</td>
</tr>
<tr>
<td>
<code>rowsPerPage?</code>
</td>
<td>
<code>number</code>
</td>
<td>
Número de filas por página (por defecto: <code>10</code>).
</td>
</tr>
<tr>
<td>
<code>onRowClick?</code>
</td>
<td>
<code>(row: T) =&gt; void</code>
</td>
<td>Función que se ejecuta al hacer clic en una fila.</td>
</tr>
<tr>
<td>
<code>rowKey?</code>
</td>
<td>
<code>(row: T) =&gt; string | number</code>
</td>
<td>
Función para obtener un identificador único de cada fila
(evita usar <code>index</code>).
</td>
</tr>
<tr>
<td>
<code>className?</code>
</td>
<td>
<code>{"{ table?, row?, cell?, header? }"}</code>
</td>
<td>
Clases personalizadas para la tabla, filas, celdas y
encabezados.
</td>
</tr>
<tr>
<td>
<code>controlledPage?</code>
</td>
<td>
<code>number</code>
</td>
<td>Página actual controlada desde el componente padre.</td>
</tr>
<tr>
<td>
<code>onPageChange?</code>
</td>
<td>
<code>(page: number) =&gt; void</code>
</td>
<td>Función callback que se dispara al cambiar de página.</td>
</tr>
</tbody>
</table>
</section>
</div>
</>
);
}
-122
View File
@@ -1,122 +0,0 @@
"use client";
import React from "react";
import Input from "@/components/input";
export default function Page() {
return (
<div className="container my-5">
<h1 className="mb-4 display-5">
📦 Componente: <code>Input</code>
</h1>
<p className="mb-4 text-muted">
El componente <code>{`<Input />`}</code> encapsula un campo de entrada
HTML estándar con mejoras en accesibilidad, estilo y experiencia de
usuario. Soporta etiquetas, clases personalizadas, inputs
deshabilitados, y funcionalidad para mostrar u ocultar contraseñas.
</p>
<hr className="my-5" />
<section>
<h2 className="h5">🔹 Uso básico</h2>
<p>Un campo de texto simple con etiqueta y placeholder.</p>
<Input label="Nombre" name="nombre" placeholder="Escribe tu nombre" />
<pre className="mt-2 bg-light p-3 rounded">
<code>{`<Input label="Nombre" name="nombre" placeholder="Escribe tu nombre" />`}</code>
</pre>
</section>
<hr className="my-5" />
<section>
<h2 className="h5">🔐 Campo de tipo contraseña</h2>
<p className="mb-2">
Incluye un botón para mostrar u ocultar el contenido del campo.
</p>
<Input
label="Contraseña"
name="password"
type="password"
placeholder="••••••••"
/>
<pre className="mt-2 bg-light p-3 rounded">
<code>{`<Input
label="Contraseña"
name="password"
type="password"
placeholder="••••••••"
/>`}</code>
</pre>
</section>
<hr className="my-5" />
<section>
<h2 className="h5">🚫 Input deshabilitado</h2>
<Input
label="Correo electrónico"
name="email"
placeholder="email@ejemplo.com"
disabled
/>
<pre className="mt-2 bg-light p-3 rounded">
<code>{`<Input
label="Correo electrónico"
name="email"
placeholder="email@ejemplo.com"
disabled
/>`}</code>
</pre>
</section>
<hr className="my-5" />
<section>
<h2 className="h5">🎨 Personalización de clases</h2>
<p>
Puedes personalizar el estilo de cada parte del input: contenedor,
etiqueta e input.
</p>
<Input
label="Usuario"
name="username"
placeholder="ej: miguel_dev"
className={{
container: "mb-3 bg-light p-3 rounded",
label: "fw-bold text-primary",
input: "border border-primary",
}}
/>
<pre className="mt-2 bg-light p-3 rounded">
<code>{`<Input
label="Usuario"
name="username"
placeholder="ej: miguel_dev"
className={{
container: "mb-3 bg-light p-3 rounded",
label: "fw-bold text-primary",
input: "border border-primary",
}}
/>`}</code>
</pre>
</section>
<hr className="my-5" />
<section>
<h2 className="h5">📏 Input en contenedor reducido</h2>
<p>Se adapta a contenedores pequeños sin problemas.</p>
<div style={{ width: "300px" }} className="mt-3 ms-1 border p-3">
<Input label="Nombre" name="nombre" placeholder="Escribe tu nombre" />
</div>
<pre className="mt-2 bg-light p-3 rounded">
<code>{`<div style={{ width: "300px" }}>
<Input label="Nombre" name="nombre" placeholder="Escribe tu nombre" />
</div>`}</code>
</pre>
</section>
</div>
);
}
+2
View File
@@ -3,6 +3,7 @@ import { Roboto } from "next/font/google";
import 'bootstrap-icons/font/bootstrap-icons.css';
import '@/styles/sass/bootstrap.scss';
import Navbar from "@/components/navbar";
const roboto = Roboto({
weight: ["100", "300", "400", "500", "700", "900"],
@@ -25,6 +26,7 @@ export default function RootLayout({
return (
<html lang="en">
<body className={`${roboto.className}`}>
<Navbar />
{children}
</body>
</html>
+27 -15
View File
@@ -6,28 +6,40 @@ import React from "react";
export default function Page() {
return (
<main className="">
<div className="container">
<div className="row text-center py-5">
<div>
<Image
src="/CIDWA.png"
alt="CIDWA"
width={250}
height={250}
className="img-fluid"
/>
</div>
<h1 className="display-3 fw-bold">
Libreria de componentes de CIDWA
</h1>
<div className="container-fluid bg-gradient-primary-bottom text-center py-5">
<div className="mt-4">
<Image
src="/CIDWA.png"
alt="CIDWA"
width={250}
height={250}
className="img-fluid"
/>
</div>
<div className="row">
<h1 className="display-3 fw-bold text-azul">Libreria de componentes de CIDWA</h1>
</div>
<div className="container">
<h2>Componentes</h2>
<div className="row row-gap-4">
<div className="col-lg-4">
<ComponentCard title="Table" src="/table.png" href="table" />
</div>
<div className="col-lg-4">
<ComponentCard title="Input" src="/input.png" href="input" />
</div>
<div className="col-lg-4">
<ComponentCard
title="Pagination"
src="/pagination.png"
href="pagination"
/>
</div>
<div className="col-lg-4">
<ComponentCard title="Options" src="/options.png" href="options" />
</div>
<div className="col-lg-4">
<ComponentCard title="Select" src="/select.png" href="select" />
</div>
</div>
</div>
</main>
-225
View File
@@ -1,225 +0,0 @@
"use client";
import React, { useState } from "react";
import Table, { Header, TablaSkeleton } from "@/components/table";
import VistaComponente from "@/components/vista-componente";
import { tableBasicExample } from "@/snippets/table/table-basic";
export default function Page() {
const headers: Header<{
name: string;
age: number;
email: string;
acciones?: null;
}>[] = [
{ key: "name", label: "Nombre", width: "30%" },
{ key: "age", label: "Edad", width: "15%" },
{ key: "email", label: "Correo", width: "40%" },
];
const baseData = [
{ name: "Juan", age: 20, email: "juan@example.com" },
{ name: "María", age: 25, email: "maria@example.com" },
{ name: "Pedro", age: 30, email: "pedro@example.com" },
{ name: "Luisa", age: 28, email: "luisa@example.com" },
];
const [currentPage, setCurrentPage] = useState(1);
const [search, setSearch] = useState("");
const filteredData = baseData.filter((item) =>
item.name.toLowerCase().includes(search.toLowerCase())
);
return (
<div className="container my-5">
<h1 className="mb-4 display-5">
📊 Componente: <code>Table</code>
</h1>
<p className="mb-4 text-muted">
El componente <code>&lt;Table /&gt;</code> permite mostrar datos en
forma tabular, con funcionalidades como ordenamiento, paginación,
renderizado personalizado, clic en filas y un estado de carga esqueleto.
</p>
<hr className="my-5" />
{/* USO BÁSICO */}
<VistaComponente
title="Uso básico"
description="Tabla simple con headers y datos sin configuración adicional."
code={tableBasicExample}
>
<Table headers={headers} data={baseData} />
</VistaComponente>
{/* FILAS CLICABLES */}
<VistaComponente
title="🔱 Filas clicables"
description="Usa `onRowClick` para ejecutar acciones al hacer clic en una fila."
>
<Table
headers={headers}
data={baseData}
onRowClick={(row) => alert(`Seleccionaste a ${row.name}`)}
/>
</VistaComponente>
{/* PAGINACIÓN */}
<VistaComponente
title="📄 Paginación personalizada"
description="Controla cuántas filas se muestran por página con `rowsPerPage`."
>
<Table headers={headers} data={baseData} rowsPerPage={2} />
</VistaComponente>
{/* CONTROL DE PÁGINA EXTERNO */}
<VistaComponente
title="🔀 Paginación controlada externamente"
description="Usa `controlledPage` y `onPageChange` para controlar la página actual desde el estado del componente padre."
>
<Table
headers={headers}
data={baseData}
rowsPerPage={2}
controlledPage={currentPage}
onPageChange={setCurrentPage}
/>
</VistaComponente>
{/* RENDER PERSONALIZADO */}
<VistaComponente
title="🎨 Render personalizado por celda"
description="Personaliza cómo se muestran ciertas columnas usando la propiedad `render`."
>
<Table
headers={[
...headers,
{
key: "acciones",
label: "Acciones",
width: "15%",
render: (_, row) => (
<button
className="btn btn-sm btn-primary"
onClick={() => alert(row.email)}
>
Ver correo
</button>
),
},
]}
data={baseData.map((d) => ({ ...d, acciones: null }))}
/>
</VistaComponente>
{/* PERSONALIZACIÓN DE ANCHO */}
<VistaComponente
title="🖖 Personalización del tamaño de columnas"
description="Puedes controlar el ancho de cada columna usando la propiedad `width` dentro de cada header."
>
<Table
headers={[
{ key: "name", label: "Nombre", width: "50%" },
{ key: "age", label: "Edad", width: "15%" },
{ key: "email", label: "Correo electrónico", width: "35%" },
]}
data={baseData}
/>
</VistaComponente>
{/* ANCHO AUTOMÁTICO */}
<VistaComponente
title="📏 Columnas con tamaño automático"
description="Si no defines el ancho de las columnas, el navegador ajusta el tamaño según el contenido."
>
<Table
headers={[
{ key: "name", label: "Nombre" },
{ key: "age", label: "Edad" },
{ key: "email", label: "Correo electrónico" },
]}
data={[
{ name: "Ana", age: 19, email: "ana@example.com" },
{
name: "Luis Alberto Martínez",
age: 34,
email: "luis.martinez@correo-ejemplo.com",
},
{ name: "Carla", age: 27, email: "carla@example.com" },
]}
/>
</VistaComponente>
{/* SKELETON */}
<VistaComponente
title="⏳ Tabla en estado de carga"
description="El componente `TablaSkeleton` simula una tabla vacía mientras se cargan los datos."
>
<TablaSkeleton rows={3} columns={4} />
</VistaComponente>
{/* ROW KEY */}
<VistaComponente
title="🆔 Usar identificadores únicos con `rowKey`"
description="Proporciona una función para identificar cada fila de forma única."
>
<Table
headers={headers}
data={baseData.map((item, index) => ({ ...item, id: index }))}
rowKey={(row) => row.email}
/>
</VistaComponente>
{/* FILTRO */}
<VistaComponente
title="🔍 Filtro simple con input externo"
description="Filtra los datos con un input controlado."
>
<input
className="form-control mb-3"
placeholder="Buscar por nombre..."
value={search}
onChange={(e) => setSearch(e.target.value)}
/>
<Table headers={headers} data={filteredData} />
</VistaComponente>
{/* PROPS */}
<section className="mb-5">
<h2 className="h5"> Props del componente</h2>
<ul>
<li>
<code>headers</code>: Columnas de la tabla (con key, label, render,
width...)
</li>
<li>
<code>data</code>: Datos a mostrar.
</li>
<li>
<code>rowsPerPage?</code>: Número de filas por página (por defecto:
10).
</li>
<li>
<code>onRowClick?</code>: Función al hacer clic en una fila.
</li>
<li>
<code>rowKey?</code>: Función que retorna un ID único para cada
fila.
</li>
<li>
<code>className?</code>: Estilos personalizados (tabla, fila, celda,
header).
</li>
<li>
<code>controlledPage?</code>: Página actual controlada externamente.
</li>
<li>
<code>onPageChange?</code>: Callback para cambiar la página.
</li>
</ul>
</section>
</div>
);
}
+72
View File
@@ -0,0 +1,72 @@
import React from 'react';
/**
* Botón reutilizable con soporte para variantes de Bootstrap y personalización de íconos.
*
* @component
*/
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
/**
* Variante Bootstrap o personalizada. Ej: `primary`, `danger`, `mi-clase-custom`.
* @default "primary"
*/
variant?: string;
/**
* Si se usa variante `outline` (por ejemplo, `btn-outline-primary`).
* @default false
*/
outline?: boolean;
/**
* Ícono Bootstrap (`string`, como `check-circle`) o componente React.
*/
icon?: string | React.ReactNode;
/**
* Tamaño del botón: `sm` o `lg` para tamaños Bootstrap.
*/
size?: 'sm' | 'lg';
/**
* Clases adicionales personalizadas para el botón.
*/
className?: string;
}
/**
* Componente `Button` con estilos de Bootstrap y soporte para íconos.
*/
const Button: React.FC<ButtonProps> = ({
children,
type = 'button',
icon,
className = '',
variant = 'primary',
outline = false,
disabled = false,
size,
...rest
}) => {
const btnVariant = outline ? `btn-outline-${variant}` : `btn-${variant}`;
const sizeClass = size ? `btn-${size}` : '';
const combinedClasses = `btn ${btnVariant} ${sizeClass} ${className}`.trim();
return (
<button
type={type}
disabled={disabled}
className={combinedClasses}
{...rest}
>
{typeof icon === 'string' ? (
<i className={`me-2 bi bi-${icon}`}></i>
) : (
icon && <span className="me-2">{icon}</span>
)}
{children}
</button>
);
};
export default Button;
+58
View File
@@ -0,0 +1,58 @@
import React from "react";
export interface CheckboxOption<T = any> {
label: string;
value: T;
disabled?: boolean;
}
interface CheckboxOptionGroupProps<T = any> {
name: string;
options: CheckboxOption<T>[];
selectedValues: T[];
onChange: (value: T, checked: boolean) => void;
maxSelected?: number;
disabled?: boolean;
className?: string;
}
export default function CheckboxOptionGroup<T>({
name,
options,
selectedValues,
onChange,
maxSelected,
disabled = false,
className = "",
}: CheckboxOptionGroupProps<T>) {
const isSelected = (value: T) => selectedValues.includes(value);
return (
<div className={`d-flex flex-column gap-2 mb-3 ${className}`}>
{options.map((option, index) => {
const checked = isSelected(option.value);
const limitReached =
maxSelected !== undefined &&
selectedValues.length >= maxSelected &&
!checked;
return (
<label
key={index}
className="option list-group-item-action d-flex align-items-center"
>
<input
type="checkbox"
name={name}
className="form-check-input me-2"
disabled={disabled || option.disabled || limitReached}
checked={checked}
onChange={(e) => onChange(option.value, e.target.checked)}
/>
{option.label}
</label>
);
})}
</div>
);
}
+11 -4
View File
@@ -1,13 +1,20 @@
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
import { vscDarkPlus } from 'react-syntax-highlighter/dist/cjs/styles/prism';
export const CodeBlock = ({ code }: { code: string }) => (
<div className="position-relative">
<pre className="bg-dark text-white p-3 rounded overflow-auto mb-0">
<code>{code}</code>
</pre>
<SyntaxHighlighter
language="tsx"
style={vscDarkPlus}
customStyle={{ margin: 0, padding: '1rem', borderRadius: '0.5rem' }}
>
{code}
</SyntaxHighlighter>
<button
className="btn btn-sm btn-light position-absolute top-0 end-0 m-2"
onClick={() => navigator.clipboard.writeText(code)}
>
Copiar
<i className="bi bi-clipboard"></i>
</button>
</div>
);
+23
View File
@@ -0,0 +1,23 @@
interface IndiceContenidoProps {
items: {
id: string;
label: string;
}[];
}
export default function IndiceContenido({ items }: IndiceContenidoProps) {
return (
<aside className="bd-sidebar">
<p className="text-muted fs-5">On this page</p>
<nav className="bd-links">
<ul className="list-unstyled">
{items.map((item) => (
<li key={item.id}>
<a href={`#${item.id}`}>{item.label}</a>
</li>
))}
</ul>
</nav>
</aside>
);
}
+14 -34
View File
@@ -2,7 +2,7 @@ import React, { useState } from "react";
export interface InputProps
extends Omit<React.InputHTMLAttributes<HTMLInputElement>, "className"> {
label: string;
label?: string;
className?: {
container?: string;
label?: string;
@@ -13,13 +13,13 @@ export interface InputProps
/**
* Componente de entrada (`Input`) reutilizable en React.
*
* Este componente renderiza un campo de entrada (`<input>`) con una etiqueta y
* Este componente renderiza un campo de entrada (`<input>`) con una etiqueta opcional y
* funcionalidad para mostrar u ocultar contraseñas cuando el tipo es `password`.
* También permite personalizar clases CSS y es accesible mediante `aria-label` y `htmlFor`.
*
* @component
* @param {Object} props - Propiedades del componente.
* @param {string} props.label - Etiqueta visible asociada al campo de entrada.
* @param {string} [props.label] - Etiqueta visible asociada al campo de entrada.
* @param {Object} [props.className] - Clases CSS personalizables para distintos elementos.
* @param {string} [props.className.container] - Clases para el contenedor del input.
* @param {string} [props.className.label] - Clases para la etiqueta del input.
@@ -30,29 +30,6 @@ export interface InputProps
* @param {string} [props.value] - Valor del input (para uso controlado).
* @param {function} [props.onChange] - Manejador del cambio de valor del input.
* @param {React.InputHTMLAttributes<HTMLInputElement>} [rest] - Atributos adicionales compatibles con `<input>`.
*
* @example
* // Ejemplo de uso con contraseña:
* <Input
* label="Contraseña"
* name="password"
* type="password"
* className={{
* container: "mb-4",
* label: "text-sm font-bold",
* input: "border"
* }}
* />
*
* @example
* // Ejemplo de input controlado:
* <Input
* label="Correo electrónico"
* name="email"
* type="email"
* value={email}
* onChange={(e) => setEmail(e.target.value)}
* />
*/
export default function Input(props: InputProps) {
const { className, label, type, name, id, ...rest } = props;
@@ -60,18 +37,21 @@ export default function Input(props: InputProps) {
const isPassword = type === "password";
const inputType = isPassword && showPassword ? "text" : type;
const inputId = id || name || `input-${label.replace(/\s+/g, "-")}`;
const inputId =
id || name || (label ? `input-${label.replace(/\s+/g, "-")}` : undefined);
const togglePasswordVisibility = () => setShowPassword(!showPassword);
return (
<div className={className?.container || ""}>
<label
htmlFor={inputId}
className={`form-label ${className?.label || ""}`}
>
{label}
</label>
<div className={className?.container || "mb-3"}>
{label && inputId && (
<label
htmlFor={inputId}
className={`form-label ${className?.label || ""}`}
>
{label}
</label>
)}
<div className="input-group">
<input
id={inputId}
+47
View File
@@ -0,0 +1,47 @@
import React from "react";
import Link from "next/link";
const Navbar: React.FC = () => {
return (
<nav className="navbar navbar-expand-lg navbar-light bg-transparent fixed-top">
<div className="container">
<div className="mx-auto">
<ul className="navbar-nav gap-5">
<li className="nav-item">
<Link className="nav-link" href="/">
Inicio
</Link>
</li>
<li className="nav-item">
<Link className="nav-link" href="/table">
Table
</Link>
</li>
<li className="nav-item">
<Link className="nav-link" href="/input">
Input
</Link>
</li>
<li className="nav-item">
<Link className="nav-link" href="/pagination">
Pagination
</Link>
</li>
<li className="nav-item">
<Link className="nav-link" href="/options">
Options
</Link>
</li>
<li className="nav-item">
<Link className="nav-link" href="/select">
Select
</Link>
</li>
</ul>
</div>
</div>
</nav>
);
};
export default Navbar;
+28 -21
View File
@@ -1,54 +1,52 @@
import React, { useMemo } from 'react';
import React, { useMemo } from "react";
interface PaginationProps {
totalPages: number;
currentPage: number;
onPageChange: (page: number) => void;
delta?: number;
}
export default function Pagination({
currentPage,
onPageChange,
totalPages,
delta = 1,
}: PaginationProps) {
const handlePageChange = (page: number) => {
if (page >= 1 && page <= totalPages) {
if (page >= 1 && page <= totalPages && page !== currentPage) {
onPageChange(page);
}
};
// Lógica para mostrar paginación compacta cuando hay muchas páginas
const visiblePages = useMemo(() => {
const pages: number[] = [];
const delta = 0; // Cuántas páginas cercanas a la actual mostrar
const pages: (number | -1)[] = [];
if (totalPages <= 5) {
if (totalPages <= 5 + delta * 2) {
return Array.from({ length: totalPages }, (_, i) => i + 1);
}
const leftBoundary = Math.max(1, currentPage - delta);
const rightBoundary = Math.min(totalPages, currentPage + delta);
const left = Math.max(2, currentPage - delta);
const right = Math.min(totalPages - 1, currentPage + delta);
if (leftBoundary > 2) {
pages.push(1, -1); // -1 es un marcador de "..."
}
pages.push(1);
if (left > 2) pages.push(-1);
for (let i = leftBoundary; i <= rightBoundary; i++) {
for (let i = left; i <= right; i++) {
pages.push(i);
}
if (rightBoundary < totalPages - 1) {
pages.push(-1, totalPages);
}
if (right < totalPages - 1) pages.push(-1);
pages.push(totalPages);
return pages;
}, [currentPage, totalPages]);
}, [currentPage, totalPages, delta]);
return (
<nav aria-label="Page navigation">
<ul className="pagination gap-1 m-0">
<ul className="pagination gap-1">
{/* Botón Anterior */}
<li className={`page-item ${currentPage === 1 ? 'disabled' : ''}`}>
<li className={`page-item ${currentPage === 1 ? "disabled" : ""}`}>
<button
className="page-link shadow-sm rounded border-0 bg-white"
onClick={() => handlePageChange(currentPage - 1)}
@@ -68,9 +66,16 @@ export default function Pagination({
</span>
</li>
) : (
<li key={page} className="page-item">
<li
key={page}
className={`page-item ${currentPage === page ? "active" : ""}`}
>
<button
className={`page-link shadow-sm rounded border-0 ${currentPage === page ? 'active' : 'bg-white text-primary'}`}
className={`page-link shadow-sm rounded border-0 ${
currentPage === page
? ""
: "bg-white text-primary"
}`}
onClick={() => handlePageChange(page)}
>
{page}
@@ -81,7 +86,9 @@ export default function Pagination({
{/* Botón Siguiente */}
<li
className={`page-item ${currentPage === totalPages ? 'disabled' : ''}`}
className={`page-item ${
currentPage === totalPages ? "disabled" : ""
}`}
>
<button
className="page-link shadow-sm rounded border-0 bg-white"
+46
View File
@@ -0,0 +1,46 @@
import React from "react";
export interface RadioOption<T = any> {
label: string;
value: T;
disabled?: boolean;
}
interface RadioOptionGroupProps<T = any> {
name: string;
options: RadioOption<T>[];
selectedValue?: T;
onChange: (value: T) => void;
disabled?: boolean;
className?: string;
}
export default function RadioOptionGroup<T>({
name,
options,
selectedValue,
onChange,
disabled = false,
className = "",
}: RadioOptionGroupProps<T>) {
return (
<div className={`d-flex flex-column gap-2 mb-3 ${className}`}>
{options.map((option, index) => (
<label
key={index}
className="option list-group-item-action d-flex align-items-center"
>
<input
type="radio"
name={name}
className="form-check-input me-2"
disabled={disabled || option.disabled}
checked={option.value === selectedValue}
onChange={() => onChange(option.value)}
/>
{option.label}
</label>
))}
</div>
);
}
+103
View File
@@ -0,0 +1,103 @@
import React, { useId } from 'react';
interface Option {
value: string | number;
label: string;
}
interface SelectProps
extends Omit<React.SelectHTMLAttributes<HTMLSelectElement>, 'className'> {
label: string;
className?: {
container?: string;
label?: string;
input?: string;
};
options: Option[];
placeholder?: string;
}
/**
* Componente `Select` reutilizable en React.
*
* Este componente renderiza un elemento `<select>` con una etiqueta visible,
* opciones personalizadas y estilos configurables por clase. Es compatible
* con el uso controlado (`value`) o no controlado (`defaultValue`).
*
* Además, genera automáticamente un `id` si no se proporciona, manteniendo
* buenas prácticas de accesibilidad.
*
* @component
* @example
* // Uso básico
* <Select
* label="País"
* name="pais"
* options={[
* { value: 'mx', label: 'México' },
* { value: 'us', label: 'Estados Unidos' },
* { value: 'ca', label: 'Canadá' }
* ]}
* placeholder="Selecciona un país"
* defaultValue=""
* />
*
* @param {Object} props - Propiedades del componente.
* @param {string} props.label - Etiqueta visible asociada al campo.
* @param {Array<{ value: string | number, label: string }>} props.options - Lista de opciones disponibles en el select.
* @param {string} [props.placeholder] - Opción deshabilitada inicial que actúa como guía o título (si se proporciona).
* @param {string} [props.name] - Nombre del campo (útil en formularios).
* @param {string} [props.id] - ID del campo; si no se proporciona, se genera automáticamente.
* @param {string | number} [props.defaultValue] - Valor por defecto del select (modo no controlado).
* @param {string | number} [props.value] - Valor actual del select (modo controlado).
* @param {Object} [props.className] - Objeto para aplicar clases CSS personalizadas.
* @param {string} [props.className.container] - Clases del contenedor principal.
* @param {string} [props.className.label] - Clases de la etiqueta.
* @param {string} [props.className.input] - Clases del `<select>`.
* @param {React.SelectHTMLAttributes<HTMLSelectElement>} [rest] - Cualquier otra prop válida para `<select>`.
*
* @returns {JSX.Element} Un elemento `<select>` con sus opciones y etiqueta asociada.
*/
const Select: React.FC<SelectProps> = ({
label,
className,
options,
placeholder,
id,
name,
defaultValue,
value,
...rest
}) => {
const generatedId = useId();
const selectId = id || name || generatedId;
return (
<div className={`mb-3 ${className?.container ?? ''}`}>
<label htmlFor={selectId} className={`form-label ${className?.label ?? ''}`}>
{label}
</label>
<select
id={selectId}
name={name}
className={`form-select bg-white ${className?.input ?? ''}`}
defaultValue={defaultValue ?? (value === undefined ? '' : undefined)}
value={value}
{...rest}
>
{placeholder && (
<option value="" disabled>
{placeholder}
</option>
)}
{options.map((option) => (
<option key={option.value.toString()} value={option.value}>
{option.label}
</option>
))}
</select>
</div>
);
};
export default Select;
+1 -1
View File
@@ -169,7 +169,7 @@ interface TablaSkeletonProps {
columns?: number;
}
export function TablaSkeleton({ rows = 5, columns = 5 }: TablaSkeletonProps) {
export function TableSkeleton({ rows = 5, columns = 5 }: TablaSkeletonProps) {
return (
<table className="e-table placeholder-glow">
<thead>
+11 -5
View File
@@ -1,11 +1,13 @@
import React, { useState } from "react";
import { CodeBlock } from "./code-block";
import Button from "./button";
import { useState } from "react";
interface VistaComponenteProps {
title: string;
description?: string;
code?: string;
children: React.ReactNode;
id: string;
}
export default function VistaComponente({
@@ -13,11 +15,12 @@ export default function VistaComponente({
description,
code,
children,
id,
}: VistaComponenteProps) {
const [showCode, setShowCode] = useState(false);
return (
<section className="mb-5">
<section className="mb-5" id={id}>
<h2 className="h5 mb-2">{title}</h2>
{description && <p className="text-muted mb-2">{description}</p>}
@@ -26,12 +29,15 @@ export default function VistaComponente({
{code && (
<div className="d-flex flex-column gap-2">
<button
className="btn btn-sm btn-outline-secondary align-self-start"
<Button
size="sm"
outline
variant="secondary"
className="align-self-start mb-3"
onClick={() => setShowCode((prev) => !prev)}
>
{showCode ? "Ocultar código" : "Ver código y copiar"}
</button>
</Button>
{showCode && <CodeBlock code={code} />}
</div>
)}
+23
View File
@@ -0,0 +1,23 @@
export const inputBasicExample = `<Input
label="Nombre"
placeholder="Ingresa tu nombre"
value={value}
onChange={(e) => setValue(e.target.value)}
/>`;
export const inputPasswordExample = `<Input
type="password"
label="Contraseña"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>`;
export const inputStylesExample = `<Input
label="Correo"
placeholder="ejemplo@correo.com"
className={{
container: "mb-4",
label: "text-primary fw-bold",
input: "border-success",
}}
/>`;
+59
View File
@@ -0,0 +1,59 @@
export const radioBasicExample = `
<RadioOptionGroup
name="grupo-radio"
options={[
{ label: "Opción 1", value: "op1" },
{ label: "Opción 2", value: "op2" },
{ label: "Opción 3", value: "op3" },
]}
selectedValue={selectedRadio}
onChange={setSelectedRadio}
/>
`;
export const radioDisabledExample = `
<RadioOptionGroup
name="grupo-radio-disabled"
options={[
{ label: "Activo", value: "a" },
{ label: "Inactivo", value: "b", disabled: true },
]}
selectedValue="a"
onChange={() => {}}
/>
`;
export const checkboxBasicExample = `
<CheckboxOptionGroup
name="grupo-checkbox"
options={[
{ label: "Activo", value: "a" },
{ label: "Inactivo", value: "b" },
{ label: "Pendiente", value: "c" },
]}
selectedValues={selectedCheckboxes}
onChange={(value, checked) => {
setSelectedCheckboxes((prev) =>
checked ? [...prev, value] : prev.filter((v) => v !== value)
);
}}
/>
`;
export const checkboxLimitExample = `
<CheckboxOptionGroup
name="grupo-checkbox-limit"
options={[
{ label: "Activo", value: "a" },
{ label: "Inactivo", value: "b" },
{ label: "Pendiente", value: "c" },
]}
selectedValues={selectedCheckboxes}
onChange={(value, checked) => {
setSelectedCheckboxes((prev) =>
checked ? [...prev, value] : prev.filter((v) => v !== value)
);
}}
maxSelected={2}
/>
`;
@@ -0,0 +1,16 @@
export const paginationBasic = `
<Pagination
totalPages={5}
currentPage={currentPage}
onPageChange={setCurrentPage}
/>
`;
export const paginationWithDelta = `
<Pagination
totalPages={20}
currentPage={currentPage}
onPageChange={setCurrentPage}
delta={2}
/>
`;
+32
View File
@@ -0,0 +1,32 @@
export const countrySelectSnippet = `
<Select
label="País"
name="pais"
placeholder="Selecciona un país"
options={[
{ value: "mx", label: "México" },
{ value: "us", label: "Estados Unidos" },
{ value: "ca", label: "Canadá" },
]}
value={pais}
onChange={(e) => setPais(e.target.value)}
/>
`;
export const languageSelectSnippet = `
<Select
label="Idioma"
name="idioma"
placeholder="Selecciona un idioma"
options={[
{ value: "es", label: "Español" },
{ value: "en", label: "Inglés" },
{ value: "fr", label: "Francés" },
]}
className={{
container: "mb-4",
label: "text-primary fw-bold",
input: "border border-success",
}}
/>
`;
@@ -0,0 +1,30 @@
export const tableAutoPagination = `import React from "react";
import Table, { Header } from "@/components/table";
export default function Page() {
const headers: Header<{
name: string;
age: number;
email: string;
}>[] = [
{ key: "name", label: "Nombre"},
{ key: "age", label: "Edad" },
{ key: "email", label: "Correo" },
];
const baseData = [
{ name: "Juan", age: 20, email: "juan@example.com" },
{ name: "María", age: 25, email: "maria@example.com" },
{ name: "Pedro", age: 30, email: "pedro@example.com" },
{ name: "Luisa", age: 28, email: "luisa@example.com" },
];
return (
<Table
headers={headers}
data={baseData}
rowsPerPage={2}
/>
)
}
`;
+35 -3
View File
@@ -1,4 +1,36 @@
// src/code-snippets/table-basic.ts
export const tableBasicExample = `
<Table headers={headers} data={baseData} />
export const tableBasicExample = `import React from "react";
import Table, { Header } from "@/components/table";
export default function Page() {
const headers: Header<{
name: string;
age: number;
email: string;
}>[] = [
{ key: "name", label: "Nombre"},
{ key: "age", label: "Edad" },
{ key: "email", label: "Correo" },
];
const baseData = [
{ name: "Juan", age: 20, email: "juan@example.com" },
{ name: "María", age: 25, email: "maria@example.com" },
{ name: "Pedro", age: 30, email: "pedro@example.com" },
{ name: "Luisa", age: 28, email: "luisa@example.com" },
];
return (
<Table headers={headers} data={baseData} />
)
}
`;
export const tableSkeletonExample = `import React from "react";
import { TableSkeleton } from "@/components/table";
export default function Page() {
return (
<TableSkeleton rows={3} columns={4} />
)
}
`;
+27
View File
@@ -0,0 +1,27 @@
export const tableClickable = `export default function Page() {
const headers: Header<{
name: string;
age: number;
email: string;
}>[] = [
{ key: "name", label: "Nombre"},
{ key: "age", label: "Edad" },
{ key: "email", label: "Correo" },
];
const baseData = [
{ name: "Juan", age: 20, email: "juan@example.com" },
{ name: "María", age: 25, email: "maria@example.com" },
{ name: "Pedro", age: 30, email: "pedro@example.com" },
{ name: "Luisa", age: 28, email: "luisa@example.com" },
];
return (
<Table
headers={headers}
data={baseData}
onRowClick={(row) => alert(\`Seleccionaste a \${row.name}\`)}
/>
)
}
`;
+23
View File
@@ -0,0 +1,23 @@
export const tableColumnWidth = `interface RowData {
name: string;
age: number;
email: string;
}
export default function Page() {
const headers: Header<RowData>[] = [
{ key: "name", label: "Nombre", width: "50%" },
{ key: "age", label: "Edad", width: "15%" },
{ key: "email", label: "Correo electrónico", width: "35%" },
];
const data: RowData[] = [
{ name: "Juan", age: 20, email: "juan@example.com" },
{ name: "María", age: 25, email: "maria@example.com" },
{ name: "Pedro", age: 30, email: "pedro@example.com" },
{ name: "Luisa", age: 28, email: "luisa@example.com" },
];
return <Table headers={headers} data={data} />;
}
`;
@@ -0,0 +1,37 @@
export const tableCustomCellRender = `
interface RowData {
name: string;
age: number;
email: string;
acciones?: null;
}
export default function Page() {
const headers: Header<RowData>[] = [
{ key: "name", label: "Nombre", width: "30%" },
{ key: "age", label: "Edad", width: "15%" },
{ key: "email", label: "Correo", width: "40%" },
{
key: "acciones",
label: "Acciones",
width: "15%",
render: (_, row) => (
<button
className="btn btn-sm btn-primary"
onClick={() => alert(row.email)}
>
Ver correo
</button>
),
},
];
const data = [
{ name: "Juan", age: 20, email: "juan@example.com", acciones: null },
{ name: "María", age: 25, email: "maria@example.com", acciones: null },
{ name: "Pedro", age: 30, email: "pedro@example.com", acciones: null },
];
return <Table headers={headers} data={data} />;
}
`;
@@ -0,0 +1,50 @@
export const tableCustomPagination = `interface RowData {
name: string;
age: number;
email: string;
}
export default function Page() {
const headers: Header<RowData>[] = [
{ key: "name", label: "Nombre" },
{ key: "age", label: "Edad" },
{ key: "email", label: "Correo" },
];
const [currentPage, setCurrentPage] = useState(1);
const [data, setData] = useState<any[]>([]);
const [loading, setLoading] = useState(false);
const rowsPerPage = 2;
useEffect(() => {
setLoading(true);
fetch(\`https://api.example.com/users?page=\${currentPage}&limit=\${rowsPerPage}\`)
.then((res) => res.json())
.then((json) => {
setData(json.data);
})
.catch((err) => {
console.error("Error fetching data:", err);
setData([]);
})
.finally(() => setLoading(false));
}, [currentPage]);
return (
<>
{loading ? (
<TablaSkeleton rows={2} columns={3} />
) : (
<Table
headers={headers}
data={data}
rowsPerPage={rowsPerPage}
controlledPage={currentPage}
onPageChange={setCurrentPage}
/>
)}
</>
)
}
`;
+42
View File
@@ -0,0 +1,42 @@
export const tableFilter = `import React, { useState } from "react";
import Table, { Header } from "@/components/table";
import Input from "@/components/input";
interface RowData {
name: string;
age: number;
email: string;
}
export default function Page() {
const headers: Header<RowData>[] = [
{ key: "name", label: "Nombre", width: "30%" },
{ key: "age", label: "Edad", width: "15%" },
{ key: "email", label: "Correo", width: "40%" },
];
const data: RowData[] = [
{ name: "Juan", age: 20, email: "juan@example.com" },
{ name: "María", age: 25, email: "maria@example.com" },
{ name: "Pedro", age: 30, email: "pedro@example.com" },
{ name: "Luisa", age: 28, email: "luisa@example.com" },
];
const [search, setSearch] = useState("");
const filteredData = data.filter((item) =>
item.name.toLowerCase().includes(search.toLowerCase())
);
return (
<>
<Input
placeholder="Buscar por nombre..."
value={search}
onChange={(e) => setSearch(e.target.value)}
/>
<Table headers={headers} data={filteredData} />
</>
);
}
`;
+56
View File
@@ -10,3 +10,59 @@
@extend .text-white;
}
}
.unlink {
@extend .text-decoration-none;
@extend .text-dark;
}
.flex-center {
@extend .d-flex;
@extend .justify-content-center;
@extend .align-items-center;
}
.option {
@extend .box;
@extend .py-2;
cursor: pointer;
transition: all 0.3s;
&:has(input:checked) {
@extend .bg-primary;
@extend .bg-opacity-75;
@extend .text-white;
}
input[type='radio']:checked {
border: 1px solid #ffffff;
@extend .bg-primary;
@extend .bg-opacity-75;
}
&:hover {
background-color: #f1f1f1 !important;
}
&:has(input:disabled) {
@extend .bg-light;
@extend .text-muted;
cursor: not-allowed;
}
}
.border-top-azul {
position: relative;
&::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 5px;
background: $azul;
border-radius: 5px 5px 0 0;
}
}
+4 -12
View File
@@ -3,7 +3,7 @@
.bg-gradient-#{$color-name}-top {
background-image: linear-gradient(
to top,
$color-value,
rgba($color-value, 0.5),
$body-bg
);
}
@@ -11,24 +11,16 @@
.bg-gradient-#{$color-name}-bottom {
background-image: linear-gradient(
to bottom,
$color-value,
rgba($color-value, 0.3),
$body-bg
);
}
.bg-gradient-#{$color-name}-left {
background-image: linear-gradient(
to left,
$color-value,
$body-bg
);
background-image: linear-gradient(to left, rgba($color-value, 0.5), $body-bg);
}
.bg-gradient-#{$color-name}-right {
background-image: linear-gradient(
to right,
$color-value,
$body-bg
);
background-image: linear-gradient(to right, rgba($color-value, 0.5), $body-bg);
}
}
+43 -14
View File
@@ -1,28 +1,57 @@
@import 'bootstrap/scss/functions';
@import "bootstrap/scss/functions";
@import 'variables';
@import "variables";
@import 'bootstrap/scss/variables';
@import 'bootstrap/scss/variables-dark';
@import "bootstrap/scss/variables";
@import "bootstrap/scss/variables-dark";
// Include any default map overrides here
$azul: #003d79;
$dorado: #d59f0f;
$custom-colors: (
'azul': $azul,
'dorado': $dorado,
"azul": $azul,
"dorado": $dorado,
);
$theme-colors: map-merge($theme-colors, $custom-colors);
@import 'gradients';
@import 'extends';
@import 'table';
@import 'utilities';
@import "gradients";
@import "extends";
@import "table";
@import "utilities";
@import 'bootstrap/scss/maps';
@import 'bootstrap/scss/mixins';
@import 'bootstrap/scss/bootstrap';
@import "bootstrap/scss/maps";
@import "bootstrap/scss/mixins";
@import "bootstrap/scss/bootstrap";
//Add additional custom code here
@media (min-width: 992px) {
.bd-layout {
display: grid;
grid-template-areas: "sidebar main";
grid-template-columns: 1fr 5fr;
gap: 1.5rem;
}
}
.bd-sidebar {
grid-area: sidebar;
position: sticky;
top: ($spacer * 3);
height: calc(100vh - 6rem);
}
ul.list-unstyled li {
padding: 0.5rem 0;
a {
color: $azul;
transition: color 0.2s;
text-decoration: none;
&:hover {
color: $dorado;
}
}
}
//Add additional custom code here