@axzydev/axzy_ui_system 1.3.0 → 1.3.2
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/README.md +6 -2
- package/dist/index.cjs +2342 -1715
- package/dist/index.cjs.map +1 -1
- package/dist/index.css +1 -1
- package/dist/index.d.cts +286 -9
- package/dist/index.d.ts +286 -9
- package/dist/index.js +2164 -1538
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/App.tsx +153 -153
- package/src/components/molecules/calendar/calendar.doc.mdx +2 -1
- package/src/components/molecules/calendar/calendar.tsx +102 -24
- 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/table/table.doc.mdx +64 -21
- package/src/components/molecules/table/table.props.ts +81 -2
- package/src/components/molecules/table/table.stories.tsx +155 -0
- package/src/components/molecules/table/table.tsx +177 -36
- package/src/components/organisms/data-table/ITDataTable.doc.mdx +37 -3
- package/src/components/organisms/data-table/ITDataTable.stories.tsx +43 -0
- package/src/components/organisms/data-table/dataTable.props.ts +64 -0
- package/src/components/organisms/data-table/dataTable.tsx +240 -82
- package/src/components/organisms/sidebar/sidebar.doc.mdx +5 -0
- package/src/components/organisms/sidebar/sidebar.props.ts +5 -0
- package/src/components/organisms/sidebar/sidebar.stories.tsx +1 -1
- package/src/components/organisms/sidebar/sidebar.tsx +27 -7
- package/src/components/organisms/topbar/topbar.doc.mdx +3 -0
- package/src/components/organisms/topbar/topbar.props.ts +16 -0
- package/src/components/organisms/topbar/topbar.tsx +133 -98
- package/src/components/templates/layout/layout.doc.mdx +2 -0
- package/src/components/templates/layout/layout.tsx +7 -1
- package/src/components/templates/page/page.doc.mdx +8 -2
- package/src/components/templates/page/page.props.ts +11 -1
- package/src/components/templates/page/page.tsx +11 -1
- package/src/dev.css +58 -1
- package/src/hooks/useElementSize.ts +46 -0
- package/src/hooks/useVirtualRows.ts +123 -0
- package/src/index.ts +6 -2
- package/src/sandbox/LandingShowcase.tsx +465 -0
- package/src/sandbox/NotFoundShowcase.tsx +70 -0
- package/src/sandbox/TopbarSearch.tsx +85 -0
- package/src/sandbox/navigation.ts +148 -0
- package/src/sandbox/useHashRoute.ts +149 -0
- package/src/showcases/DataShowcases.tsx +321 -24
- package/src/showcases/FormShowcases.tsx +178 -0
- package/src/showcases/PageShowcases.tsx +31 -0
- package/src/showcases/StructureShowcases.tsx +42 -4
- package/src/types/table.types.ts +48 -0
- package/src/utils/styles.ts +9 -4
- package/src/utils/table.utils.ts +31 -0
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { Meta, Title, Description, Primary, Controls, Stories } from '@storybook/blocks';
|
|
2
|
+
import * as StoriesData from './masked-input.stories';
|
|
3
|
+
|
|
4
|
+
<Meta of={StoriesData} />
|
|
5
|
+
|
|
6
|
+
# ITMaskedInput
|
|
7
|
+
|
|
8
|
+
Input con máscara de formato: defines un patrón y el usuario solo escribe los caracteres útiles; los separadores se insertan solos y los huecos se pintan con un carácter de relleno.
|
|
9
|
+
|
|
10
|
+
## Props
|
|
11
|
+
|
|
12
|
+
| Prop | Type | Default | Description |
|
|
13
|
+
|------|------|---------|-------------|
|
|
14
|
+
| `name` | `string` | required | Nombre del campo para formularios. |
|
|
15
|
+
| `mask` | `string` | required | Patrón: `9`=dígito, `A`=letra (mayúscula), `a`=letra, `x`=alfanumérico, `*`=cualquiera. Cualquier otro carácter es un separador literal auto-insertado. |
|
|
16
|
+
| `value` | `string` | — | Valor limpio (raw): solo caracteres útiles, sin separadores ni relleno. |
|
|
17
|
+
| `onChange` | `(e) => void` | required | `e.target.value` es el valor raw. |
|
|
18
|
+
| `onBlur` | `(e) => void` | — | Blur del input. |
|
|
19
|
+
| `onComplete` | `(value) => void` | — | Dispara cuando la máscara queda llena. |
|
|
20
|
+
| `pattern` | `RegExp` | — | Regex validada con full-match contra el raw en blur; si no cumple muestra error. |
|
|
21
|
+
| `placeholderChar` | `string` | `"_"` | Carácter pintado en slots vacíos. |
|
|
22
|
+
| `variant` | `ColorsTypes` | `"primary"` | Color de acento del tema. |
|
|
23
|
+
| `size` | `"sm" \| "md" \| "lg"` | `"md"` | Tamaño del input. |
|
|
24
|
+
| `label` | `string` | — | Etiqueta sobre el input. |
|
|
25
|
+
| `disabled` | `boolean` | `false` | Desactiva el input. |
|
|
26
|
+
| `required` | `boolean` | `false` | Marca el campo como requerido. |
|
|
27
|
+
| `touched` | `boolean` | `false` | Indica si el campo fue tocado. |
|
|
28
|
+
| `error` | `string \| boolean` | — | Estado de error. |
|
|
29
|
+
| `className` | `string` | — | Clases del wrapper externo. |
|
|
30
|
+
| `containerClassName` | `string` | — | Clases del contenedor del input. |
|
|
31
|
+
| `iconLeft` / `iconRight` | `ReactNode` | — | Iconos laterales. |
|
|
32
|
+
| `autoFocus` | `boolean` | `false` | Enfoca al montar. |
|
|
33
|
+
|
|
34
|
+
## Tokens del mask
|
|
35
|
+
|
|
36
|
+
| Token | Acepta |
|
|
37
|
+
|-------|--------|
|
|
38
|
+
| `9` | un dígito |
|
|
39
|
+
| `A` | una letra (se guarda en mayúsculas) |
|
|
40
|
+
| `a` | una letra (tal cual) |
|
|
41
|
+
| `x` | un carácter alfanumérico |
|
|
42
|
+
| `*` | cualquier carácter |
|
|
43
|
+
| cualquier otro | literal insertado solo (p.ej. `-`, `/`, `(`, `)`, espacio) |
|
|
44
|
+
|
|
45
|
+
## Usage
|
|
46
|
+
|
|
47
|
+
```tsx
|
|
48
|
+
import { ITMaskedInput } from '@axzydev/axzy_ui_system';
|
|
49
|
+
|
|
50
|
+
// Código de activación
|
|
51
|
+
<ITMaskedInput
|
|
52
|
+
name="code"
|
|
53
|
+
mask="xxxx-xxxx-xxxx"
|
|
54
|
+
label="Código"
|
|
55
|
+
onChange={(e) => setCode(e.target.value)}
|
|
56
|
+
/>
|
|
57
|
+
|
|
58
|
+
// Teléfono con validación regex sobre el valor limpio
|
|
59
|
+
<ITMaskedInput
|
|
60
|
+
name="phone"
|
|
61
|
+
mask="(999) 999-9999"
|
|
62
|
+
pattern={/^\d{10}$/}
|
|
63
|
+
onChange={(e) => setPhone(e.target.value)}
|
|
64
|
+
/>
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Notes
|
|
68
|
+
|
|
69
|
+
- ¿El valor? **Solo caracteres útiles**: escribir `ABC1-2345-6789` devuelve `ABC123456789` en `onChange`.
|
|
70
|
+
- El cursor salta a huecos vacíos con selección automática; borrar un separador lo vuelve a insertar.
|
|
71
|
+
- `pattern` valida contra el raw completo (full-match); el mensaje de error genérico es "Formato inválido" salvo que pases `error` como string.
|
|
72
|
+
- `onComplete` es ideal para tarjetas, CVV o códigos de un solo uso.
|
|
73
|
+
|
|
74
|
+
## Stories
|
|
75
|
+
|
|
76
|
+
<Primary />
|
|
77
|
+
<Controls />
|
|
78
|
+
<Stories />
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { ColorsTypes } from "@/types/colors.types";
|
|
2
|
+
import { SizesTypes } from "@/types/sizes.types";
|
|
3
|
+
import { ReactNode } from "react";
|
|
4
|
+
|
|
5
|
+
export interface ITMaskedInputProps {
|
|
6
|
+
/** Unique name attribute for the underlying input element. */
|
|
7
|
+
name: string;
|
|
8
|
+
/**
|
|
9
|
+
* Mask pattern. Tokens are filled by the user and everything else is an
|
|
10
|
+
* auto-inserted literal.
|
|
11
|
+
* - `9` — digit
|
|
12
|
+
* - `A` — letter (stored uppercase)
|
|
13
|
+
* - `a` — letter
|
|
14
|
+
* - `x` — alphanumeric
|
|
15
|
+
* - `*` — any character
|
|
16
|
+
* - any other character ( `-`, `/`, `(`, `)`, ` `, `.` ) is a literal separator
|
|
17
|
+
* @example "xxxx-xxxx-xxxx"
|
|
18
|
+
* @example "(999) 999-9999"
|
|
19
|
+
* @example "9999-9999-9999-9999"
|
|
20
|
+
*/
|
|
21
|
+
mask: string;
|
|
22
|
+
/** Clean value containing only the useful characters (no separators, no placeholders). */
|
|
23
|
+
value?: string;
|
|
24
|
+
/** Called when the masked value changes. `event.target.value` is the raw value without separators. */
|
|
25
|
+
onChange: (event: { target: { name: string; value: string } }) => void;
|
|
26
|
+
/** Called when the input loses focus. */
|
|
27
|
+
onBlur?: (event: React.FocusEvent<HTMLInputElement>) => void;
|
|
28
|
+
/** Fired when all token slots are filled with valid characters. */
|
|
29
|
+
onComplete?: (value: string) => void;
|
|
30
|
+
/** Optional regex full-matched against the raw value to validate the field (e.g. `/^\\d{16}$/`). */
|
|
31
|
+
pattern?: RegExp;
|
|
32
|
+
/** Character shown for empty token slots. @default "_" */
|
|
33
|
+
placeholderChar?: string;
|
|
34
|
+
/** Color variant matching the design system (ColorsTypes). @default "primary" */
|
|
35
|
+
variant?: ColorsTypes;
|
|
36
|
+
/** Size preset: "sm" | "md" | "lg". @default "md" */
|
|
37
|
+
size?: SizesTypes;
|
|
38
|
+
/** Label displayed above the input. */
|
|
39
|
+
label?: string;
|
|
40
|
+
/** Disables the input. @default false */
|
|
41
|
+
disabled?: boolean;
|
|
42
|
+
/** Marks the field as required. */
|
|
43
|
+
required?: boolean;
|
|
44
|
+
/** Visually indicates the field has been interacted with. */
|
|
45
|
+
touched?: boolean;
|
|
46
|
+
/** Error state — pass a string message or `true` for default styling. */
|
|
47
|
+
error?: string | boolean;
|
|
48
|
+
/** Additional CSS classes for the outer wrapper element. */
|
|
49
|
+
className?: string;
|
|
50
|
+
/** Additional CSS classes for the underlying input container. */
|
|
51
|
+
containerClassName?: string;
|
|
52
|
+
/** Icon element rendered on the left side of the input. */
|
|
53
|
+
iconLeft?: ReactNode;
|
|
54
|
+
/** Icon element rendered on the right side of the input. */
|
|
55
|
+
iconRight?: ReactNode;
|
|
56
|
+
/** Auto-focus the input on mount. @default false */
|
|
57
|
+
autoFocus?: boolean;
|
|
58
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import type { Meta } from '@storybook/react';
|
|
2
|
+
import { useState } from 'react';
|
|
3
|
+
import ITMaskedInput from './masked-input';
|
|
4
|
+
|
|
5
|
+
const meta = {
|
|
6
|
+
title: 'Components/Form Elements/ITMaskedInput',
|
|
7
|
+
component: ITMaskedInput,
|
|
8
|
+
parameters: { layout: 'padded' },
|
|
9
|
+
argTypes: {
|
|
10
|
+
mask: { control: 'text' },
|
|
11
|
+
placeholderChar: { control: 'text' },
|
|
12
|
+
onChange: { action: 'changed' },
|
|
13
|
+
onComplete: { action: 'completed' },
|
|
14
|
+
disabled: { control: 'boolean' },
|
|
15
|
+
error: { control: 'text' },
|
|
16
|
+
variant: {
|
|
17
|
+
control: 'select',
|
|
18
|
+
options: ['primary', 'secondary', 'success', 'danger', 'warning', 'info', 'purple', 'gray'],
|
|
19
|
+
},
|
|
20
|
+
size: { control: 'select', options: ['sm', 'md', 'lg'] },
|
|
21
|
+
},
|
|
22
|
+
} satisfies Meta<typeof ITMaskedInput>;
|
|
23
|
+
|
|
24
|
+
export default meta;
|
|
25
|
+
|
|
26
|
+
function CodeMaskedWrapper(args: any) {
|
|
27
|
+
const [value, setValue] = useState<string>('');
|
|
28
|
+
return (
|
|
29
|
+
<div className="w-full max-w-xs">
|
|
30
|
+
<ITMaskedInput
|
|
31
|
+
{...args}
|
|
32
|
+
value={value}
|
|
33
|
+
onChange={(e) => {
|
|
34
|
+
setValue(e.target.value);
|
|
35
|
+
args.onChange(e);
|
|
36
|
+
}}
|
|
37
|
+
/>
|
|
38
|
+
<p className="mt-2 text-xs font-mono text-slate-500">
|
|
39
|
+
raw: {value || '(vacío)'}
|
|
40
|
+
</p>
|
|
41
|
+
</div>
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export const Alphanumeric: any = {
|
|
46
|
+
render: CodeMaskedWrapper,
|
|
47
|
+
args: {
|
|
48
|
+
name: 'code',
|
|
49
|
+
label: 'Código único',
|
|
50
|
+
mask: 'xxxx-xxxx-xxxx',
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export const Phone: any = {
|
|
55
|
+
render: CodeMaskedWrapper,
|
|
56
|
+
args: {
|
|
57
|
+
name: 'phone',
|
|
58
|
+
label: 'Teléfono',
|
|
59
|
+
mask: '(999) 999-9999',
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
export const CreditCard: any = {
|
|
64
|
+
render: CodeMaskedWrapper,
|
|
65
|
+
args: {
|
|
66
|
+
name: 'card',
|
|
67
|
+
label: 'Número de tarjeta',
|
|
68
|
+
mask: '9999-9999-9999-9999',
|
|
69
|
+
onComplete: undefined,
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const PhoneValidatedWrapper = (args: any) => {
|
|
74
|
+
const [value, setValue] = useState<string>('5512345678');
|
|
75
|
+
const [complete, setComplete] = useState<string | null>(null);
|
|
76
|
+
return (
|
|
77
|
+
<div className="w-full max-w-xs">
|
|
78
|
+
<ITMaskedInput
|
|
79
|
+
{...args}
|
|
80
|
+
value={value}
|
|
81
|
+
onChange={(e) => {
|
|
82
|
+
setValue(e.target.value);
|
|
83
|
+
args.onChange(e);
|
|
84
|
+
setComplete(null);
|
|
85
|
+
}}
|
|
86
|
+
onComplete={(v) => setComplete(v)}
|
|
87
|
+
/>
|
|
88
|
+
{complete && (
|
|
89
|
+
<p className="mt-2 text-xs font-mono text-emerald-600">
|
|
90
|
+
¡Completo! raw: {complete}
|
|
91
|
+
</p>
|
|
92
|
+
)}
|
|
93
|
+
</div>
|
|
94
|
+
);
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
export const WithPattern: any = {
|
|
98
|
+
render: PhoneValidatedWrapper,
|
|
99
|
+
args: {
|
|
100
|
+
name: 'validated_phone',
|
|
101
|
+
label: 'Teléfono válido (regex)',
|
|
102
|
+
mask: '999-999-9999',
|
|
103
|
+
pattern: /^\d{9}$/,
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
export const Sizes: any = {
|
|
108
|
+
render: () => (
|
|
109
|
+
<div className="flex flex-col gap-4 w-full max-w-xs">
|
|
110
|
+
<ITMaskedInput name="sz_sm" label="Small" mask="xxxx-xxxx" size="sm" onChange={() => {}} />
|
|
111
|
+
<ITMaskedInput name="sz_md" label="Medium" mask="xxxx-xxxx" size="md" onChange={() => {}} />
|
|
112
|
+
<ITMaskedInput name="sz_lg" label="Large" mask="xxxx-xxxx" size="lg" onChange={() => {}} />
|
|
113
|
+
</div>
|
|
114
|
+
),
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
export const Disabled: any = {
|
|
118
|
+
args: {
|
|
119
|
+
name: 'disabled_code',
|
|
120
|
+
label: 'Deshabilitado',
|
|
121
|
+
mask: 'xxxx-xxxx',
|
|
122
|
+
value: 'abcd',
|
|
123
|
+
disabled: true,
|
|
124
|
+
onChange: () => {},
|
|
125
|
+
},
|
|
126
|
+
};
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import React, { useRef, useState, useMemo } from "react";
|
|
2
|
+
import clsx from "clsx";
|
|
3
|
+
import ITInput from "@/components/atoms/input/input";
|
|
4
|
+
import { ITMaskedInputProps } from "./masked-input.props";
|
|
5
|
+
|
|
6
|
+
type Token = "9" | "A" | "a" | "x" | "*";
|
|
7
|
+
|
|
8
|
+
type MaskRule =
|
|
9
|
+
| { type: "token"; token: Token }
|
|
10
|
+
| { type: "literal"; char: string };
|
|
11
|
+
|
|
12
|
+
const TOKEN_SET = /[9Aax*]/;
|
|
13
|
+
|
|
14
|
+
function parseMask(mask: string): MaskRule[] {
|
|
15
|
+
return mask.split("").map((ch) =>
|
|
16
|
+
TOKEN_SET.test(ch)
|
|
17
|
+
? { type: "token", token: ch as Token }
|
|
18
|
+
: { type: "literal", char: ch }
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function matchToken(token: Token, ch: string): boolean {
|
|
23
|
+
switch (token) {
|
|
24
|
+
case "9":
|
|
25
|
+
return /\d/.test(ch);
|
|
26
|
+
case "A":
|
|
27
|
+
case "a":
|
|
28
|
+
return /[A-Za-z]/.test(ch);
|
|
29
|
+
case "x":
|
|
30
|
+
return /[A-Za-z0-9]/.test(ch);
|
|
31
|
+
case "*":
|
|
32
|
+
return ch.length > 0;
|
|
33
|
+
default:
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function extract(input: string, rules: MaskRule[], maskChar: string): string {
|
|
39
|
+
let raw = "";
|
|
40
|
+
let slot = 0;
|
|
41
|
+
for (const ch of input) {
|
|
42
|
+
while (slot < rules.length && rules[slot].type === "literal") slot++;
|
|
43
|
+
if (slot >= rules.length) break;
|
|
44
|
+
const rule = rules[slot];
|
|
45
|
+
if (
|
|
46
|
+
rule.type === "token" &&
|
|
47
|
+
ch !== maskChar &&
|
|
48
|
+
matchToken(rule.token, ch)
|
|
49
|
+
) {
|
|
50
|
+
raw += rule.token === "A" ? ch.toUpperCase() : ch;
|
|
51
|
+
slot++;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return raw;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function format(raw: string, rules: MaskRule[], maskChar: string): string {
|
|
58
|
+
let out = "";
|
|
59
|
+
let idx = 0;
|
|
60
|
+
for (const rule of rules) {
|
|
61
|
+
if (rule.type === "literal") {
|
|
62
|
+
out += rule.char;
|
|
63
|
+
} else {
|
|
64
|
+
const ch = raw[idx];
|
|
65
|
+
out += ch !== undefined ? ch : maskChar;
|
|
66
|
+
idx++;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return out;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function countConsumed(input: string, rules: MaskRule[], upto: number, maskChar: string): number {
|
|
73
|
+
let slot = 0;
|
|
74
|
+
let n = 0;
|
|
75
|
+
for (let i = 0; i < upto && i < input.length; i++) {
|
|
76
|
+
const ch = input[i];
|
|
77
|
+
while (slot < rules.length && rules[slot].type === "literal") slot++;
|
|
78
|
+
if (slot >= rules.length) break;
|
|
79
|
+
const rule = rules[slot];
|
|
80
|
+
if (rule.type === "token" && ch !== maskChar && matchToken(rule.token, ch)) {
|
|
81
|
+
n++;
|
|
82
|
+
slot++;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return n;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function caretAfter(n: number, rules: MaskRule[]): number {
|
|
89
|
+
let pos = 0;
|
|
90
|
+
let consumed = 0;
|
|
91
|
+
for (const rule of rules) {
|
|
92
|
+
if (rule.type === "token") {
|
|
93
|
+
consumed++;
|
|
94
|
+
if (consumed > n) break;
|
|
95
|
+
}
|
|
96
|
+
pos++;
|
|
97
|
+
}
|
|
98
|
+
return pos;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Text input that applies a formatting mask while the user types, exposing only
|
|
103
|
+
* the clean (raw) value through `onChange`.
|
|
104
|
+
*
|
|
105
|
+
* The mask is defined by a pattern string. Tokens are filled by the user and
|
|
106
|
+
* any other character acts as an auto-inserted literal separator. Empty slots
|
|
107
|
+
* are painted with `placeholderChar`, so the mask is always visible.
|
|
108
|
+
*
|
|
109
|
+
* @example
|
|
110
|
+
* ```tsx
|
|
111
|
+
* <ITMaskedInput
|
|
112
|
+
* name="code"
|
|
113
|
+
* mask="xxxx-xxxx-xxxx"
|
|
114
|
+
* label="Código"
|
|
115
|
+
* onChange={(e) => setCode(e.target.value)}
|
|
116
|
+
* />
|
|
117
|
+
* ```
|
|
118
|
+
*
|
|
119
|
+
* @example
|
|
120
|
+
* ```tsx
|
|
121
|
+
* <ITMaskedInput
|
|
122
|
+
* name="phone"
|
|
123
|
+
* mask="(999) 999-9999"
|
|
124
|
+
* pattern={/^\d{10}$/}
|
|
125
|
+
* onChange={(e) => setPhone(e.target.value)}
|
|
126
|
+
* />
|
|
127
|
+
* ```
|
|
128
|
+
*
|
|
129
|
+
* @example
|
|
130
|
+
* ```tsx
|
|
131
|
+
* <ITMaskedInput
|
|
132
|
+
* name="card"
|
|
133
|
+
* mask="9999-9999-9999-9999"
|
|
134
|
+
* onComplete={(card) => validateCard(card)}
|
|
135
|
+
* />
|
|
136
|
+
* ```
|
|
137
|
+
*/
|
|
138
|
+
export default function ITMaskedInput({
|
|
139
|
+
name,
|
|
140
|
+
mask,
|
|
141
|
+
value,
|
|
142
|
+
onChange,
|
|
143
|
+
onBlur,
|
|
144
|
+
onComplete,
|
|
145
|
+
pattern,
|
|
146
|
+
placeholderChar = "_",
|
|
147
|
+
variant = "primary",
|
|
148
|
+
size = "md",
|
|
149
|
+
label,
|
|
150
|
+
disabled = false,
|
|
151
|
+
required,
|
|
152
|
+
touched,
|
|
153
|
+
error,
|
|
154
|
+
className,
|
|
155
|
+
containerClassName,
|
|
156
|
+
iconLeft,
|
|
157
|
+
iconRight,
|
|
158
|
+
autoFocus = false,
|
|
159
|
+
}: ITMaskedInputProps) {
|
|
160
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
161
|
+
const rawRef = useRef<string>(value ?? "");
|
|
162
|
+
const isControlled = value !== undefined;
|
|
163
|
+
|
|
164
|
+
const [internalRaw, setInternalRaw] = useState<string>(value ?? "");
|
|
165
|
+
const [invalidPattern, setInvalidPattern] = useState(false);
|
|
166
|
+
|
|
167
|
+
const rules = useMemo(() => parseMask(mask), [mask]);
|
|
168
|
+
const tokenCount = useMemo(() => rules.filter((r) => r.type === "token").length, [rules]);
|
|
169
|
+
|
|
170
|
+
const raw = isControlled ? (value ?? "") : internalRaw;
|
|
171
|
+
const display = useMemo(() => format(raw, rules, placeholderChar), [raw, rules, placeholderChar]);
|
|
172
|
+
|
|
173
|
+
const scheduleCaret = (caret: number) => {
|
|
174
|
+
const inputEl = containerRef.current?.querySelector("input");
|
|
175
|
+
if (!inputEl) return;
|
|
176
|
+
requestAnimationFrame(() => {
|
|
177
|
+
if (document.activeElement === inputEl) {
|
|
178
|
+
const pos = Math.min(caret, inputEl.value.length);
|
|
179
|
+
inputEl.setSelectionRange(pos, pos);
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
185
|
+
const el = e.target as HTMLInputElement;
|
|
186
|
+
const inputVal = el.value ?? "";
|
|
187
|
+
const caretBefore = el.selectionStart ?? inputVal.length;
|
|
188
|
+
|
|
189
|
+
const nextRaw = extract(inputVal, rules, placeholderChar);
|
|
190
|
+
const targetCaret = caretAfter(
|
|
191
|
+
countConsumed(inputVal, rules, caretBefore, placeholderChar),
|
|
192
|
+
rules
|
|
193
|
+
);
|
|
194
|
+
|
|
195
|
+
if (nextRaw !== rawRef.current) {
|
|
196
|
+
rawRef.current = nextRaw;
|
|
197
|
+
setInternalRaw(nextRaw);
|
|
198
|
+
onChange?.({ target: { name, value: nextRaw } });
|
|
199
|
+
if (nextRaw.length === tokenCount && nextRaw.length > 0) {
|
|
200
|
+
onComplete?.(nextRaw);
|
|
201
|
+
}
|
|
202
|
+
if (invalidPattern) setInvalidPattern(false);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
scheduleCaret(targetCaret);
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
const handleBlur = (e: React.FocusEvent<HTMLInputElement>) => {
|
|
209
|
+
if (pattern && rawRef.current.length > 0 && !pattern.test(rawRef.current)) {
|
|
210
|
+
setInvalidPattern(true);
|
|
211
|
+
}
|
|
212
|
+
onBlur?.(e);
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
const resolvedError = invalidPattern
|
|
216
|
+
? typeof error === "string"
|
|
217
|
+
? error
|
|
218
|
+
: "Formato inválido"
|
|
219
|
+
: error;
|
|
220
|
+
|
|
221
|
+
return (
|
|
222
|
+
<div ref={containerRef} className={clsx("w-full", className)}>
|
|
223
|
+
<ITInput
|
|
224
|
+
name={name}
|
|
225
|
+
type="text"
|
|
226
|
+
label={label}
|
|
227
|
+
value={display}
|
|
228
|
+
onChange={handleChange}
|
|
229
|
+
onBlur={(e) => handleBlur(e as React.FocusEvent<HTMLInputElement>)}
|
|
230
|
+
maxLength={mask.length}
|
|
231
|
+
variant={variant}
|
|
232
|
+
size={size}
|
|
233
|
+
disabled={disabled}
|
|
234
|
+
required={required}
|
|
235
|
+
touched={touched}
|
|
236
|
+
error={resolvedError}
|
|
237
|
+
containerClassName={containerClassName}
|
|
238
|
+
iconLeft={iconLeft}
|
|
239
|
+
iconRight={iconRight}
|
|
240
|
+
autoFocus={autoFocus}
|
|
241
|
+
/>
|
|
242
|
+
</div>
|
|
243
|
+
);
|
|
244
|
+
}
|
|
@@ -11,15 +11,37 @@ Basic data table with column definitions, sorting, and configurable visual varia
|
|
|
11
11
|
|
|
12
12
|
| Prop | Type | Default | Description |
|
|
13
13
|
|------|------|---------|-------------|
|
|
14
|
-
| `columns` | `
|
|
15
|
-
| `data` | `
|
|
16
|
-
| `variant` | `
|
|
17
|
-
| `
|
|
18
|
-
| `
|
|
19
|
-
| `
|
|
20
|
-
| `
|
|
21
|
-
| `
|
|
22
|
-
| `
|
|
14
|
+
| `columns` | `Column<T>[]` | — | Column definitions: key, label, type, sortable, filter, render, actions, plus layout fields `width` (`number` px or CSS `string`), `minWidth` (px), `align` (`"left" \| "center" \| "right"`), and `truncate` (ellipsis + native `title`). |
|
|
15
|
+
| `data` | `T[]` | `[]` | Row data array. |
|
|
16
|
+
| `variant` | `"default" \| "striped" \| "bordered"` | `"default"` | Visual style variant. |
|
|
17
|
+
| `size` | `"sm" \| "md" \| "lg"` | `"md"` | Font-size preset (does not change padding). |
|
|
18
|
+
| `containerClassName` | `string` | — | CSS classes for the outermost container. |
|
|
19
|
+
| `className` | `string` | — | Declared for API compatibility but **currently not applied** by ITTable. Use `containerClassName` for the outer wrapper. |
|
|
20
|
+
| `itemsPerPageOptions` | `number[]` | `[5, 10, 20]` | Page-size options shown in the paginator. |
|
|
21
|
+
| `defaultItemsPerPage` | `number` | `10` | Initial rows per page. |
|
|
22
|
+
| `title` | `string` | — | Optional heading rendered above the table. |
|
|
23
|
+
| `renderCard` | `(row: T) => ReactNode` | — | Custom card renderer for the cards view. |
|
|
24
|
+
| `defaultView` | `"table" \| "cards"` | `"table"` | Initial view mode. |
|
|
25
|
+
| `showVerticalBorder` | `boolean` | `true` | Show vertical borders between columns. |
|
|
26
|
+
| `verticalBorderClassname` | `string` | — | Custom class for the vertical borders. |
|
|
27
|
+
| `onRowClick` | `(row: T, event: MouseEvent<HTMLElement> \| KeyboardEvent<HTMLElement>) => void` | — | Row/card activation. `MouseEvent` for pointer, `KeyboardEvent` for Enter/Space. Skipped when the interaction starts inside an interactive child. |
|
|
28
|
+
| `layout` | `"auto" \| "fixed"` | `"auto"` | Table layout algorithm. `"fixed"` honors column `width` values and drops the `min-w-max` / `min-w-[150px]` floors so content no longer forces horizontal scrolling. |
|
|
29
|
+
| `density` | `"compact" \| "normal" \| "comfortable"` | `"normal"` | Cell padding / row-height preset. Independent of `size`, which only controls font-size. |
|
|
30
|
+
| `autoCardBreakpoint` | `number` | `0` | Container width (px) below which the table automatically falls back to the cards view. `0` disables the fallback. |
|
|
31
|
+
| `virtualized` | `boolean` | `false` | Renders only the visible row window with spacer rows preserving scroll height. Table view only; cards view is never virtualized. |
|
|
32
|
+
| `virtualizedMaxHeight` | `number` | `400` | Max height (px) of the virtualized scroll container. Only used when `virtualized` is true. |
|
|
33
|
+
| `rowHeight` | `number` | `getRowHeight(size, density)` | Assumed uniform row height (px). Size-aware: `size="md"` baseline is 33 / 45 / 53 for compact / normal / comfortable; `size` shifts it by the font-size line box (sm 16px / md 20px / lg 28px). |
|
|
34
|
+
| `overscan` | `number` | `5` | Extra rows rendered above and below the viewport to avoid blank gaps. |
|
|
35
|
+
| `stickyHeader` | `boolean` | `false` | Keeps header cells sticky while the virtualized body scrolls. Only effective when `virtualized` is true. |
|
|
36
|
+
|
|
37
|
+
### Column fields
|
|
38
|
+
|
|
39
|
+
| Field | Type | Default | Description |
|
|
40
|
+
|-------|------|---------|-------------|
|
|
41
|
+
| `width` | `number \| string` | — | Fixed column width. `number` = px, `string` = any CSS length (e.g. `"20%"`). A `<colgroup>` is only emitted when at least one column sets it. |
|
|
42
|
+
| `minWidth` | `number` | — | Minimum column width in px, applied to header and body cells via inline style. |
|
|
43
|
+
| `align` | `"left" \| "center" \| "right"` | `"left"` | Horizontal alignment of the column's header and cells. |
|
|
44
|
+
| `truncate` | `boolean` | `false` | Truncates overflowing cell content with an ellipsis and exposes the raw string/number via the native `title` attribute. |
|
|
23
45
|
|
|
24
46
|
## Usage
|
|
25
47
|
|
|
@@ -27,28 +49,49 @@ Basic data table with column definitions, sorting, and configurable visual varia
|
|
|
27
49
|
import { ITTable } from '@axzydev/axzy_ui_system';
|
|
28
50
|
|
|
29
51
|
const columns = [
|
|
30
|
-
{ key: 'name', label: 'Name', sortable: true },
|
|
31
|
-
{ key: 'role', label: 'Role' },
|
|
32
|
-
{ key: 'status', label: 'Status',
|
|
52
|
+
{ key: 'name', label: 'Name', type: 'string', sortable: true, filter: true },
|
|
53
|
+
{ key: 'role', label: 'Role', type: 'string', filter: 'catalog', catalogOptions: { data: roles } },
|
|
54
|
+
{ key: 'status', label: 'Status', type: 'boolean', filter: true },
|
|
33
55
|
];
|
|
34
56
|
const data = [
|
|
35
|
-
{ name: 'Alice', role: '
|
|
36
|
-
{ name: 'Bob', role: '
|
|
57
|
+
{ name: 'Alice', role: 'ADMIN', status: true },
|
|
58
|
+
{ name: 'Bob', role: 'EDITOR', status: false },
|
|
59
|
+
];
|
|
60
|
+
|
|
61
|
+
<ITTable columns={columns} data={data} title="Users" />
|
|
62
|
+
<ITTable columns={columns} data={data} variant="striped" size="sm" defaultItemsPerPage={5} />
|
|
63
|
+
<ITTable
|
|
64
|
+
columns={columns}
|
|
65
|
+
data={data}
|
|
66
|
+
defaultView="cards"
|
|
67
|
+
onRowClick={(row, event) => console.log(row, event.type)}
|
|
68
|
+
/>
|
|
69
|
+
|
|
70
|
+
// No horizontal scroll: fixed layout + per-column widths + truncation
|
|
71
|
+
const fixedColumns = [
|
|
72
|
+
{ key: 'name', label: 'Name', type: 'string', width: '30%', truncate: true },
|
|
73
|
+
{ key: 'email', label: 'Email', type: 'string', width: 220, truncate: true },
|
|
74
|
+
{ key: 'role', label: 'Role', type: 'string', width: 140, align: 'center' },
|
|
37
75
|
];
|
|
38
76
|
|
|
39
|
-
<ITTable columns={
|
|
40
|
-
<ITTable columns={columns} data={data} variant="striped" sortKey="name" sortDirection="asc" />
|
|
41
|
-
<ITTable columns={columns} data={data} variant="bordered" onRowClick={(row) => console.log(row)} />
|
|
42
|
-
<ITTable columns={columns} data={[]} emptyMessage="No records found" />
|
|
43
|
-
<ITTable columns={columns} data={data} loading />
|
|
77
|
+
<ITTable columns={fixedColumns} data={data} layout="fixed" density="compact" />
|
|
44
78
|
```
|
|
45
79
|
|
|
46
80
|
## Notes
|
|
47
81
|
|
|
48
82
|
- `columns[].render` provides custom cell rendering via render function
|
|
49
83
|
- `columns[].sortable` enables column header sort toggles
|
|
50
|
-
-
|
|
51
|
-
- `
|
|
84
|
+
- `onRowClick` fires in **both** the table and cards views. The event is a `MouseEvent` for pointer interaction and a `KeyboardEvent` for Enter/Space activation.
|
|
85
|
+
- Clicks/keys that start inside an interactive child (`button`, `a`, `input`, `select`, `textarea`, `[role="button"]`, `[role="link"]`, `[data-row-click-ignore]`) do **not** trigger `onRowClick`.
|
|
86
|
+
- When `onRowClick` is omitted no `cursor-pointer`, focus ring, `tabIndex`, or `role` is added.
|
|
87
|
+
- `density` controls padding/row height (`compact` 33px / `normal` 45px / `comfortable` 53px at `size="md"`); `size` changes font-size and shifts the row height by the line box (sm 16px / md 20px / lg 28px).
|
|
88
|
+
- **Anti horizontal scroll recipe:** `layout="fixed"` + a `width` on every column + `truncate` on long text + `density="compact"`. `width` accepts a `number` (px) or any CSS length string (`"20%"`, `"12rem"`). A `<colgroup>` is only emitted when at least one column defines a `width`.
|
|
89
|
+
- With the default `layout="auto"`, `min-w-max` stays active so column widths can still reflow as different cell content enters the window during vertical scroll. Use `layout="fixed"` + column `width` for stable widths.
|
|
90
|
+
- `autoCardBreakpoint` swaps to the cards view when the container (not the viewport) is narrower than the given px value; the Table toggle is disabled while forced, the Cards toggle stays enabled.
|
|
91
|
+
- **Virtualization** applies to the **table view only** — the cards view is never virtualized. It only pays off with a large page size: raise `defaultItemsPerPage` and `itemsPerPageOptions` (defaults are `10` and `[5, 10, 20]`). Rows are assumed to have a uniform `rowHeight`; use `truncate` + `layout="fixed"` for predictable heights.
|
|
92
|
+
- Scroll resets to the top on page or items-per-page change, but **not** on filter change (known limitation). `aria-rowcount` reflects the current page (+1), not the whole dataset, because pagination is independent.
|
|
93
|
+
- `virtualizedMaxHeight`, `rowHeight`, `overscan`, and `stickyHeader` only take effect when `virtualized` is true. `stickyHeader` makes each `<th>` sticky using the `--color-table-headerBg` token (fallback `#f8fafc`).
|
|
94
|
+
- `TableDensity` is exported from the package. `ITSearchTable` does **not** support `layout`, `density`, `autoCardBreakpoint`, `virtualized`, or per-column `width` — do not assume parity.
|
|
52
95
|
- Three visual variants: default (no borders), striped (alternating rows), bordered (full grid)
|
|
53
96
|
|
|
54
97
|
## Stories
|