Componentes
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 44 KiB |
@@ -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><Button /></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><button></code>.
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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><Input /></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<HTMLInputElement>) => 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<HTMLInputElement></code>
|
||||
</td>
|
||||
<td>
|
||||
Cualquier otra prop válida de un <code><input /></code>{" "}
|
||||
estándar.
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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><Pagination /></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) => 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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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><Select /></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: { value: string | number; label: string
|
||||
}[]
|
||||
</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?: { container?, label?, input? }</code>{" "}
|
||||
– Clases personalizadas.
|
||||
</li>
|
||||
<li>
|
||||
<code>...rest</code> – Cualquier otra prop válida de{" "}
|
||||
<code><select></code>.
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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><Table /></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<T>[]</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) => 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) => 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) => void</code>
|
||||
</td>
|
||||
<td>Función callback que se dispara al cambiar de página.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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><Table /></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>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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}
|
||||
|
||||
@@ -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;
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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",
|
||||
}}
|
||||
/>`;
|
||||
@@ -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}
|
||||
/>
|
||||
`;
|
||||
@@ -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}
|
||||
/>
|
||||
)
|
||||
}
|
||||
`;
|
||||
@@ -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} />
|
||||
)
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -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}\`)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
`;
|
||||
@@ -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}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
`;
|
||||
@@ -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} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
`;
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||