@axzydev/axzy_ui_system 1.2.15 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/dist/index.cjs +3067 -1409
  2. package/dist/index.cjs.map +1 -1
  3. package/dist/index.css +1 -1
  4. package/dist/index.d.cts +620 -54
  5. package/dist/index.d.ts +620 -54
  6. package/dist/index.js +2983 -1320
  7. package/dist/index.js.map +1 -1
  8. package/package.json +1 -1
  9. package/src/App.tsx +28 -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/chip-input/chip-input.doc.mdx +44 -0
  21. package/src/components/molecules/chip-input/chip-input.props.ts +42 -0
  22. package/src/components/molecules/chip-input/chip-input.stories.tsx +52 -0
  23. package/src/components/molecules/chip-input/chip-input.tsx +205 -0
  24. package/src/components/molecules/date-picker/datePicker.tsx +45 -56
  25. package/src/components/molecules/dropdown-menu/dropdown-menu.doc.mdx +46 -0
  26. package/src/components/molecules/dropdown-menu/dropdown-menu.props.ts +41 -0
  27. package/src/components/molecules/dropdown-menu/dropdown-menu.stories.tsx +41 -0
  28. package/src/components/molecules/dropdown-menu/dropdown-menu.tsx +219 -0
  29. package/src/components/molecules/field/field.doc.mdx +43 -0
  30. package/src/components/molecules/field/field.props.ts +23 -0
  31. package/src/components/molecules/field/field.stories.tsx +42 -0
  32. package/src/components/molecules/field/field.tsx +54 -0
  33. package/src/components/molecules/multi-select/multi-select.doc.mdx +55 -0
  34. package/src/components/molecules/multi-select/multi-select.props.ts +65 -0
  35. package/src/components/molecules/multi-select/multi-select.stories.tsx +59 -0
  36. package/src/components/molecules/multi-select/multi-select.tsx +386 -0
  37. package/src/components/molecules/search-select/search-select.doc.mdx +5 -0
  38. package/src/components/molecules/search-select/search-select.props.ts +10 -1
  39. package/src/components/molecules/search-select/search-select.stories.tsx +18 -0
  40. package/src/components/molecules/search-select/search-select.tsx +102 -28
  41. package/src/components/molecules/time-picker/timePicker.tsx +14 -26
  42. package/src/components/molecules/wysiwyg/wysiwyg.props.ts +51 -0
  43. package/src/components/molecules/wysiwyg/wysiwyg.stories.tsx +95 -0
  44. package/src/components/molecules/wysiwyg/wysiwyg.tsx +609 -0
  45. package/src/components/organisms/data-table/dataTable.tsx +16 -5
  46. package/src/components/organisms/form-builder/fieldRenderer.tsx +144 -27
  47. package/src/components/organisms/form-builder/formBuilder.context.tsx +77 -15
  48. package/src/components/organisms/form-builder/formBuilder.doc.mdx +213 -86
  49. package/src/components/organisms/form-builder/formBuilder.props.ts +118 -36
  50. package/src/components/organisms/form-builder/formBuilder.stories.tsx +694 -96
  51. package/src/components/organisms/form-builder/formBuilder.tsx +152 -69
  52. package/src/components/organisms/form-builder/useFormBuilder.ts +59 -21
  53. package/src/hooks/useFloatingPanel.ts +100 -0
  54. package/src/index.ts +24 -4
  55. package/src/showcases/DataShowcases.tsx +76 -0
  56. package/src/showcases/FormShowcases.tsx +396 -10
  57. package/src/showcases/HomeShowcase.tsx +6 -6
  58. package/src/showcases/NavigationShowcases.tsx +72 -2
  59. package/src/showcases/StructureShowcases.tsx +64 -0
  60. package/src/types/chip.types.ts +13 -0
@@ -8,6 +8,7 @@ import {
8
8
  ITInput,
9
9
  ITSelect,
10
10
  ITSlideToggle,
11
+ ITChip,
11
12
  ITDataTableFetchParams,
12
13
  ITDataTableResponse
13
14
  } from "../index";
@@ -738,3 +739,78 @@ export const ImageShowcase = () => {
738
739
  />
739
740
  );
740
741
  };
742
+
743
+ // ITChip Showcase
744
+ export const ChipShowcase = () => {
745
+ const [active, setActive] = useState<string[]>(["mx"]);
746
+
747
+ const toggle = (id: string) =>
748
+ setActive((prev) => (prev.includes(id) ? prev.filter((x) => x !== id) : [...prev, id]));
749
+
750
+ const filters = [
751
+ { id: "mx", label: "México" },
752
+ { id: "es", label: "España" },
753
+ { id: "co", label: "Colombia" },
754
+ ];
755
+
756
+ const code = `<ITChip\n label="React"\n color="primary"\n variant="soft"\n removable\n onRemove={() => remove()}\n/>`;
757
+
758
+ return (
759
+ <ShowcaseLayout
760
+ title="ITChip"
761
+ description="Etiqueta compacta (tag / pill) para labels, filtros activos y selecciones múltiples. Variantes soft, filled y outlined, icono, botón de quitar (X) y estado seleccionado."
762
+ code={code}
763
+ demo={
764
+ <div className="w-full max-w-lg space-y-5">
765
+ <div className="flex flex-wrap items-center gap-2">
766
+ <ITChip label="Soft" variant="soft" color="primary" />
767
+ <ITChip label="Filled" variant="filled" color="success" />
768
+ <ITChip label="Outlined" variant="outlined" color="purple" />
769
+ </div>
770
+ <div className="flex flex-wrap items-center gap-2">
771
+ {filters.map((f) => (
772
+ <ITChip
773
+ key={f.id}
774
+ label={f.label}
775
+ color="primary"
776
+ selected={active.includes(f.id)}
777
+ onClick={() => toggle(f.id)}
778
+ />
779
+ ))}
780
+ </div>
781
+ <div className="flex flex-wrap items-center gap-2">
782
+ <ITChip label="México" color="danger" variant="outlined" removable onRemove={() => {}} />
783
+ <ITChip label="Deshabilitado" color="secondary" disabled />
784
+ </div>
785
+ </div>
786
+ }
787
+ controls={
788
+ <p className="text-xs text-slate-500">
789
+ Los chips con onClick son toggles de filtro (role=button, Enter/Space). Los chips removibles muestran una X.
790
+ </p>
791
+ }
792
+ doc={{
793
+ summary: "Etiqueta/pill para labels, filtros y multi-valor, con variantes y estados.",
794
+ examples: [
795
+ '<ITChip label="React" color="primary" />',
796
+ '<ITChip label="México" color="success" variant="outlined" removable onRemove={remove} />',
797
+ '<ITChip label="Activos" selected onClick={toggle} />',
798
+ ],
799
+ props: [
800
+ { name: "label", type: "string", description: "Texto del chip." },
801
+ { name: "children", type: "ReactNode", description: "Contenido custom (sobreescribe label)." },
802
+ { name: "color", type: "ColorsTypes", default: '"secondary"', description: "Color de la paleta." },
803
+ { name: "size", type: '"sm" | "md" | "lg"', default: '"md"', description: "Tamaño." },
804
+ { name: "variant", type: '"soft" | "filled" | "outlined"', default: '"soft"', description: "Estilo visual." },
805
+ { name: "icon", type: "ReactNode", description: "Icono a la izquierda." },
806
+ { name: "removable", type: "boolean", default: "false", description: "Muestra botón (X)." },
807
+ { name: "onRemove", type: "() => void", description: "Se dispara al quitar." },
808
+ { name: "onClick", type: "() => void", description: "Hace el chip clickeable." },
809
+ { name: "selected", type: "boolean", default: "false", description: "Estado seleccionado." },
810
+ { name: "disabled", type: "boolean", default: "false", description: "Deshabilitado." },
811
+ ],
812
+ notes: ["Colores desde theme.colors: respetan tema y modo oscuro."],
813
+ }}
814
+ />
815
+ );
816
+ };
@@ -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,11 @@ import {
11
11
  ITSlideToggle,
12
12
  ITDropfile,
13
13
  ITFormBuilder,
14
+ ITWysiwyg,
15
+ ITField,
16
+ ITMultiSelect,
17
+ ITChipInput,
18
+ ITBadget,
14
19
  UploadStatus
15
20
  } from "../index";
16
21
  import { ShowcaseLayout, CodeViewer } from "./ShowcaseLayout";
@@ -750,25 +755,29 @@ export const SelectShowcase = () => {
750
755
  // 4. ITSearchSelect Showcase
751
756
  export const SearchSelectShowcase = () => {
752
757
  const [val, setVal] = useState<any>("");
758
+ const [customVal, setCustomVal] = useState<any>("MX");
759
+ const [clearVal, setClearVal] = useState<any>("ES");
753
760
  const [isLoading, setIsLoading] = useState(false);
754
761
  const [disabled, setDisabled] = useState(false);
755
762
 
756
763
  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" }
764
+ { label: "Argentina", value: "AR", flag: "🇦🇷", region: "Sudamérica" },
765
+ { label: "Brasil", value: "BR", flag: "🇧🇷", region: "Sudamérica" },
766
+ { label: "Colombia", value: "CO", flag: "🇨🇴", region: "Sudamérica" },
767
+ { label: "México", value: "MX", flag: "🇲🇽", region: "Norteamérica" },
768
+ { label: "Perú", value: "PE", flag: "🇵🇪", region: "Sudamérica" },
769
+ { label: "España", value: "ES", flag: "🇪🇸", region: "Europa" }
763
770
  ];
764
771
 
765
772
  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
773
 
774
+ 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/>`;
775
+
767
776
  return (
768
777
  <ShowcaseLayout
769
778
  title="ITSearchSelect"
770
779
  description="Selector avanzado con barra de búsqueda para filtrar colecciones grandes o cargar opciones remotas."
771
- code={code}
780
+ code={`${code}\n\n${customCode}`}
772
781
  demo={
773
782
  <div className="w-full max-w-sm">
774
783
  <ITSearchSelect
@@ -785,6 +794,50 @@ export const SearchSelectShowcase = () => {
785
794
  )}
786
795
  </div>
787
796
  }
797
+ gallery={
798
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-8">
799
+ <div className="w-full max-w-sm">
800
+ <p className="mb-3 text-[10px] font-bold uppercase tracking-wider text-slate-400">
801
+ Template personalizado (renderOption)
802
+ </p>
803
+ <ITSearchSelect
804
+ name="country_custom"
805
+ label="País (template personalizado)"
806
+ value={customVal}
807
+ options={options}
808
+ onChange={(value) => setCustomVal(value)}
809
+ renderOption={(option, { isSelected }) => (
810
+ <div className="flex items-center gap-2">
811
+ <span>{option.flag}</span>
812
+ <span className="flex-1">{option.label}</span>
813
+ <span className="text-xs text-secondary-400">{option.region}</span>
814
+ {isSelected && <FaCheck className="text-primary-500" size={10} />}
815
+ </div>
816
+ )}
817
+ />
818
+ <p className="mt-2 text-xs text-slate-500 font-mono">
819
+ {customVal ? `País seleccionado: "${customVal}"` : "Sin selección"}
820
+ </p>
821
+ </div>
822
+
823
+ <div className="w-full max-w-sm">
824
+ <p className="mb-3 text-[10px] font-bold uppercase tracking-wider text-slate-400">
825
+ Botón de limpiar (X)
826
+ </p>
827
+ <ITSearchSelect
828
+ name="country_clear"
829
+ label="País (clearable)"
830
+ value={clearVal}
831
+ options={options}
832
+ onChange={(value) => setClearVal(value)}
833
+ onClear={() => setClearVal("")}
834
+ />
835
+ <p className="mt-2 text-xs text-slate-500 font-mono">
836
+ {clearVal ? `País seleccionado: "${clearVal}"` : "Sin selección"}
837
+ </p>
838
+ </div>
839
+ </div>
840
+ }
788
841
  controls={
789
842
  <>
790
843
  <div className="flex items-center justify-between">
@@ -1120,7 +1173,98 @@ export const DropfileShowcase = () => {
1120
1173
  );
1121
1174
  };
1122
1175
 
1123
- // 10. ITFormBuilder Showcase
1176
+ // 10. ITWysiwyg Showcase
1177
+ export const WysiwygShowcase = () => {
1178
+ const [html, setHtml] = useState(
1179
+ "<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>"
1180
+ );
1181
+ const [size, setSize] = useState<any>("md");
1182
+ const [error, setError] = useState<string | undefined>(undefined);
1183
+
1184
+ const code = `<ITWysiwyg\n label="Descripción"\n value={html}\n onChange={setHtml}\n size="${size}"\n highlightColor="#fde68a"\n/>`;
1185
+
1186
+ return (
1187
+ <ShowcaseLayout
1188
+ title="ITWysiwyg"
1189
+ description="Editor WYSIWYG ligero sin dependencias externas: negrita, itálica, subrayado, marcador amarillo y listas. Implementado a mano sobre Selection/Range (sin execCommand)."
1190
+ code={code}
1191
+ doc={{
1192
+ summary:
1193
+ "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.",
1194
+ description:
1195
+ "Soporta bold, italic, underline, marcador amarillo configurable y listas con viñetas/numeradas. Las operaciones togglean envolviendo/desenvolviendo elementos, sin usar document.execCommand.",
1196
+ examples: [
1197
+ '<ITWysiwyg label="Descripción" value={html} onChange={setHtml} />',
1198
+ '<ITWysiwyg size="lg" highlightColor="#fef08a" toolbar={["bold", "ul"]} />',
1199
+ ],
1200
+ props: [
1201
+ { name: "value", type: "string", description: "HTML controlado. Se refleja solo cuando el editor no está enfocado." },
1202
+ { name: "onChange", type: "(html: string) => void", description: "Devuelve el HTML del editor al cambiar." },
1203
+ { name: "label", type: "string", description: "Texto de etiqueta sobre el editor." },
1204
+ { name: "placeholder", type: "string", description: "Texto fantasma cuando el editor está vacío." },
1205
+ { name: "error", type: "string", description: "Mensaje de validación (borde/ring en rojo)." },
1206
+ { name: "disabled", type: "boolean", default: "false", description: "Deshabilita edición y toolbar." },
1207
+ { name: "readOnly", type: "boolean", default: "false", description: "Solo lectura." },
1208
+ { name: "size", type: "sm | md | lg", default: "md", description: "Padding y fuente del área editable." },
1209
+ { name: "minHeight", type: "number", default: "128", description: "Altura mínima en px del área editable." },
1210
+ { name: "highlightColor", type: "string", default: "#fde68a", description: "Color del marcador." },
1211
+ { name: "toolbar", type: "ToolbarAction[]", default: "todas", description: "Subconjunto de acciones: bold, italic, underline, highlight, ul, ol, clear." },
1212
+ ],
1213
+ notes: [
1214
+ "Las listas operan por bloques de primer nivel (no dividen selecciones parciales dentro de un bloque).",
1215
+ "El pegado se inserta como texto plano (sin HTML externo).",
1216
+ "Al estar enfocado el editor, cambios externos del value no se aplican para evitar saltos del cursor.",
1217
+ ],
1218
+ }}
1219
+ demo={
1220
+ <div className="w-full max-w-2xl space-y-4">
1221
+ <ITWysiwyg
1222
+ label="Contenido del artículo"
1223
+ value={html}
1224
+ onChange={setHtml}
1225
+ size={size}
1226
+ error={error}
1227
+ placeholder="Escribe o pega tu contenido..."
1228
+ />
1229
+ <div className="p-4 bg-slate-950 rounded-xl border border-slate-800">
1230
+ <p className="text-[10px] font-bold uppercase tracking-wider text-slate-500 mb-2">
1231
+ HTML generado
1232
+ </p>
1233
+ <pre className="text-[11px] text-emerald-300 font-mono whitespace-pre-wrap break-words">
1234
+ {html}
1235
+ </pre>
1236
+ </div>
1237
+ </div>
1238
+ }
1239
+ controls={
1240
+ <>
1241
+ <ITSelect
1242
+ name="size_ctrl"
1243
+ label="Tamaño"
1244
+ value={size}
1245
+ onChange={(e: any) => setSize(e.target.value)}
1246
+ options={[
1247
+ { label: "Small", value: "sm" },
1248
+ { label: "Medium", value: "md" },
1249
+ { label: "Large", value: "lg" },
1250
+ ]}
1251
+ />
1252
+ <div className="flex items-center justify-between pt-2">
1253
+ <span className="text-sm font-semibold text-gray-700">Forzar error</span>
1254
+ <ITSlideToggle
1255
+ isOn={!!error}
1256
+ onToggle={(v) => setError(v ? "Este campo es obligatorio." : undefined)}
1257
+ activeColor="danger"
1258
+ size="sm"
1259
+ />
1260
+ </div>
1261
+ </>
1262
+ }
1263
+ />
1264
+ );
1265
+ };
1266
+
1267
+ // 11. ITFormBuilder Showcase
1124
1268
  export const FormBuilderShowcase = () => {
1125
1269
  const [values, setValues] = useState({
1126
1270
  name: "",
@@ -1161,7 +1305,38 @@ export const FormBuilderShowcase = () => {
1161
1305
  <div className="space-y-8">
1162
1306
  <ShowcaseLayout
1163
1307
  title="ITFormBuilder"
1164
- description="Generador dinámico de formularios basado en un esquema estructurado JSON."
1308
+ 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."
1309
+ doc={{
1310
+ summary:
1311
+ "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).",
1312
+ description:
1313
+ "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.",
1314
+ examples: [
1315
+ `<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} />`,
1316
+ `<ITFormBuilder fields={[{ name: "email", type: "text" }, { name: "pwd", type: "password" }]} values={...} handleChange={...} handleBlur={...} touched={...} errors={...} />`,
1317
+ ],
1318
+ props: [
1319
+ { name: "fields", type: "FieldConfig[]", description: "(V1, legacy) Definiciones de campo. Mantenido por compatibilidad." },
1320
+ { name: "config", type: "FieldConfigV2[]", description: "(V2) Definiciones de campo con motor de reglas, secciones y custom. Recomendado." },
1321
+ { name: "columns", type: "number", default: "12", description: "Número de columnas del grid responsive (1-12)." },
1322
+ { name: "values", type: "Record<string, unknown>", required: true, description: "Valores actuales del formulario, indexados por `name`." },
1323
+ { name: "handleChange", type: "(event) => void", required: true, description: "Handler onChange compatible con Formik." },
1324
+ { name: "handleBlur", type: "(event) => void", required: true, description: "Handler onBlur compatible con Formik." },
1325
+ { name: "touched", type: "Record<string, boolean>", required: true, description: "Estado de touched por campo." },
1326
+ { name: "errors", type: "Record<string, string>", required: true, description: "Errores de validación por campo." },
1327
+ { name: "setFieldValue", type: "(field, value, shouldValidate?) => Promise", description: "Setter programático de valor (Formik parity). Necesario para V2 `onChangeAction`/`dynamicProps`." },
1328
+ { name: "setFieldTouched", type: "(field, touched?, shouldValidate?) => Promise", description: "Setter programático de touched." },
1329
+ { name: "setFieldError", type: "(field, message?) => void", description: "Setter programático de error de campo." },
1330
+ { name: "isSubmitting", type: "boolean", default: "false", description: "Bandera de submit en curso; expuesta al contexto." },
1331
+ ],
1332
+ notes: [
1333
+ "Cuando se pasan ambos `config` y `fields`, gana `config` (V2).",
1334
+ "Si no se pasa `config` ni `fields` el componente renderiza un grid vacío.",
1335
+ "El hook `useFormBuilder` debe usarse dentro del subárbol de un `ITFormBuilder` con la API V2 (lanza un error claro fuera del provider).",
1336
+ "`validation` y `asyncValidation` NO se ejecutan automáticamente — intégralos con Yup en el Formik padre, o ejecútalos manualmente dentro de `onChangeAction`.",
1337
+ "Opciones async (`options: () => Promise<...>`) montan `ITSearchSelect` con un placeholder de spinner mientras cargan.",
1338
+ ],
1339
+ }}
1165
1340
  code={code}
1166
1341
  demo={
1167
1342
  <div className="w-full max-w-md space-y-6">
@@ -1527,3 +1702,214 @@ const CurrencyConverterExample = () => {
1527
1702
  </div>
1528
1703
  );
1529
1704
  };
1705
+
1706
+ // 12. ITField Showcase
1707
+ export const FieldShowcase = () => {
1708
+ const [email, setEmail] = useState("");
1709
+ const [showHelp, setShowHelp] = useState(true);
1710
+ const invalid = email.length > 0 && !email.includes("@");
1711
+
1712
+ 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>`;
1713
+
1714
+ return (
1715
+ <ShowcaseLayout
1716
+ title="ITField"
1717
+ description="Contenedor de campo de formulario que estandariza label, asterisco de requerido, texto de ayuda y mensaje de error alrededor de cualquier control."
1718
+ code={code}
1719
+ demo={
1720
+ <div className="w-full max-w-sm">
1721
+ <ITField
1722
+ label="Correo"
1723
+ htmlFor="email"
1724
+ required
1725
+ error={invalid ? "Ingresa un correo válido." : undefined}
1726
+ helpText={showHelp ? "Nunca compartimos tu correo." : undefined}
1727
+ >
1728
+ <ITInput name="email" value={email} onChange={(e: any) => setEmail(e.target.value)} onBlur={() => {}} />
1729
+ </ITField>
1730
+ </div>
1731
+ }
1732
+ controls={
1733
+ <div className="flex items-center justify-between">
1734
+ <span className="text-sm font-semibold text-gray-700">Mostrar ayuda</span>
1735
+ <ITSlideToggle isOn={showHelp} onToggle={setShowHelp} size="sm" />
1736
+ </div>
1737
+ }
1738
+ doc={{
1739
+ summary: "Wrapper que unifica label, requerido, ayuda y error para cualquier control.",
1740
+ examples: [
1741
+ '<ITField label="Email" htmlFor="email" required error={errors.email} helpText="...">',
1742
+ ' <ITInput name="email" value={email} onChange={onChange} />',
1743
+ '</ITField>',
1744
+ ],
1745
+ props: [
1746
+ { name: "label", type: "string", description: "Etiqueta sobre el control." },
1747
+ { name: "htmlFor", type: "string", description: "id del control (htmlFor)." },
1748
+ { name: "required", type: "boolean", default: "false", description: "Muestra asterisco rojo." },
1749
+ { name: "error", type: "string | boolean", description: "Mensaje de error o true para el genérico." },
1750
+ { name: "helpText", type: "string", description: "Texto de ayuda si no hay error." },
1751
+ { name: "children", type: "ReactNode", required: true, description: "El control del formulario." },
1752
+ ],
1753
+ notes: ["El error tiene prioridad sobre helpText; solo se muestra uno."],
1754
+ }}
1755
+ />
1756
+ );
1757
+ };
1758
+
1759
+ // 13. ITMultiSelect Showcase
1760
+ export const MultiSelectShowcase = () => {
1761
+ const [value, setValue] = useState<(string | number)[]>(["mx", "es"]);
1762
+
1763
+ const options = [
1764
+ { value: "mx", label: "México" },
1765
+ { value: "es", label: "España" },
1766
+ { value: "co", label: "Colombia" },
1767
+ { value: "ar", label: "Argentina" },
1768
+ { value: "pe", label: "Perú" },
1769
+ { value: "cl", label: "Chile" },
1770
+ ];
1771
+
1772
+ 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/>`;
1773
+
1774
+ return (
1775
+ <ShowcaseLayout
1776
+ title="ITMultiSelect"
1777
+ 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."
1778
+ code={code}
1779
+ demo={
1780
+ <div className="w-full max-w-sm">
1781
+ <ITMultiSelect
1782
+ label="Países"
1783
+ options={options}
1784
+ value={value}
1785
+ onChange={(values) => setValue(values)}
1786
+ maxVisibleChips={3}
1787
+ />
1788
+ <p className="mt-2 text-xs text-slate-500 font-mono">
1789
+ {value.length} seleccionados: {JSON.stringify(value)}
1790
+ </p>
1791
+ </div>
1792
+ }
1793
+ controls={
1794
+ <div className="space-y-2">
1795
+ <p className="text-xs text-slate-500">
1796
+ Usa las flechas para navegar, Enter para alternar y Backspace para quitar el último chip.
1797
+ </p>
1798
+ </div>
1799
+ }
1800
+ gallery={
1801
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-8">
1802
+ <div className="w-full max-w-sm">
1803
+ <p className="mb-3 text-[10px] font-bold uppercase tracking-wider text-slate-400">Plantilla de opción (renderOption)</p>
1804
+ <ITMultiSelect
1805
+ label="Skills (template)"
1806
+ options={options}
1807
+ value={value}
1808
+ onChange={(values) => setValue(values)}
1809
+ renderOption={(option, { isSelected }) => (
1810
+ <span className={isSelected ? "font-bold" : ""}>{option.label}</span>
1811
+ )}
1812
+ />
1813
+ </div>
1814
+ <div className="w-full max-w-sm">
1815
+ <p className="mb-3 text-[10px] font-bold uppercase tracking-wider text-slate-400">Con error</p>
1816
+ <ITMultiSelect
1817
+ label="Requerido"
1818
+ options={options}
1819
+ value={[]}
1820
+ onChange={() => {}}
1821
+ required
1822
+ touched
1823
+ error
1824
+ />
1825
+ </div>
1826
+ </div>
1827
+ }
1828
+ doc={{
1829
+ summary: "Select múltiple con búsqueda, chips removibles y navegación por teclado.",
1830
+ examples: [
1831
+ '<ITMultiSelect label="Skills" options={skills} value={selected} onChange={setSelected} />',
1832
+ '<ITMultiSelect options={opts} value={v} onChange={setV} renderOption={(o, { isSelected }) => ...} />',
1833
+ ],
1834
+ props: [
1835
+ { name: "options", type: "ITMultiSelectOption[]", required: true, description: "Opciones { label, value, ...custom }." },
1836
+ { name: "value", type: "(string | number)[]", required: true, description: "Valores seleccionados (controlado)." },
1837
+ { name: "onChange", type: "(values, options) => void", description: "Devuelve los valores seleccionados." },
1838
+ { name: "label", type: "string", description: "Etiqueta del control." },
1839
+ { name: "placeholder", type: "string", default: '"Selecciona opciones"', description: "Texto cuando está vacío." },
1840
+ { name: "maxVisibleChips", type: "number", default: "3", description: "Chips visibles antes de +N." },
1841
+ { name: "clearable", type: "boolean", default: "true", description: "Botón de limpiar (X)." },
1842
+ { name: "searchable", type: "boolean", default: "true", description: "Habilita el input de búsqueda." },
1843
+ { name: "onSearch", type: "(query) => void", description: "Búsqueda remota." },
1844
+ { name: "renderOption", type: "(option, state) => ReactNode", description: "Plantilla de opción." },
1845
+ { name: "error", type: "string | boolean", description: "Estado de error." },
1846
+ ],
1847
+ notes: [
1848
+ "role=combobox + listbox con aria-multiselectable.",
1849
+ "Backspace con input vacío elimina el último chip.",
1850
+ ],
1851
+ }}
1852
+ />
1853
+ );
1854
+ };
1855
+
1856
+ // 14. ITChipInput Showcase
1857
+ export const ChipInputShowcase = () => {
1858
+ const [tags, setTags] = useState<string[]>(["react", "typescript"]);
1859
+ const [maxTags, setMaxTags] = useState(5);
1860
+
1861
+ 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/>`;
1862
+
1863
+ return (
1864
+ <ShowcaseLayout
1865
+ title="ITChipInput"
1866
+ 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."
1867
+ code={code}
1868
+ demo={
1869
+ <div className="w-full max-w-sm">
1870
+ <ITChipInput
1871
+ label="Etiquetas"
1872
+ value={tags}
1873
+ onChange={setTags}
1874
+ maxTags={maxTags}
1875
+ placeholder="Agrega una etiqueta..."
1876
+ helpText="Enter o coma para agregar. Backspace para borrar la última."
1877
+ />
1878
+ <div className="mt-3 flex flex-wrap gap-1">
1879
+ {tags.map((t) => (
1880
+ <ITBadget key={t} label={t} color="primary" size="sm" />
1881
+ ))}
1882
+ </div>
1883
+ </div>
1884
+ }
1885
+ controls={
1886
+ <ITInput
1887
+ name="maxTags"
1888
+ label="Máximo de etiquetas"
1889
+ type="number"
1890
+ value={maxTags}
1891
+ onChange={(e: any) => setMaxTags(Number(e.target.value) || 0)}
1892
+ onBlur={() => {}}
1893
+ />
1894
+ }
1895
+ doc={{
1896
+ summary: "Input que convierte texto en etiquetas (chips) removibles con validación.",
1897
+ examples: [
1898
+ '<ITChipInput label="Etiquetas" value={tags} onChange={setTags} />',
1899
+ '<ITChipInput value={emails} onChange={setEmails} validate={(v) => isEmail(v) ? undefined : "Correo inválido"} />',
1900
+ ],
1901
+ props: [
1902
+ { name: "value", type: "string[]", required: true, description: "Etiquetas actuales (controlado)." },
1903
+ { name: "onChange", type: "(values: string[]) => void", description: "Devuelve las etiquetas." },
1904
+ { name: "delimiters", type: "string[]", default: '["Enter", ","]', description: "Teclas que confirman una etiqueta." },
1905
+ { name: "maxTags", type: "number", description: "Máximo de etiquetas." },
1906
+ { name: "allowDuplicates", type: "boolean", default: "false", description: "Permite duplicados." },
1907
+ { name: "validate", type: "(value) => string | undefined", description: "Rechaza una etiqueta con mensaje." },
1908
+ { name: "color", type: "ColorsTypes", default: '"primary"', description: "Color de los chips." },
1909
+ { name: "error", type: "string | boolean", description: "Estado de error." },
1910
+ ],
1911
+ notes: ["Backspace en input vacío elimina la última etiqueta."],
1912
+ }}
1913
+ />
1914
+ );
1915
+ };
@@ -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>
@@ -1,12 +1,14 @@
1
1
  import React, { useState } from "react";
2
- import { FaInfoCircle, FaSlidersH, FaRegBell, FaSync } from "react-icons/fa";
2
+ import { FaInfoCircle, FaSlidersH, FaRegBell, FaSync, FaEdit, FaCopy, FaTrash, FaDownload } from "react-icons/fa";
3
3
  import {
4
4
  ITTabs,
5
5
  ITStepper,
6
6
  ITPagination,
7
7
  ITTripleFilter,
8
8
  ITSelect,
9
- ITSlideToggle
9
+ ITSlideToggle,
10
+ ITDropdownMenu,
11
+ ITCard
10
12
  } from "../index";
11
13
  import { ShowcaseLayout } from "./ShowcaseLayout";
12
14
 
@@ -191,3 +193,71 @@ export const TripleFilterShowcase = () => {
191
193
  />
192
194
  );
193
195
  };
196
+
197
+ // ITDropdownMenu Showcase
198
+ export const DropdownMenuShowcase = () => {
199
+ const [lastAction, setLastAction] = useState<string>("—");
200
+
201
+ const items = [
202
+ { id: "edit", label: "Editar", icon: <FaEdit />, onClick: () => setLastAction("Editar") },
203
+ { id: "copy", label: "Duplicar", icon: <FaCopy />, shortcut: "⌘D", onClick: () => setLastAction("Duplicar") },
204
+ { id: "download", label: "Descargar", icon: <FaDownload />, disabled: true },
205
+ { id: "delete", label: "Eliminar", icon: <FaTrash />, danger: true, divider: true, onClick: () => setLastAction("Eliminar") },
206
+ ];
207
+
208
+ const code = `<ITDropdownMenu\n items={[\n { id: 'edit', label: 'Editar', icon: <FaEdit />, onClick: edit },\n { id: 'delete', label: 'Eliminar', icon: <FaTrash />, danger: true, divider: true, onClick: remove },\n ]}\n/>`;
209
+
210
+ return (
211
+ <ShowcaseLayout
212
+ title="ITDropdownMenu"
213
+ description="Menú de acciones con navegación por teclado y semántica ARIA. Se renderiza en portal, por lo que no lo recorta un ITCard ni ancestros con overflow: hidden."
214
+ code={code}
215
+ demo={
216
+ <div className="w-full max-w-md">
217
+ <ITCard title="Acciones de fila">
218
+ <div className="flex items-center justify-between">
219
+ <div>
220
+ <p className="text-sm font-semibold text-slate-700 dark:text-slate-200">Reporte Q3.pdf</p>
221
+ <p className="text-xs text-slate-500">Última acción: {lastAction}</p>
222
+ </div>
223
+ <ITDropdownMenu items={items} />
224
+ </div>
225
+ </ITCard>
226
+ </div>
227
+ }
228
+ controls={
229
+ <p className="text-xs text-slate-500">
230
+ Teclado: flechas para navegar, Enter para activar, Esc para cerrar. El menú se abre dentro del ITCard sin recortarse.
231
+ </p>
232
+ }
233
+ gallery={
234
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-8">
235
+ <div className="w-full max-w-xs">
236
+ <p className="mb-3 text-[10px] font-bold uppercase tracking-wider text-slate-400">Trigger personalizado</p>
237
+ <ITDropdownMenu items={items} trigger={<span className="text-sm font-semibold px-1">Acciones</span>} triggerLabel="Acciones" placement="bottom-start" />
238
+ </div>
239
+ <div className="w-full max-w-xs">
240
+ <p className="mb-3 text-[10px] font-bold uppercase tracking-wider text-slate-400">Placement top-start</p>
241
+ <ITDropdownMenu items={items.slice(0, 3)} placement="top-start" />
242
+ </div>
243
+ </div>
244
+ }
245
+ doc={{
246
+ summary: "Menú de acciones accesible (role=menu) en portal, con teclado y flip automático.",
247
+ examples: [
248
+ "<ITDropdownMenu items={items} />",
249
+ "<ITDropdownMenu items={items} trigger={<span>Acciones</span>} placement=\"bottom-start\" />",
250
+ ],
251
+ props: [
252
+ { name: "items", type: "ITDropdownMenuItem[]", required: true, description: "Acciones { id, label, icon?, shortcut?, onClick?, disabled?, danger?, divider? }." },
253
+ { name: "trigger", type: "ReactNode", default: "icono elipsis", description: "Contenido del trigger." },
254
+ { name: "triggerLabel", type: "string", default: '"Abrir menú"', description: "Etiqueta accesible del trigger." },
255
+ { name: "placement", type: '"bottom-start" | "bottom-end" | "top-start" | "top-end"', default: '"bottom-end"', description: "Posición del menú." },
256
+ { name: "onSelect", type: "(id, item) => void", description: "Se dispara al activar un item." },
257
+ { name: "disabled", type: "boolean", default: "false", description: "Deshabilita el trigger." },
258
+ ],
259
+ notes: ["El panel voltea arriba cuando no hay espacio abajo.", "role=menu / menuitem + aria-haspopup / aria-expanded."],
260
+ }}
261
+ />
262
+ );
263
+ };