@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.
- package/dist/index.cjs +3349 -1457
- package/dist/index.cjs.map +1 -1
- package/dist/index.css +1 -1
- package/dist/index.d.cts +719 -54
- package/dist/index.d.ts +719 -54
- package/dist/index.js +3283 -1387
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/App.tsx +32 -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/calendar/calendar.doc.mdx +2 -1
- package/src/components/molecules/calendar/calendar.tsx +102 -24
- 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/masked-input/masked-input.doc.mdx +78 -0
- package/src/components/molecules/masked-input/masked-input.props.ts +58 -0
- package/src/components/molecules/masked-input/masked-input.stories.tsx +126 -0
- package/src/components/molecules/masked-input/masked-input.tsx +244 -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/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 +26 -4
- package/src/showcases/DataShowcases.tsx +76 -0
- package/src/showcases/FormShowcases.tsx +574 -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
|
@@ -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.
|
|
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={
|
|
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>
|