diff --git a/package-lock.json b/package-lock.json
index ebb1a85..05fd0ae 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -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",
diff --git a/package.json b/package.json
index 0f43efe..09c1b1e 100644
--- a/package.json
+++ b/package.json
@@ -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",
diff --git a/public/input.png b/public/input.png
index 8eae39b..099f75c 100644
Binary files a/public/input.png and b/public/input.png differ
diff --git a/public/options.png b/public/options.png
new file mode 100644
index 0000000..b1db4b8
Binary files /dev/null and b/public/options.png differ
diff --git a/public/pagination.png b/public/pagination.png
new file mode 100644
index 0000000..dc0bc41
Binary files /dev/null and b/public/pagination.png differ
diff --git a/public/select.png b/public/select.png
new file mode 100644
index 0000000..82afd52
Binary files /dev/null and b/public/select.png differ
diff --git a/public/table.png b/public/table.png
index fb3418b..3d1d70c 100644
Binary files a/public/table.png and b/public/table.png differ
diff --git a/src/app/(components)/button/page.tsx b/src/app/(components)/button/page.tsx
new file mode 100644
index 0000000..f7d1e20
--- /dev/null
+++ b/src/app/(components)/button/page.tsx
@@ -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 (
+ <>
+
+
+
+
+ 🧩 Componente: Button
+
+
+
+ El componente <Button /> permite crear botones con
+ estilos de Bootstrap, variantes personalizables, íconos opcionales y
+ soporte completo para props de HTML estándar.
+
+
+
+
+
alert("¡Hola mundo!")}>
+ Clic aquí
+`}
+ >
+ alert("¡Hola mundo!")} className="mb-3">Clic aquí
+
+
+
+ Guardar
+`}
+ >
+
+ Guardar
+
+
+
+
+
+ >
+ );
+}
diff --git a/src/app/(components)/input/page.tsx b/src/app/(components)/input/page.tsx
new file mode 100644
index 0000000..c38e1d8
--- /dev/null
+++ b/src/app/(components)/input/page.tsx
@@ -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 (
+ <>
+
+
+
+ 🔤 Componente: Input
+
+
+
+ El componente <Input /> es un campo de entrada
+ reutilizable con soporte para etiquetas, tipos como{" "}
+ password, accesibilidad mejorada, y personalización de
+ estilos. Además, incluye un botón para mostrar/ocultar contraseñas
+ cuando el tipo es password.
+
+
+
+
+
+ setValue(e.target.value)}
+ />
+
+
+
+ setPassword(e.target.value)}
+ />
+
+
+
+
+
+
+
+
+ >
+ );
+}
diff --git a/src/app/(components)/layout.tsx b/src/app/(components)/layout.tsx
new file mode 100644
index 0000000..cb55daf
--- /dev/null
+++ b/src/app/(components)/layout.tsx
@@ -0,0 +1,13 @@
+import React from "react";
+
+export default function ComponentsLayout({
+ children,
+}: {
+ children: React.ReactNode;
+}) {
+ return (
+
+ {children}
+
+ );
+}
diff --git a/src/app/(components)/options/page.tsx b/src/app/(components)/options/page.tsx
new file mode 100644
index 0000000..4d9a7ff
--- /dev/null
+++ b/src/app/(components)/options/page.tsx
@@ -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([
+ "a",
+ ]);
+ const [checkboxLimitSelected, setCheckboxLimitSelected] = useState(
+ []
+ );
+
+ const radioOptions: RadioOption[] = [
+ { label: "Opción 1", value: "op1" },
+ { label: "Opción 2", value: "op2" },
+ { label: "Opción 3", value: "op3" },
+ ];
+
+ const checkboxOptions: CheckboxOption[] = [
+ { label: "Activo", value: "a" },
+ { label: "Inactivo", value: "b" },
+ { label: "Pendiente", value: "c" },
+ ];
+
+ const handleCheckboxChange = (
+ value: string,
+ checked: boolean,
+ setValues: React.Dispatch>
+ ) => {
+ setValues((prev) =>
+ checked ? [...prev, value] : prev.filter((v) => v !== value)
+ );
+ };
+
+ return (
+ <>
+
+
+
+ Componentes: RadioOptionGroup y{" "}
+ CheckboxOptionGroup
+
+
+
+ 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{" "}
+ @/styles/sass/_extneds.scss y es el selector{" "}
+ .option.
+
+
+
+
+
+
+
+
+
+
+
+ {}}
+ />
+
+
+
+
+ handleCheckboxChange(value, checked, setCheckboxBasicSelected)
+ }
+ />
+
+
+
+
+ handleCheckboxChange(value, checked, setCheckboxLimitSelected)
+ }
+ maxSelected={2}
+ />
+
+
+
+ Props comunes
+
+
+ name: Nombre del grupo de inputs.
+
+
+ options: Lista de opciones con label,{" "}
+ value y opcionalmente disabled.
+
+
+ className?: Clases CSS adicionales.
+
+
+
+
+ Props de RadioOptionGroup
+
+
+
+ selectedValue?: Valor seleccionado actualmente.
+
+
+ onChange: Función que se ejecuta al cambiar de
+ opción.
+
+
+ disabled?: Deshabilita todo el grupo.
+
+
+
+
+ ☑️ Props de CheckboxOptionGroup
+
+
+
+ selectedValues: Lista de valores seleccionados.
+
+
+ onChange: Función que se ejecuta al marcar o
+ desmarcar una opción.
+
+
+ maxSelected?: Número máximo de opciones
+ seleccionables.
+
+
+ disabled?: Deshabilita todo el grupo.
+
+
+
+
+ >
+ );
+}
diff --git a/src/app/(components)/pagination/page.tsx b/src/app/(components)/pagination/page.tsx
new file mode 100644
index 0000000..5609caf
--- /dev/null
+++ b/src/app/(components)/pagination/page.tsx
@@ -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 (
+ <>
+
+
+
+ 📄 Componente: Pagination
+
+
+
+ El componente <Pagination /> permite navegar entre
+ páginas de forma controlada. Es completamente reutilizable y permite
+ mostrar botones de anterior/siguiente y paginación con saltos.
+
+
+
+
+
+
+ {/* Ejemplo básico */}
+
+
+
+ Página actual: {currentPage1}
+
+
+
+ {/* Ejemplo con delta */}
+
+
+
+ Página actual: {currentPage2}
+
+
+
+ {/* Props */}
+
+ ⚙️ Props del componente
+
+
+ totalPages: number — Total de páginas
+ disponibles.
+
+
+ currentPage: number — Página actual.
+
+
+ onPageChange: (page: number) => void —
+ Callback que se ejecuta al cambiar de página.
+
+
+ delta?: number — Número de páginas vecinas
+ visibles (por defecto: 1).
+
+
+ className?: string — Clases CSS
+ personalizadas para el contenedor.
+
+
+
+
+ >
+ );
+}
diff --git a/src/app/(components)/select/page.tsx b/src/app/(components)/select/page.tsx
new file mode 100644
index 0000000..386aae1
--- /dev/null
+++ b/src/app/(components)/select/page.tsx
@@ -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 (
+ <>
+
+
+
+ 📑 Componente: Select
+
+
+
+ El componente <Select /> permite mostrar una lista
+ desplegable con opciones personalizables, soporta etiquetas, estilos
+ por clase, y es compatible con formularios.
+
+
+
+
+
+ setPais(e.target.value)}
+ />
+
+
+
+
+
+
+
+ ⚙️ Props del componente
+
+
+ label: string – Etiqueta visible del campo.
+
+
+
+ options: { value: string | number; label: string
+ }[]
+ {" "}
+ – Opciones a mostrar.
+
+
+ placeholder?: string – Texto inicial deshabilitado.
+
+
+ name?: string – Nombre del campo.
+
+
+ id?: string – ID del campo (se genera si no se
+ proporciona).
+
+
+ defaultValue?: string | number – Valor por defecto
+ (modo no controlado).
+
+
+ value?: string | number – Valor actual del select
+ (modo controlado).
+
+
+ className?: { container?, label?, input? }{" "}
+ – Clases personalizadas.
+
+
+ ...rest – Cualquier otra prop válida de{" "}
+ <select>.
+
+
+
+
+ >
+ );
+}
diff --git a/src/app/(components)/table/page.tsx b/src/app/(components)/table/page.tsx
new file mode 100644
index 0000000..2831f8d
--- /dev/null
+++ b/src/app/(components)/table/page.tsx
@@ -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 (
+ <>
+
+
+
+ Componente: Table
+
+
+
+ El componente <Table /> 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{" "}
+ @/styles/sass/_table.scss
+
+
+
+
+
+
+
+
+
+
+ {/* SKELETON */}
+
+
+
+
+
+ alert(`Seleccionaste a ${row.name}`)}
+ />
+
+
+
+
+
+
+
+
+
+
+
+ (
+ alert(row.email)}
+ >
+ Ver correo
+
+ ),
+ },
+ ]}
+ data={baseData.map((d) => ({ ...d, acciones: null }))}
+ />
+
+
+
+
+
+
+ {/* FILTRO */}
+
+ setSearch(e.target.value)}
+ />
+
+
+
+
+ ({ ...item, id: index }))}
+ rowKey={(row) => row.email}
+ />
+
+
+ {/* PROPS */}
+
+ Props del componente
+
+
+
+ Prop
+ Tipo
+ Descripción
+
+
+
+
+
+ headers
+
+
+ Header<T>[]
+
+
+ Columnas de la tabla. Cada header puede tener key, label,
+ width, y una función render.
+
+
+
+
+ data
+
+
+ T[]
+
+ Datos a mostrar en la tabla.
+
+
+
+ rowsPerPage?
+
+
+ number
+
+
+ Número de filas por página (por defecto: 10).
+
+
+
+
+ onRowClick?
+
+
+ (row: T) => void
+
+ Función que se ejecuta al hacer clic en una fila.
+
+
+
+ rowKey?
+
+
+ (row: T) => string | number
+
+
+ Función para obtener un identificador único de cada fila
+ (evita usar index).
+
+
+
+
+ className?
+
+
+ {"{ table?, row?, cell?, header? }"}
+
+
+ Clases personalizadas para la tabla, filas, celdas y
+ encabezados.
+
+
+
+
+ controlledPage?
+
+
+ number
+
+ Página actual controlada desde el componente padre.
+
+
+
+ onPageChange?
+
+
+ (page: number) => void
+
+ Función callback que se dispara al cambiar de página.
+
+
+
+
+
+ >
+ );
+}
diff --git a/src/app/input/page.tsx b/src/app/input/page.tsx
deleted file mode 100644
index 77f1452..0000000
--- a/src/app/input/page.tsx
+++ /dev/null
@@ -1,122 +0,0 @@
-"use client";
-
-import React from "react";
-import Input from "@/components/input";
-
-export default function Page() {
- return (
-
-
- 📦 Componente: Input
-
-
-
- El componente {` `} 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.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- );
-}
diff --git a/src/app/layout.tsx b/src/app/layout.tsx
index b64257c..53df080 100644
--- a/src/app/layout.tsx
+++ b/src/app/layout.tsx
@@ -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 (
+
{children}
diff --git a/src/app/page.tsx b/src/app/page.tsx
index 0f6d9b4..98bf2e4 100644
--- a/src/app/page.tsx
+++ b/src/app/page.tsx
@@ -6,28 +6,40 @@ import React from "react";
export default function Page() {
return (
-
-
-
-
-
-
- Libreria de componentes de CIDWA
-
+
+
+
-
+
Libreria de componentes de CIDWA
+
+
diff --git a/src/app/table/page.tsx b/src/app/table/page.tsx
deleted file mode 100644
index 6b70e07..0000000
--- a/src/app/table/page.tsx
+++ /dev/null
@@ -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 (
-
-
- 📊 Componente: Table
-
-
-
- El componente <Table /> permite mostrar datos en
- forma tabular, con funcionalidades como ordenamiento, paginación,
- renderizado personalizado, clic en filas y un estado de carga esqueleto.
-
-
-
-
- {/* USO BÁSICO */}
-
-
-
-
- {/* FILAS CLICABLES */}
-
- alert(`Seleccionaste a ${row.name}`)}
- />
-
-
- {/* PAGINACIÓN */}
-
-
-
-
- {/* CONTROL DE PÁGINA EXTERNO */}
-
-
-
-
- {/* RENDER PERSONALIZADO */}
-
- (
- alert(row.email)}
- >
- Ver correo
-
- ),
- },
- ]}
- data={baseData.map((d) => ({ ...d, acciones: null }))}
- />
-
-
- {/* PERSONALIZACIÓN DE ANCHO */}
-
-
-
-
- {/* ANCHO AUTOMÁTICO */}
-
-
-
-
- {/* SKELETON */}
-
-
-
-
- {/* ROW KEY */}
-
- ({ ...item, id: index }))}
- rowKey={(row) => row.email}
- />
-
-
- {/* FILTRO */}
-
- setSearch(e.target.value)}
- />
-
-
-
- {/* PROPS */}
-
- ⚙️ Props del componente
-
-
- headers: Columnas de la tabla (con key, label, render,
- width...)
-
-
- data: Datos a mostrar.
-
-
- rowsPerPage?: Número de filas por página (por defecto:
- 10).
-
-
- onRowClick?: Función al hacer clic en una fila.
-
-
- rowKey?: Función que retorna un ID único para cada
- fila.
-
-
- className?: Estilos personalizados (tabla, fila, celda,
- header).
-
-
- controlledPage?: Página actual controlada externamente.
-
-
- onPageChange?: Callback para cambiar la página.
-
-
-
-
- );
-}
diff --git a/src/components/button.tsx b/src/components/button.tsx
new file mode 100644
index 0000000..dd46dc3
--- /dev/null
+++ b/src/components/button.tsx
@@ -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 {
+ /**
+ * 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 = ({
+ 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 (
+
+ {typeof icon === 'string' ? (
+
+ ) : (
+ icon && {icon}
+ )}
+ {children}
+
+ );
+};
+
+export default Button;
diff --git a/src/components/checkbox-option-group.tsx b/src/components/checkbox-option-group.tsx
new file mode 100644
index 0000000..66720ac
--- /dev/null
+++ b/src/components/checkbox-option-group.tsx
@@ -0,0 +1,58 @@
+import React from "react";
+
+export interface CheckboxOption {
+ label: string;
+ value: T;
+ disabled?: boolean;
+}
+
+interface CheckboxOptionGroupProps {
+ name: string;
+ options: CheckboxOption[];
+ selectedValues: T[];
+ onChange: (value: T, checked: boolean) => void;
+ maxSelected?: number;
+ disabled?: boolean;
+ className?: string;
+}
+
+export default function CheckboxOptionGroup({
+ name,
+ options,
+ selectedValues,
+ onChange,
+ maxSelected,
+ disabled = false,
+ className = "",
+}: CheckboxOptionGroupProps) {
+ const isSelected = (value: T) => selectedValues.includes(value);
+
+ return (
+
+ {options.map((option, index) => {
+ const checked = isSelected(option.value);
+ const limitReached =
+ maxSelected !== undefined &&
+ selectedValues.length >= maxSelected &&
+ !checked;
+
+ return (
+
+ onChange(option.value, e.target.checked)}
+ />
+ {option.label}
+
+ );
+ })}
+
+ );
+}
diff --git a/src/components/code-block.tsx b/src/components/code-block.tsx
index ddfa8dd..bdf4c1e 100644
--- a/src/components/code-block.tsx
+++ b/src/components/code-block.tsx
@@ -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 }) => (
-
- {code}
-
+
+ {code}
+
navigator.clipboard.writeText(code)}
>
- Copiar
+
);
diff --git a/src/components/content-index.tsx b/src/components/content-index.tsx
new file mode 100644
index 0000000..069d313
--- /dev/null
+++ b/src/components/content-index.tsx
@@ -0,0 +1,23 @@
+interface IndiceContenidoProps {
+ items: {
+ id: string;
+ label: string;
+ }[];
+}
+
+export default function IndiceContenido({ items }: IndiceContenidoProps) {
+ return (
+
+ );
+}
diff --git a/src/components/input.tsx b/src/components/input.tsx
index 87bb9d3..2b32e67 100644
--- a/src/components/input.tsx
+++ b/src/components/input.tsx
@@ -2,7 +2,7 @@ import React, { useState } from "react";
export interface InputProps
extends Omit, "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 (` `) con una etiqueta y
+ * Este componente renderiza un campo de entrada (` `) 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} [rest] - Atributos adicionales compatibles con ` `.
- *
- * @example
- * // Ejemplo de uso con contraseña:
- *
- *
- * @example
- * // Ejemplo de input controlado:
- * 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 (
-
-
- {label}
-
+
+ {label && inputId && (
+
+ {label}
+
+ )}
{
+ return (
+
+
+
+
+
+
+ Inicio
+
+
+
+
+ Table
+
+
+
+
+ Input
+
+
+
+
+ Pagination
+
+
+
+
+ Options
+
+
+
+
+ Select
+
+
+
+
+
+
+ );
+};
+
+export default Navbar;
diff --git a/src/components/pagination.tsx b/src/components/pagination.tsx
index 7514b3e..19a2ac2 100644
--- a/src/components/pagination.tsx
+++ b/src/components/pagination.tsx
@@ -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 (
-
+
{/* Botón Anterior */}
-
+
handlePageChange(currentPage - 1)}
@@ -68,9 +66,16 @@ export default function Pagination({
) : (
-
+
handlePageChange(page)}
>
{page}
@@ -81,7 +86,9 @@ export default function Pagination({
{/* Botón Siguiente */}
{
+ label: string;
+ value: T;
+ disabled?: boolean;
+}
+
+interface RadioOptionGroupProps {
+ name: string;
+ options: RadioOption[];
+ selectedValue?: T;
+ onChange: (value: T) => void;
+ disabled?: boolean;
+ className?: string;
+}
+
+export default function RadioOptionGroup({
+ name,
+ options,
+ selectedValue,
+ onChange,
+ disabled = false,
+ className = "",
+}: RadioOptionGroupProps) {
+ return (
+
+ {options.map((option, index) => (
+
+ onChange(option.value)}
+ />
+ {option.label}
+
+ ))}
+
+ );
+}
diff --git a/src/components/select.tsx b/src/components/select.tsx
new file mode 100644
index 0000000..364d43c
--- /dev/null
+++ b/src/components/select.tsx
@@ -0,0 +1,103 @@
+import React, { useId } from 'react';
+
+interface Option {
+ value: string | number;
+ label: string;
+}
+
+interface SelectProps
+ extends Omit, 'className'> {
+ label: string;
+ className?: {
+ container?: string;
+ label?: string;
+ input?: string;
+ };
+ options: Option[];
+ placeholder?: string;
+}
+
+/**
+ * Componente `Select` reutilizable en React.
+ *
+ * Este componente renderiza un elemento `` 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
+ *
+ *
+ * @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 ``.
+ * @param {React.SelectHTMLAttributes} [rest] - Cualquier otra prop válida para ``.
+ *
+ * @returns {JSX.Element} Un elemento `` con sus opciones y etiqueta asociada.
+ */
+const Select: React.FC = ({
+ label,
+ className,
+ options,
+ placeholder,
+ id,
+ name,
+ defaultValue,
+ value,
+ ...rest
+}) => {
+ const generatedId = useId();
+ const selectId = id || name || generatedId;
+
+ return (
+
+
+ {label}
+
+
+ {placeholder && (
+
+ {placeholder}
+
+ )}
+ {options.map((option) => (
+
+ {option.label}
+
+ ))}
+
+
+ );
+};
+
+export default Select;
diff --git a/src/components/table.tsx b/src/components/table.tsx
index 7acc404..b90bae2 100644
--- a/src/components/table.tsx
+++ b/src/components/table.tsx
@@ -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 (
diff --git a/src/components/vista-componente.tsx b/src/components/vista-componente.tsx
index 8afb9ea..8a215ce 100644
--- a/src/components/vista-componente.tsx
+++ b/src/components/vista-componente.tsx
@@ -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 (
-
+
{title}
{description && {description}
}
@@ -26,12 +29,15 @@ export default function VistaComponente({
{code && (
- setShowCode((prev) => !prev)}
>
{showCode ? "Ocultar código" : "Ver código y copiar"}
-
+
{showCode && }
)}
diff --git a/src/snippets/input/input-examples.ts b/src/snippets/input/input-examples.ts
new file mode 100644
index 0000000..4d3ff71
--- /dev/null
+++ b/src/snippets/input/input-examples.ts
@@ -0,0 +1,23 @@
+export const inputBasicExample = ` setValue(e.target.value)}
+/>`;
+
+export const inputPasswordExample = ` setPassword(e.target.value)}
+/>`;
+
+export const inputStylesExample = ` `;
diff --git a/src/snippets/options/option-examples.ts b/src/snippets/options/option-examples.ts
new file mode 100644
index 0000000..93ca2b4
--- /dev/null
+++ b/src/snippets/options/option-examples.ts
@@ -0,0 +1,59 @@
+export const radioBasicExample = `
+
+`;
+
+export const radioDisabledExample = `
+ {}}
+/>
+`;
+
+export const checkboxBasicExample = `
+ {
+ setSelectedCheckboxes((prev) =>
+ checked ? [...prev, value] : prev.filter((v) => v !== value)
+ );
+ }}
+/>
+`;
+
+export const checkboxLimitExample = `
+ {
+ setSelectedCheckboxes((prev) =>
+ checked ? [...prev, value] : prev.filter((v) => v !== value)
+ );
+ }}
+ maxSelected={2}
+/>
+`;
diff --git a/src/snippets/pagination/pagination-example.ts b/src/snippets/pagination/pagination-example.ts
new file mode 100644
index 0000000..3e60188
--- /dev/null
+++ b/src/snippets/pagination/pagination-example.ts
@@ -0,0 +1,16 @@
+export const paginationBasic = `
+
+`;
+
+export const paginationWithDelta = `
+
+`;
diff --git a/src/snippets/select/select.tsx b/src/snippets/select/select.tsx
new file mode 100644
index 0000000..fcbad84
--- /dev/null
+++ b/src/snippets/select/select.tsx
@@ -0,0 +1,32 @@
+export const countrySelectSnippet = `
+ setPais(e.target.value)}
+/>
+`;
+
+export const languageSelectSnippet = `
+
+`;
\ No newline at end of file
diff --git a/src/snippets/table/table-auto-pagination.ts b/src/snippets/table/table-auto-pagination.ts
new file mode 100644
index 0000000..fde61f7
--- /dev/null
+++ b/src/snippets/table/table-auto-pagination.ts
@@ -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 (
+
+ )
+}
+`;
diff --git a/src/snippets/table/table-basic.ts b/src/snippets/table/table-basic.ts
index 857b439..2684ce2 100644
--- a/src/snippets/table/table-basic.ts
+++ b/src/snippets/table/table-basic.ts
@@ -1,4 +1,36 @@
-// src/code-snippets/table-basic.ts
-export const tableBasicExample = `
-
+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 (
+
+ )
+}
+`;
+
+export const tableSkeletonExample = `import React from "react";
+import { TableSkeleton } from "@/components/table";
+
+export default function Page() {
+ return (
+
+ )
+}
`;
diff --git a/src/snippets/table/table-clickable.ts b/src/snippets/table/table-clickable.ts
new file mode 100644
index 0000000..700ca76
--- /dev/null
+++ b/src/snippets/table/table-clickable.ts
@@ -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 (
+ alert(\`Seleccionaste a \${row.name}\`)}
+ />
+ )
+}
+`;
diff --git a/src/snippets/table/table-column-width.ts b/src/snippets/table/table-column-width.ts
new file mode 100644
index 0000000..1fdfc8d
--- /dev/null
+++ b/src/snippets/table/table-column-width.ts
@@ -0,0 +1,23 @@
+export const tableColumnWidth = `interface RowData {
+ name: string;
+ age: number;
+ email: string;
+}
+
+export default function Page() {
+ const headers: Header[] = [
+ { 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 ;
+}
+`;
diff --git a/src/snippets/table/table-custom-cell-render.ts b/src/snippets/table/table-custom-cell-render.ts
new file mode 100644
index 0000000..4874dcb
--- /dev/null
+++ b/src/snippets/table/table-custom-cell-render.ts
@@ -0,0 +1,37 @@
+export const tableCustomCellRender = `
+interface RowData {
+ name: string;
+ age: number;
+ email: string;
+ acciones?: null;
+}
+
+export default function Page() {
+ const headers: Header[] = [
+ { 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) => (
+ alert(row.email)}
+ >
+ Ver correo
+
+ ),
+ },
+ ];
+
+ 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 ;
+}
+`;
diff --git a/src/snippets/table/table-custom-pagination.ts b/src/snippets/table/table-custom-pagination.ts
new file mode 100644
index 0000000..121e5f2
--- /dev/null
+++ b/src/snippets/table/table-custom-pagination.ts
@@ -0,0 +1,50 @@
+export const tableCustomPagination = `interface RowData {
+ name: string;
+ age: number;
+ email: string;
+}
+
+export default function Page() {
+ const headers: Header[] = [
+ { key: "name", label: "Nombre" },
+ { key: "age", label: "Edad" },
+ { key: "email", label: "Correo" },
+ ];
+
+ const [currentPage, setCurrentPage] = useState(1);
+ const [data, setData] = useState([]);
+ 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 ? (
+
+ ) : (
+
+ )}
+ >
+ )
+}
+`;
diff --git a/src/snippets/table/table-filter.ts b/src/snippets/table/table-filter.ts
new file mode 100644
index 0000000..522d3fa
--- /dev/null
+++ b/src/snippets/table/table-filter.ts
@@ -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[] = [
+ { 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 (
+ <>
+ setSearch(e.target.value)}
+ />
+
+ >
+ );
+}
+`;
diff --git a/src/styles/sass/_extends.scss b/src/styles/sass/_extends.scss
index c4accbc..e2d4d70 100644
--- a/src/styles/sass/_extends.scss
+++ b/src/styles/sass/_extends.scss
@@ -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;
+ }
+}
diff --git a/src/styles/sass/_gradients.scss b/src/styles/sass/_gradients.scss
index a51ebcb..e9db7e4 100644
--- a/src/styles/sass/_gradients.scss
+++ b/src/styles/sass/_gradients.scss
@@ -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);
}
}
diff --git a/src/styles/sass/bootstrap.scss b/src/styles/sass/bootstrap.scss
index 121c755..1aa33e6 100644
--- a/src/styles/sass/bootstrap.scss
+++ b/src/styles/sass/bootstrap.scss
@@ -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
\ No newline at end of file