@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.
- package/dist/index.cjs +3067 -1409
- package/dist/index.cjs.map +1 -1
- package/dist/index.css +1 -1
- package/dist/index.d.cts +620 -54
- package/dist/index.d.ts +620 -54
- package/dist/index.js +2983 -1320
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/App.tsx +28 -0
- package/src/components/atoms/badget/badget.tsx +2 -4
- package/src/components/atoms/chip/chip.doc.mdx +47 -0
- package/src/components/atoms/chip/chip.props.ts +32 -0
- package/src/components/atoms/chip/chip.stories.tsx +64 -0
- package/src/components/atoms/chip/chip.tsx +112 -0
- package/src/components/atoms/grid/grid.tsx +1 -2
- package/src/components/molecules/accordion/accordion.doc.mdx +47 -0
- package/src/components/molecules/accordion/accordion.props.ts +35 -0
- package/src/components/molecules/accordion/accordion.stories.tsx +68 -0
- package/src/components/molecules/accordion/accordion.tsx +123 -0
- package/src/components/molecules/chip-input/chip-input.doc.mdx +44 -0
- package/src/components/molecules/chip-input/chip-input.props.ts +42 -0
- package/src/components/molecules/chip-input/chip-input.stories.tsx +52 -0
- package/src/components/molecules/chip-input/chip-input.tsx +205 -0
- package/src/components/molecules/date-picker/datePicker.tsx +45 -56
- package/src/components/molecules/dropdown-menu/dropdown-menu.doc.mdx +46 -0
- package/src/components/molecules/dropdown-menu/dropdown-menu.props.ts +41 -0
- package/src/components/molecules/dropdown-menu/dropdown-menu.stories.tsx +41 -0
- package/src/components/molecules/dropdown-menu/dropdown-menu.tsx +219 -0
- package/src/components/molecules/field/field.doc.mdx +43 -0
- package/src/components/molecules/field/field.props.ts +23 -0
- package/src/components/molecules/field/field.stories.tsx +42 -0
- package/src/components/molecules/field/field.tsx +54 -0
- package/src/components/molecules/multi-select/multi-select.doc.mdx +55 -0
- package/src/components/molecules/multi-select/multi-select.props.ts +65 -0
- package/src/components/molecules/multi-select/multi-select.stories.tsx +59 -0
- package/src/components/molecules/multi-select/multi-select.tsx +386 -0
- package/src/components/molecules/search-select/search-select.doc.mdx +5 -0
- package/src/components/molecules/search-select/search-select.props.ts +10 -1
- package/src/components/molecules/search-select/search-select.stories.tsx +18 -0
- package/src/components/molecules/search-select/search-select.tsx +102 -28
- package/src/components/molecules/time-picker/timePicker.tsx +14 -26
- package/src/components/molecules/wysiwyg/wysiwyg.props.ts +51 -0
- package/src/components/molecules/wysiwyg/wysiwyg.stories.tsx +95 -0
- package/src/components/molecules/wysiwyg/wysiwyg.tsx +609 -0
- package/src/components/organisms/data-table/dataTable.tsx +16 -5
- package/src/components/organisms/form-builder/fieldRenderer.tsx +144 -27
- package/src/components/organisms/form-builder/formBuilder.context.tsx +77 -15
- package/src/components/organisms/form-builder/formBuilder.doc.mdx +213 -86
- package/src/components/organisms/form-builder/formBuilder.props.ts +118 -36
- package/src/components/organisms/form-builder/formBuilder.stories.tsx +694 -96
- package/src/components/organisms/form-builder/formBuilder.tsx +152 -69
- package/src/components/organisms/form-builder/useFormBuilder.ts +59 -21
- package/src/hooks/useFloatingPanel.ts +100 -0
- package/src/index.ts +24 -4
- package/src/showcases/DataShowcases.tsx +76 -0
- package/src/showcases/FormShowcases.tsx +396 -10
- package/src/showcases/HomeShowcase.tsx +6 -6
- package/src/showcases/NavigationShowcases.tsx +72 -2
- package/src/showcases/StructureShowcases.tsx +64 -0
- 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.
|
|
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={
|
|
212
|
+
<ITGrid container spacing={3}>
|
|
213
213
|
{FRAMEWORKS.map((fw) => (
|
|
214
|
-
<ITGrid item xs={
|
|
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-
|
|
216
|
-
<div className="w-
|
|
217
|
-
<img src={fw.icon} alt={fw.name} className="w-
|
|
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-[
|
|
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
|
+
};
|