@axzydev/axzy_ui_system 1.2.16 → 1.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/dist/index.cjs +3349 -1457
  2. package/dist/index.cjs.map +1 -1
  3. package/dist/index.css +1 -1
  4. package/dist/index.d.cts +719 -54
  5. package/dist/index.d.ts +719 -54
  6. package/dist/index.js +3283 -1387
  7. package/dist/index.js.map +1 -1
  8. package/package.json +1 -1
  9. package/src/App.tsx +32 -0
  10. package/src/components/atoms/badget/badget.tsx +2 -4
  11. package/src/components/atoms/chip/chip.doc.mdx +47 -0
  12. package/src/components/atoms/chip/chip.props.ts +32 -0
  13. package/src/components/atoms/chip/chip.stories.tsx +64 -0
  14. package/src/components/atoms/chip/chip.tsx +112 -0
  15. package/src/components/atoms/grid/grid.tsx +1 -2
  16. package/src/components/molecules/accordion/accordion.doc.mdx +47 -0
  17. package/src/components/molecules/accordion/accordion.props.ts +35 -0
  18. package/src/components/molecules/accordion/accordion.stories.tsx +68 -0
  19. package/src/components/molecules/accordion/accordion.tsx +123 -0
  20. package/src/components/molecules/calendar/calendar.doc.mdx +2 -1
  21. package/src/components/molecules/calendar/calendar.tsx +102 -24
  22. package/src/components/molecules/chip-input/chip-input.doc.mdx +44 -0
  23. package/src/components/molecules/chip-input/chip-input.props.ts +42 -0
  24. package/src/components/molecules/chip-input/chip-input.stories.tsx +52 -0
  25. package/src/components/molecules/chip-input/chip-input.tsx +205 -0
  26. package/src/components/molecules/date-picker/datePicker.tsx +45 -56
  27. package/src/components/molecules/dropdown-menu/dropdown-menu.doc.mdx +46 -0
  28. package/src/components/molecules/dropdown-menu/dropdown-menu.props.ts +41 -0
  29. package/src/components/molecules/dropdown-menu/dropdown-menu.stories.tsx +41 -0
  30. package/src/components/molecules/dropdown-menu/dropdown-menu.tsx +219 -0
  31. package/src/components/molecules/field/field.doc.mdx +43 -0
  32. package/src/components/molecules/field/field.props.ts +23 -0
  33. package/src/components/molecules/field/field.stories.tsx +42 -0
  34. package/src/components/molecules/field/field.tsx +54 -0
  35. package/src/components/molecules/masked-input/masked-input.doc.mdx +78 -0
  36. package/src/components/molecules/masked-input/masked-input.props.ts +58 -0
  37. package/src/components/molecules/masked-input/masked-input.stories.tsx +126 -0
  38. package/src/components/molecules/masked-input/masked-input.tsx +244 -0
  39. package/src/components/molecules/multi-select/multi-select.doc.mdx +55 -0
  40. package/src/components/molecules/multi-select/multi-select.props.ts +65 -0
  41. package/src/components/molecules/multi-select/multi-select.stories.tsx +59 -0
  42. package/src/components/molecules/multi-select/multi-select.tsx +386 -0
  43. package/src/components/molecules/search-select/search-select.doc.mdx +5 -0
  44. package/src/components/molecules/search-select/search-select.props.ts +10 -1
  45. package/src/components/molecules/search-select/search-select.stories.tsx +18 -0
  46. package/src/components/molecules/search-select/search-select.tsx +102 -28
  47. package/src/components/molecules/time-picker/timePicker.tsx +14 -26
  48. package/src/components/molecules/wysiwyg/wysiwyg.props.ts +51 -0
  49. package/src/components/molecules/wysiwyg/wysiwyg.stories.tsx +95 -0
  50. package/src/components/molecules/wysiwyg/wysiwyg.tsx +609 -0
  51. package/src/components/organisms/form-builder/fieldRenderer.tsx +144 -27
  52. package/src/components/organisms/form-builder/formBuilder.context.tsx +77 -15
  53. package/src/components/organisms/form-builder/formBuilder.doc.mdx +213 -86
  54. package/src/components/organisms/form-builder/formBuilder.props.ts +118 -36
  55. package/src/components/organisms/form-builder/formBuilder.stories.tsx +694 -96
  56. package/src/components/organisms/form-builder/formBuilder.tsx +152 -69
  57. package/src/components/organisms/form-builder/useFormBuilder.ts +59 -21
  58. package/src/hooks/useFloatingPanel.ts +100 -0
  59. package/src/index.ts +26 -4
  60. package/src/showcases/DataShowcases.tsx +76 -0
  61. package/src/showcases/FormShowcases.tsx +574 -10
  62. package/src/showcases/HomeShowcase.tsx +6 -6
  63. package/src/showcases/NavigationShowcases.tsx +72 -2
  64. package/src/showcases/StructureShowcases.tsx +64 -0
  65. package/src/types/chip.types.ts +13 -0
@@ -1,5 +1,5 @@
1
1
  import React, { useState, useEffect, useMemo } from "react";
2
- import { FaSync, FaSave, FaTrash, FaEdit, FaCode } from "react-icons/fa";
2
+ import { FaSync, FaSave, FaTrash, FaEdit, FaCode, FaCheck } from "react-icons/fa";
3
3
  import {
4
4
  ITButton,
5
5
  ITInput,
@@ -11,6 +11,12 @@ import {
11
11
  ITSlideToggle,
12
12
  ITDropfile,
13
13
  ITFormBuilder,
14
+ ITWysiwyg,
15
+ ITField,
16
+ ITMaskedInput,
17
+ ITMultiSelect,
18
+ ITChipInput,
19
+ ITBadget,
14
20
  UploadStatus
15
21
  } from "../index";
16
22
  import { ShowcaseLayout, CodeViewer } from "./ShowcaseLayout";
@@ -750,25 +756,29 @@ export const SelectShowcase = () => {
750
756
  // 4. ITSearchSelect Showcase
751
757
  export const SearchSelectShowcase = () => {
752
758
  const [val, setVal] = useState<any>("");
759
+ const [customVal, setCustomVal] = useState<any>("MX");
760
+ const [clearVal, setClearVal] = useState<any>("ES");
753
761
  const [isLoading, setIsLoading] = useState(false);
754
762
  const [disabled, setDisabled] = useState(false);
755
763
 
756
764
  const options = [
757
- { label: "Argentina", value: "AR" },
758
- { label: "Brasil", value: "BR" },
759
- { label: "Colombia", value: "CO" },
760
- { label: "México", value: "MX" },
761
- { label: "Perú", value: "PE" },
762
- { label: "España", value: "ES" }
765
+ { label: "Argentina", value: "AR", flag: "🇦🇷", region: "Sudamérica" },
766
+ { label: "Brasil", value: "BR", flag: "🇧🇷", region: "Sudamérica" },
767
+ { label: "Colombia", value: "CO", flag: "🇨🇴", region: "Sudamérica" },
768
+ { label: "México", value: "MX", flag: "🇲🇽", region: "Norteamérica" },
769
+ { label: "Perú", value: "PE", flag: "🇵🇪", region: "Sudamérica" },
770
+ { label: "España", value: "ES", flag: "🇪🇸", region: "Europa" }
763
771
  ];
764
772
 
765
773
  const code = `<ITSearchSelect\n name="country"\n label="Seleccionar País"\n value="${val}"\n options={[\n { label: 'Argentina', value: 'AR' },\n { label: 'Brasil', value: 'BR' },...\n ]}\n isLoading={${isLoading}}\n disabled={${disabled}}\n onChange={(value) => setVal(value)}\n/>`;
766
774
 
775
+ const customCode = `<ITSearchSelect\n name="country_custom"\n label="País (template personalizado)"\n value="${customVal}"\n options={countries}\n onChange={(value) => setCustomVal(value)}\n renderOption={(option, { isSelected }) => (\n <div className="flex items-center gap-2">\n <span>{option.flag}</span>\n <span className="flex-1">{option.label}</span>\n <span className="text-xs text-secondary-400">{option.region}</span>\n {isSelected && <FaCheck className="text-primary-500" size={10} />}\n </div>\n )}\n/>`;
776
+
767
777
  return (
768
778
  <ShowcaseLayout
769
779
  title="ITSearchSelect"
770
780
  description="Selector avanzado con barra de búsqueda para filtrar colecciones grandes o cargar opciones remotas."
771
- code={code}
781
+ code={`${code}\n\n${customCode}`}
772
782
  demo={
773
783
  <div className="w-full max-w-sm">
774
784
  <ITSearchSelect
@@ -785,6 +795,50 @@ export const SearchSelectShowcase = () => {
785
795
  )}
786
796
  </div>
787
797
  }
798
+ gallery={
799
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-8">
800
+ <div className="w-full max-w-sm">
801
+ <p className="mb-3 text-[10px] font-bold uppercase tracking-wider text-slate-400">
802
+ Template personalizado (renderOption)
803
+ </p>
804
+ <ITSearchSelect
805
+ name="country_custom"
806
+ label="País (template personalizado)"
807
+ value={customVal}
808
+ options={options}
809
+ onChange={(value) => setCustomVal(value)}
810
+ renderOption={(option, { isSelected }) => (
811
+ <div className="flex items-center gap-2">
812
+ <span>{option.flag}</span>
813
+ <span className="flex-1">{option.label}</span>
814
+ <span className="text-xs text-secondary-400">{option.region}</span>
815
+ {isSelected && <FaCheck className="text-primary-500" size={10} />}
816
+ </div>
817
+ )}
818
+ />
819
+ <p className="mt-2 text-xs text-slate-500 font-mono">
820
+ {customVal ? `País seleccionado: "${customVal}"` : "Sin selección"}
821
+ </p>
822
+ </div>
823
+
824
+ <div className="w-full max-w-sm">
825
+ <p className="mb-3 text-[10px] font-bold uppercase tracking-wider text-slate-400">
826
+ Botón de limpiar (X)
827
+ </p>
828
+ <ITSearchSelect
829
+ name="country_clear"
830
+ label="País (clearable)"
831
+ value={clearVal}
832
+ options={options}
833
+ onChange={(value) => setClearVal(value)}
834
+ onClear={() => setClearVal("")}
835
+ />
836
+ <p className="mt-2 text-xs text-slate-500 font-mono">
837
+ {clearVal ? `País seleccionado: "${clearVal}"` : "Sin selección"}
838
+ </p>
839
+ </div>
840
+ </div>
841
+ }
788
842
  controls={
789
843
  <>
790
844
  <div className="flex items-center justify-between">
@@ -1120,7 +1174,98 @@ export const DropfileShowcase = () => {
1120
1174
  );
1121
1175
  };
1122
1176
 
1123
- // 10. ITFormBuilder Showcase
1177
+ // 10. ITWysiwyg Showcase
1178
+ export const WysiwygShowcase = () => {
1179
+ const [html, setHtml] = useState(
1180
+ "<div>Edita este texto con la <strong>barra de herramientas</strong>.</div><ul><li>Negrita, <em>itálica</em> y <u>subrayado</u></li><li><mark style=\"background-color:#fde68a\">Marcador amarillo</mark></li></ul>"
1181
+ );
1182
+ const [size, setSize] = useState<any>("md");
1183
+ const [error, setError] = useState<string | undefined>(undefined);
1184
+
1185
+ const code = `<ITWysiwyg\n label="Descripción"\n value={html}\n onChange={setHtml}\n size="${size}"\n highlightColor="#fde68a"\n/>`;
1186
+
1187
+ return (
1188
+ <ShowcaseLayout
1189
+ title="ITWysiwyg"
1190
+ description="Editor WYSIWYG ligero sin dependencias externas: negrita, itálica, subrayado, marcador amarillo y listas. Implementado a mano sobre Selection/Range (sin execCommand)."
1191
+ code={code}
1192
+ doc={{
1193
+ summary:
1194
+ "Editor de texto enriquecido formado por una barra de herramientas y un área contentEditable. Toda la lógica de formato es manual sobre la API Selection/Range.",
1195
+ description:
1196
+ "Soporta bold, italic, underline, marcador amarillo configurable y listas con viñetas/numeradas. Las operaciones togglean envolviendo/desenvolviendo elementos, sin usar document.execCommand.",
1197
+ examples: [
1198
+ '<ITWysiwyg label="Descripción" value={html} onChange={setHtml} />',
1199
+ '<ITWysiwyg size="lg" highlightColor="#fef08a" toolbar={["bold", "ul"]} />',
1200
+ ],
1201
+ props: [
1202
+ { name: "value", type: "string", description: "HTML controlado. Se refleja solo cuando el editor no está enfocado." },
1203
+ { name: "onChange", type: "(html: string) => void", description: "Devuelve el HTML del editor al cambiar." },
1204
+ { name: "label", type: "string", description: "Texto de etiqueta sobre el editor." },
1205
+ { name: "placeholder", type: "string", description: "Texto fantasma cuando el editor está vacío." },
1206
+ { name: "error", type: "string", description: "Mensaje de validación (borde/ring en rojo)." },
1207
+ { name: "disabled", type: "boolean", default: "false", description: "Deshabilita edición y toolbar." },
1208
+ { name: "readOnly", type: "boolean", default: "false", description: "Solo lectura." },
1209
+ { name: "size", type: "sm | md | lg", default: "md", description: "Padding y fuente del área editable." },
1210
+ { name: "minHeight", type: "number", default: "128", description: "Altura mínima en px del área editable." },
1211
+ { name: "highlightColor", type: "string", default: "#fde68a", description: "Color del marcador." },
1212
+ { name: "toolbar", type: "ToolbarAction[]", default: "todas", description: "Subconjunto de acciones: bold, italic, underline, highlight, ul, ol, clear." },
1213
+ ],
1214
+ notes: [
1215
+ "Las listas operan por bloques de primer nivel (no dividen selecciones parciales dentro de un bloque).",
1216
+ "El pegado se inserta como texto plano (sin HTML externo).",
1217
+ "Al estar enfocado el editor, cambios externos del value no se aplican para evitar saltos del cursor.",
1218
+ ],
1219
+ }}
1220
+ demo={
1221
+ <div className="w-full max-w-2xl space-y-4">
1222
+ <ITWysiwyg
1223
+ label="Contenido del artículo"
1224
+ value={html}
1225
+ onChange={setHtml}
1226
+ size={size}
1227
+ error={error}
1228
+ placeholder="Escribe o pega tu contenido..."
1229
+ />
1230
+ <div className="p-4 bg-slate-950 rounded-xl border border-slate-800">
1231
+ <p className="text-[10px] font-bold uppercase tracking-wider text-slate-500 mb-2">
1232
+ HTML generado
1233
+ </p>
1234
+ <pre className="text-[11px] text-emerald-300 font-mono whitespace-pre-wrap break-words">
1235
+ {html}
1236
+ </pre>
1237
+ </div>
1238
+ </div>
1239
+ }
1240
+ controls={
1241
+ <>
1242
+ <ITSelect
1243
+ name="size_ctrl"
1244
+ label="Tamaño"
1245
+ value={size}
1246
+ onChange={(e: any) => setSize(e.target.value)}
1247
+ options={[
1248
+ { label: "Small", value: "sm" },
1249
+ { label: "Medium", value: "md" },
1250
+ { label: "Large", value: "lg" },
1251
+ ]}
1252
+ />
1253
+ <div className="flex items-center justify-between pt-2">
1254
+ <span className="text-sm font-semibold text-gray-700">Forzar error</span>
1255
+ <ITSlideToggle
1256
+ isOn={!!error}
1257
+ onToggle={(v) => setError(v ? "Este campo es obligatorio." : undefined)}
1258
+ activeColor="danger"
1259
+ size="sm"
1260
+ />
1261
+ </div>
1262
+ </>
1263
+ }
1264
+ />
1265
+ );
1266
+ };
1267
+
1268
+ // 11. ITFormBuilder Showcase
1124
1269
  export const FormBuilderShowcase = () => {
1125
1270
  const [values, setValues] = useState({
1126
1271
  name: "",
@@ -1161,7 +1306,38 @@ export const FormBuilderShowcase = () => {
1161
1306
  <div className="space-y-8">
1162
1307
  <ShowcaseLayout
1163
1308
  title="ITFormBuilder"
1164
- description="Generador dinámico de formularios basado en un esquema estructurado JSON."
1309
+ description="Generador dinámico de formularios basado en un esquema estructurado JSON. Soporta V1 retrocompatible y la arquitectura V2 con motor de reglas, secciones, renderers custom y opciones asíncronas."
1310
+ doc={{
1311
+ summary:
1312
+ "Renderiza un grid responsive de inputs a partir de un arreglo de definiciones de campo. Totalmente controlado: delega el estado a Formik (o cualquier holder externo).",
1313
+ description:
1314
+ "La API V2 (`config`) habilita renderizado condicional (`renderWhen`/`dependsOn`), propiedades dinámicas (`dynamicProps`), campos derivados (`onChangeAction`), carga asíncrona de opciones, secciones anidadas y componentes custom. La API V1 (`fields`) se conserva para compatibilidad y soporta un subconjunto reducido sin reglas ni secciones.",
1315
+ examples: [
1316
+ `<ITFormBuilder config={[{ name: "email", label: "Email", type: "email", required: true }]} values={formik.values} handleChange={formik.handleChange} handleBlur={formik.handleBlur} touched={formik.touched} errors={formik.errors} setFieldValue={formik.setFieldValue} />`,
1317
+ `<ITFormBuilder fields={[{ name: "email", type: "text" }, { name: "pwd", type: "password" }]} values={...} handleChange={...} handleBlur={...} touched={...} errors={...} />`,
1318
+ ],
1319
+ props: [
1320
+ { name: "fields", type: "FieldConfig[]", description: "(V1, legacy) Definiciones de campo. Mantenido por compatibilidad." },
1321
+ { name: "config", type: "FieldConfigV2[]", description: "(V2) Definiciones de campo con motor de reglas, secciones y custom. Recomendado." },
1322
+ { name: "columns", type: "number", default: "12", description: "Número de columnas del grid responsive (1-12)." },
1323
+ { name: "values", type: "Record<string, unknown>", required: true, description: "Valores actuales del formulario, indexados por `name`." },
1324
+ { name: "handleChange", type: "(event) => void", required: true, description: "Handler onChange compatible con Formik." },
1325
+ { name: "handleBlur", type: "(event) => void", required: true, description: "Handler onBlur compatible con Formik." },
1326
+ { name: "touched", type: "Record<string, boolean>", required: true, description: "Estado de touched por campo." },
1327
+ { name: "errors", type: "Record<string, string>", required: true, description: "Errores de validación por campo." },
1328
+ { name: "setFieldValue", type: "(field, value, shouldValidate?) => Promise", description: "Setter programático de valor (Formik parity). Necesario para V2 `onChangeAction`/`dynamicProps`." },
1329
+ { name: "setFieldTouched", type: "(field, touched?, shouldValidate?) => Promise", description: "Setter programático de touched." },
1330
+ { name: "setFieldError", type: "(field, message?) => void", description: "Setter programático de error de campo." },
1331
+ { name: "isSubmitting", type: "boolean", default: "false", description: "Bandera de submit en curso; expuesta al contexto." },
1332
+ ],
1333
+ notes: [
1334
+ "Cuando se pasan ambos `config` y `fields`, gana `config` (V2).",
1335
+ "Si no se pasa `config` ni `fields` el componente renderiza un grid vacío.",
1336
+ "El hook `useFormBuilder` debe usarse dentro del subárbol de un `ITFormBuilder` con la API V2 (lanza un error claro fuera del provider).",
1337
+ "`validation` y `asyncValidation` NO se ejecutan automáticamente — intégralos con Yup en el Formik padre, o ejecútalos manualmente dentro de `onChangeAction`.",
1338
+ "Opciones async (`options: () => Promise<...>`) montan `ITSearchSelect` con un placeholder de spinner mientras cargan.",
1339
+ ],
1340
+ }}
1165
1341
  code={code}
1166
1342
  demo={
1167
1343
  <div className="w-full max-w-md space-y-6">
@@ -1527,3 +1703,391 @@ const CurrencyConverterExample = () => {
1527
1703
  </div>
1528
1704
  );
1529
1705
  };
1706
+
1707
+ // 12. ITField Showcase
1708
+ export const FieldShowcase = () => {
1709
+ const [email, setEmail] = useState("");
1710
+ const [showHelp, setShowHelp] = useState(true);
1711
+ const invalid = email.length > 0 && !email.includes("@");
1712
+
1713
+ const code = `<ITField\n label="Correo"\n htmlFor="email"\n required\n error={${invalid ? '"Ingresa un correo válido."' : "undefined"}}\n helpText="Nunca compartimos tu correo."\n>\n <ITInput name="email" value="${email}" onChange={(e) => setEmail(e.target.value)} />\n</ITField>`;
1714
+
1715
+ return (
1716
+ <ShowcaseLayout
1717
+ title="ITField"
1718
+ description="Contenedor de campo de formulario que estandariza label, asterisco de requerido, texto de ayuda y mensaje de error alrededor de cualquier control."
1719
+ code={code}
1720
+ demo={
1721
+ <div className="w-full max-w-sm">
1722
+ <ITField
1723
+ label="Correo"
1724
+ htmlFor="email"
1725
+ required
1726
+ error={invalid ? "Ingresa un correo válido." : undefined}
1727
+ helpText={showHelp ? "Nunca compartimos tu correo." : undefined}
1728
+ >
1729
+ <ITInput name="email" value={email} onChange={(e: any) => setEmail(e.target.value)} onBlur={() => {}} />
1730
+ </ITField>
1731
+ </div>
1732
+ }
1733
+ controls={
1734
+ <div className="flex items-center justify-between">
1735
+ <span className="text-sm font-semibold text-gray-700">Mostrar ayuda</span>
1736
+ <ITSlideToggle isOn={showHelp} onToggle={setShowHelp} size="sm" />
1737
+ </div>
1738
+ }
1739
+ doc={{
1740
+ summary: "Wrapper que unifica label, requerido, ayuda y error para cualquier control.",
1741
+ examples: [
1742
+ '<ITField label="Email" htmlFor="email" required error={errors.email} helpText="...">',
1743
+ ' <ITInput name="email" value={email} onChange={onChange} />',
1744
+ '</ITField>',
1745
+ ],
1746
+ props: [
1747
+ { name: "label", type: "string", description: "Etiqueta sobre el control." },
1748
+ { name: "htmlFor", type: "string", description: "id del control (htmlFor)." },
1749
+ { name: "required", type: "boolean", default: "false", description: "Muestra asterisco rojo." },
1750
+ { name: "error", type: "string | boolean", description: "Mensaje de error o true para el genérico." },
1751
+ { name: "helpText", type: "string", description: "Texto de ayuda si no hay error." },
1752
+ { name: "children", type: "ReactNode", required: true, description: "El control del formulario." },
1753
+ ],
1754
+ notes: ["El error tiene prioridad sobre helpText; solo se muestra uno."],
1755
+ }}
1756
+ />
1757
+ );
1758
+ };
1759
+
1760
+ // 13. ITMultiSelect Showcase
1761
+ export const MultiSelectShowcase = () => {
1762
+ const [value, setValue] = useState<(string | number)[]>(["mx", "es"]);
1763
+
1764
+ const options = [
1765
+ { value: "mx", label: "México" },
1766
+ { value: "es", label: "España" },
1767
+ { value: "co", label: "Colombia" },
1768
+ { value: "ar", label: "Argentina" },
1769
+ { value: "pe", label: "Perú" },
1770
+ { value: "cl", label: "Chile" },
1771
+ ];
1772
+
1773
+ const code = `<ITMultiSelect\n label="Países"\n options={countries}\n value={[${value.map((v) => `'${v}'`).join(", ")}]}\n onChange={(values) => setValue(values)}\n maxVisibleChips={3}\n/>`;
1774
+
1775
+ return (
1776
+ <ShowcaseLayout
1777
+ title="ITMultiSelect"
1778
+ description="Selector de múltiples valores con dropdown buscable, chips removibles y navegación por teclado. El dropdown va en portal, así que no se recorta dentro de un ITCard."
1779
+ code={code}
1780
+ demo={
1781
+ <div className="w-full max-w-sm">
1782
+ <ITMultiSelect
1783
+ label="Países"
1784
+ options={options}
1785
+ value={value}
1786
+ onChange={(values) => setValue(values)}
1787
+ maxVisibleChips={3}
1788
+ />
1789
+ <p className="mt-2 text-xs text-slate-500 font-mono">
1790
+ {value.length} seleccionados: {JSON.stringify(value)}
1791
+ </p>
1792
+ </div>
1793
+ }
1794
+ controls={
1795
+ <div className="space-y-2">
1796
+ <p className="text-xs text-slate-500">
1797
+ Usa las flechas para navegar, Enter para alternar y Backspace para quitar el último chip.
1798
+ </p>
1799
+ </div>
1800
+ }
1801
+ gallery={
1802
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-8">
1803
+ <div className="w-full max-w-sm">
1804
+ <p className="mb-3 text-[10px] font-bold uppercase tracking-wider text-slate-400">Plantilla de opción (renderOption)</p>
1805
+ <ITMultiSelect
1806
+ label="Skills (template)"
1807
+ options={options}
1808
+ value={value}
1809
+ onChange={(values) => setValue(values)}
1810
+ renderOption={(option, { isSelected }) => (
1811
+ <span className={isSelected ? "font-bold" : ""}>{option.label}</span>
1812
+ )}
1813
+ />
1814
+ </div>
1815
+ <div className="w-full max-w-sm">
1816
+ <p className="mb-3 text-[10px] font-bold uppercase tracking-wider text-slate-400">Con error</p>
1817
+ <ITMultiSelect
1818
+ label="Requerido"
1819
+ options={options}
1820
+ value={[]}
1821
+ onChange={() => {}}
1822
+ required
1823
+ touched
1824
+ error
1825
+ />
1826
+ </div>
1827
+ </div>
1828
+ }
1829
+ doc={{
1830
+ summary: "Select múltiple con búsqueda, chips removibles y navegación por teclado.",
1831
+ examples: [
1832
+ '<ITMultiSelect label="Skills" options={skills} value={selected} onChange={setSelected} />',
1833
+ '<ITMultiSelect options={opts} value={v} onChange={setV} renderOption={(o, { isSelected }) => ...} />',
1834
+ ],
1835
+ props: [
1836
+ { name: "options", type: "ITMultiSelectOption[]", required: true, description: "Opciones { label, value, ...custom }." },
1837
+ { name: "value", type: "(string | number)[]", required: true, description: "Valores seleccionados (controlado)." },
1838
+ { name: "onChange", type: "(values, options) => void", description: "Devuelve los valores seleccionados." },
1839
+ { name: "label", type: "string", description: "Etiqueta del control." },
1840
+ { name: "placeholder", type: "string", default: '"Selecciona opciones"', description: "Texto cuando está vacío." },
1841
+ { name: "maxVisibleChips", type: "number", default: "3", description: "Chips visibles antes de +N." },
1842
+ { name: "clearable", type: "boolean", default: "true", description: "Botón de limpiar (X)." },
1843
+ { name: "searchable", type: "boolean", default: "true", description: "Habilita el input de búsqueda." },
1844
+ { name: "onSearch", type: "(query) => void", description: "Búsqueda remota." },
1845
+ { name: "renderOption", type: "(option, state) => ReactNode", description: "Plantilla de opción." },
1846
+ { name: "error", type: "string | boolean", description: "Estado de error." },
1847
+ ],
1848
+ notes: [
1849
+ "role=combobox + listbox con aria-multiselectable.",
1850
+ "Backspace con input vacío elimina el último chip.",
1851
+ ],
1852
+ }}
1853
+ />
1854
+ );
1855
+ };
1856
+
1857
+ // 14. ITChipInput Showcase
1858
+ export const ChipInputShowcase = () => {
1859
+ const [tags, setTags] = useState<string[]>(["react", "typescript"]);
1860
+ const [maxTags, setMaxTags] = useState(5);
1861
+
1862
+ const code = `<ITChipInput\n label="Etiquetas"\n value={[${tags.map((t) => `'${t}'`).join(", ")}]}\n onChange={setTags}\n maxTags={${maxTags}}\n placeholder="Agrega una etiqueta..."\n/>`;
1863
+
1864
+ return (
1865
+ <ShowcaseLayout
1866
+ title="ITChipInput"
1867
+ description="Entrada de texto libre que convierte los valores escritos en etiquetas (chips) removibles. Confirma con Enter o coma, al perder foco o al pegar texto delimitado."
1868
+ code={code}
1869
+ demo={
1870
+ <div className="w-full max-w-sm">
1871
+ <ITChipInput
1872
+ label="Etiquetas"
1873
+ value={tags}
1874
+ onChange={setTags}
1875
+ maxTags={maxTags}
1876
+ placeholder="Agrega una etiqueta..."
1877
+ helpText="Enter o coma para agregar. Backspace para borrar la última."
1878
+ />
1879
+ <div className="mt-3 flex flex-wrap gap-1">
1880
+ {tags.map((t) => (
1881
+ <ITBadget key={t} label={t} color="primary" size="sm" />
1882
+ ))}
1883
+ </div>
1884
+ </div>
1885
+ }
1886
+ controls={
1887
+ <ITInput
1888
+ name="maxTags"
1889
+ label="Máximo de etiquetas"
1890
+ type="number"
1891
+ value={maxTags}
1892
+ onChange={(e: any) => setMaxTags(Number(e.target.value) || 0)}
1893
+ onBlur={() => {}}
1894
+ />
1895
+ }
1896
+ doc={{
1897
+ summary: "Input que convierte texto en etiquetas (chips) removibles con validación.",
1898
+ examples: [
1899
+ '<ITChipInput label="Etiquetas" value={tags} onChange={setTags} />',
1900
+ '<ITChipInput value={emails} onChange={setEmails} validate={(v) => isEmail(v) ? undefined : "Correo inválido"} />',
1901
+ ],
1902
+ props: [
1903
+ { name: "value", type: "string[]", required: true, description: "Etiquetas actuales (controlado)." },
1904
+ { name: "onChange", type: "(values: string[]) => void", description: "Devuelve las etiquetas." },
1905
+ { name: "delimiters", type: "string[]", default: '["Enter", ","]', description: "Teclas que confirman una etiqueta." },
1906
+ { name: "maxTags", type: "number", description: "Máximo de etiquetas." },
1907
+ { name: "allowDuplicates", type: "boolean", default: "false", description: "Permite duplicados." },
1908
+ { name: "validate", type: "(value) => string | undefined", description: "Rechaza una etiqueta con mensaje." },
1909
+ { name: "color", type: "ColorsTypes", default: '"primary"', description: "Color de los chips." },
1910
+ { name: "error", type: "string | boolean", description: "Estado de error." },
1911
+ ],
1912
+ notes: ["Backspace en input vacío elimina la última etiqueta."],
1913
+ }}
1914
+ />
1915
+ );
1916
+ };
1917
+
1918
+ // 15. ITMaskedInput Showcase
1919
+ const MaskedDemo = ({
1920
+ title,
1921
+ mask,
1922
+ pattern,
1923
+ hint,
1924
+ initial,
1925
+ }: {
1926
+ title: string;
1927
+ mask: string;
1928
+ pattern?: RegExp;
1929
+ hint?: string;
1930
+ initial?: string;
1931
+ }) => {
1932
+ const [raw, setRaw] = useState<string>(initial ?? "");
1933
+ const [complete, setComplete] = useState<string | null>(null);
1934
+ return (
1935
+ <div>
1936
+ <h4 className="text-sm font-bold text-slate-700 mb-2">{title}</h4>
1937
+ <ITMaskedInput
1938
+ name={title.toLowerCase().replace(/\s+/g, "_")}
1939
+ label={mask}
1940
+ mask={mask}
1941
+ pattern={pattern}
1942
+ value={raw}
1943
+ onChange={(e) => {
1944
+ setRaw(e.target.value);
1945
+ setComplete(null);
1946
+ }}
1947
+ onComplete={(v) => setComplete(v)}
1948
+ />
1949
+ <p className="mt-1.5 text-xs font-mono text-slate-500">
1950
+ raw: {raw || "(vacío)"}
1951
+ {complete && <span className="text-emerald-600 ml-2">✓ completo</span>}
1952
+ {hint && <span className="text-slate-400 ml-2">· {hint}</span>}
1953
+ </p>
1954
+ </div>
1955
+ );
1956
+ };
1957
+
1958
+ export const MaskedInputShowcase = () => {
1959
+ const [code, setCode] = useState<string>("");
1960
+ const [variant, setVariant] = useState<any>("primary");
1961
+ const [size, setSize] = useState<any>("md");
1962
+ const [disabled, setDisabled] = useState(false);
1963
+ const [withError, setWithError] = useState(false);
1964
+
1965
+ const codeExample = `<ITMaskedInput\n name="code"\n label="Código de activación"\n mask="xxxx-xxxx-xxxx"\n variant="${variant}"\n size="${size}"\n disabled={${disabled}}\n onChange={(e) => setCode(e.target.value)}\n/>`;
1966
+
1967
+ return (
1968
+ <ShowcaseLayout
1969
+ title="ITMaskedInput"
1970
+ description="Input de texto con máscara de formato: teclea solo caracteres útiles y los separadores se insertan solos (xxxx-xxxx-xxxx, (999) 999-9999, etc.)."
1971
+ code={codeExample}
1972
+ demo={
1973
+ <div className="w-full max-w-sm">
1974
+ <ITMaskedInput
1975
+ name="showcase_masked"
1976
+ label="Código de activación"
1977
+ mask="xxxx-xxxx-xxxx"
1978
+ value={code}
1979
+ variant={variant}
1980
+ size={size}
1981
+ disabled={disabled}
1982
+ error={withError}
1983
+ onChange={(e) => setCode(e.target.value)}
1984
+ />
1985
+ {code && (
1986
+ <p className="mt-2 text-xs text-slate-500 font-mono">
1987
+ Valor actual (raw): {code || "—"}
1988
+ </p>
1989
+ )}
1990
+ </div>
1991
+ }
1992
+ controls={
1993
+ <>
1994
+ <ITSelect
1995
+ name="masked_variant_ctrl"
1996
+ label="Tema de Color"
1997
+ value={variant}
1998
+ onChange={(e: any) => setVariant(e.target.value)}
1999
+ options={[
2000
+ { label: "Primary", value: "primary" },
2001
+ { label: "Secondary", value: "secondary" },
2002
+ { label: "Success", value: "success" },
2003
+ { label: "Danger", value: "danger" },
2004
+ { label: "Warning", value: "warning" },
2005
+ { label: "Info", value: "info" },
2006
+ ]}
2007
+ />
2008
+ <ITSelect
2009
+ name="masked_size_ctrl"
2010
+ label="Tamaño"
2011
+ value={size}
2012
+ onChange={(e: any) => setSize(e.target.value)}
2013
+ options={[
2014
+ { label: "Small", value: "sm" },
2015
+ { label: "Medium", value: "md" },
2016
+ { label: "Large", value: "lg" },
2017
+ ]}
2018
+ />
2019
+ <div className="flex items-center justify-between pt-2">
2020
+ <span className="text-sm font-semibold text-gray-700">Deshabilitado</span>
2021
+ <ITSlideToggle isOn={disabled} onToggle={setDisabled} activeColor="danger" size="sm" />
2022
+ </div>
2023
+ <div className="flex items-center justify-between">
2024
+ <span className="text-sm font-semibold text-gray-700">Error</span>
2025
+ <ITSlideToggle isOn={withError} onToggle={setWithError} activeColor="danger" size="sm" />
2026
+ </div>
2027
+ </>
2028
+ }
2029
+ gallery={
2030
+ <div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-6">
2031
+ <MaskedDemo
2032
+ title="Código alfanumérico"
2033
+ mask="xxxx-xxxx-xxxx"
2034
+ hint="Escribe: ABC1-2345"
2035
+ />
2036
+ <MaskedDemo
2037
+ title="Número de tarjeta"
2038
+ mask="9999-9999-9999-9999"
2039
+ hint="Escribe 16 dígitos"
2040
+ />
2041
+ <MaskedDemo
2042
+ title="Teléfono"
2043
+ mask="(999) 999-9999"
2044
+ pattern={/^\d{10}$/}
2045
+ hint="Regex /^\\d{10}$/"
2046
+ />
2047
+ <MaskedDemo
2048
+ title="Código postal"
2049
+ mask="99999"
2050
+ hint="Ejemplo: 44100"
2051
+ />
2052
+ <MaskedDemo
2053
+ title="CVV / OTP"
2054
+ mask="999"
2055
+ hint="3 dígitos, onComplete"
2056
+ />
2057
+ <MaskedDemo
2058
+ title="Clave con letras en MAYÚSCULA"
2059
+ mask="A-A-A-999"
2060
+ hint="Token A auto-mayúscula"
2061
+ initial="ABC123"
2062
+ />
2063
+ </div>
2064
+ }
2065
+ doc={{
2066
+ summary: "Input con máscara: value/onChange entregan solo los caracteres útiles y los separadores se auto-insertan.",
2067
+ description:
2068
+ "Define el patrón con tokens (9=dígito, A=letra mayúscula, a=letra, x=alfanumérico, *=cualquiera) y literales (todo lo demás). Los huecos vacíos se pintan con placeholderChar ('_').",
2069
+ examples: [
2070
+ '<ITMaskedInput name="code" mask="xxxx-xxxx-xxxx" onChange={(e) => setCode(e.target.value)} />',
2071
+ '<ITMaskedInput name="phone" mask="(999) 999-9999" pattern={/^\\d{10}$/} onChange={setPhone} />',
2072
+ '<ITMaskedInput name="card" mask="9999-9999-9999-9999" onComplete={validateCard} />',
2073
+ ],
2074
+ props: [
2075
+ { name: "mask", type: "string", required: true, description: "Patrón de la máscara (tokens + literales)." },
2076
+ { name: "value", type: "string", description: "Valor limpio (raw): solo caracteres útiles." },
2077
+ { name: "onChange", type: "(e) => void", description: "e.target.value = raw sin separadores." },
2078
+ { name: "onComplete", type: "(value) => void", description: "Dispara al llenar toda la máscara." },
2079
+ { name: "pattern", type: "RegExp", description: "Validación full-match del raw en blur." },
2080
+ { name: "placeholderChar", type: "string", default: '"_"', description: "Relleno de slots vacíos." },
2081
+ { name: "variant", type: "ColorsTypes", default: '"primary"', description: "Color de acento." },
2082
+ { name: "size", type: '"sm" | "md" | "lg"', default: '"md"', description: "Tamaño del input." },
2083
+ { name: "error", type: "string | boolean", description: "Estado de error." },
2084
+ ],
2085
+ notes: [
2086
+ "Los tokens A guardan letras en mayúsculas automáticamente.",
2087
+ "Borrar un separador lo vuelve a insertar; el cursor se conserva en la posición correcta.",
2088
+ "El cursor se preserva también al editar en medio de la máscara.",
2089
+ ],
2090
+ }}
2091
+ />
2092
+ );
2093
+ };
@@ -209,14 +209,14 @@ export const HomeShowcase = () => {
209
209
  <ITGrid item xs={12} md={6}>
210
210
  <ITCard title="Frameworks" className="border-slate-200/60 dark:border-slate-800/60 shadow-sm">
211
211
  <ITStack spacing={4}>
212
- <ITGrid container spacing={2}>
212
+ <ITGrid container spacing={3}>
213
213
  {FRAMEWORKS.map((fw) => (
214
- <ITGrid item xs={3} key={fw.name}>
215
- <div className="group flex flex-col items-center gap-2 p-3 rounded-xl bg-white/80 dark:bg-slate-900/60 border border-slate-100 dark:border-slate-800 hover:border-purple-400/40 hover:shadow-md hover:-translate-y-0.5 transition-all duration-200">
216
- <div className="w-9 h-9 flex items-center justify-center rounded-lg bg-slate-50 dark:bg-slate-950/40 border border-slate-100 dark:border-slate-800/80 group-hover:scale-110 transition-transform duration-200">
217
- <img src={fw.icon} alt={fw.name} className="w-6 h-6 object-contain" />
214
+ <ITGrid item xs={4} sm={4} key={fw.name}>
215
+ <div className="group flex flex-col items-center gap-2.5 p-3.5 rounded-xl bg-white/80 dark:bg-slate-900/60 border border-slate-100 dark:border-slate-800 hover:border-primary-400/40 hover:shadow-md hover:-translate-y-0.5 transition-all duration-200">
216
+ <div className="w-10 h-10 flex items-center justify-center rounded-lg bg-slate-50 dark:bg-slate-950/40 border border-slate-100 dark:border-slate-800/80 group-hover:scale-110 transition-transform duration-200">
217
+ <img src={fw.icon} alt={fw.name} className="w-7 h-7 object-contain" />
218
218
  </div>
219
- <ITText as="span" className="text-[10px] font-semibold text-slate-600 dark:text-slate-300 text-center leading-tight truncate w-full">
219
+ <ITText as="span" className="text-[11px] font-semibold text-slate-600 dark:text-slate-300 text-center">
220
220
  {fw.name}
221
221
  </ITText>
222
222
  </div>