@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.
Files changed (52) hide show
  1. package/README.md +6 -2
  2. package/dist/index.cjs +2342 -1715
  3. package/dist/index.cjs.map +1 -1
  4. package/dist/index.css +1 -1
  5. package/dist/index.d.cts +286 -9
  6. package/dist/index.d.ts +286 -9
  7. package/dist/index.js +2164 -1538
  8. package/dist/index.js.map +1 -1
  9. package/package.json +1 -1
  10. package/src/App.tsx +153 -153
  11. package/src/components/molecules/calendar/calendar.doc.mdx +2 -1
  12. package/src/components/molecules/calendar/calendar.tsx +102 -24
  13. package/src/components/molecules/masked-input/masked-input.doc.mdx +78 -0
  14. package/src/components/molecules/masked-input/masked-input.props.ts +58 -0
  15. package/src/components/molecules/masked-input/masked-input.stories.tsx +126 -0
  16. package/src/components/molecules/masked-input/masked-input.tsx +244 -0
  17. package/src/components/molecules/table/table.doc.mdx +64 -21
  18. package/src/components/molecules/table/table.props.ts +81 -2
  19. package/src/components/molecules/table/table.stories.tsx +155 -0
  20. package/src/components/molecules/table/table.tsx +177 -36
  21. package/src/components/organisms/data-table/ITDataTable.doc.mdx +37 -3
  22. package/src/components/organisms/data-table/ITDataTable.stories.tsx +43 -0
  23. package/src/components/organisms/data-table/dataTable.props.ts +64 -0
  24. package/src/components/organisms/data-table/dataTable.tsx +240 -82
  25. package/src/components/organisms/sidebar/sidebar.doc.mdx +5 -0
  26. package/src/components/organisms/sidebar/sidebar.props.ts +5 -0
  27. package/src/components/organisms/sidebar/sidebar.stories.tsx +1 -1
  28. package/src/components/organisms/sidebar/sidebar.tsx +27 -7
  29. package/src/components/organisms/topbar/topbar.doc.mdx +3 -0
  30. package/src/components/organisms/topbar/topbar.props.ts +16 -0
  31. package/src/components/organisms/topbar/topbar.tsx +133 -98
  32. package/src/components/templates/layout/layout.doc.mdx +2 -0
  33. package/src/components/templates/layout/layout.tsx +7 -1
  34. package/src/components/templates/page/page.doc.mdx +8 -2
  35. package/src/components/templates/page/page.props.ts +11 -1
  36. package/src/components/templates/page/page.tsx +11 -1
  37. package/src/dev.css +58 -1
  38. package/src/hooks/useElementSize.ts +46 -0
  39. package/src/hooks/useVirtualRows.ts +123 -0
  40. package/src/index.ts +6 -2
  41. package/src/sandbox/LandingShowcase.tsx +465 -0
  42. package/src/sandbox/NotFoundShowcase.tsx +70 -0
  43. package/src/sandbox/TopbarSearch.tsx +85 -0
  44. package/src/sandbox/navigation.ts +148 -0
  45. package/src/sandbox/useHashRoute.ts +149 -0
  46. package/src/showcases/DataShowcases.tsx +321 -24
  47. package/src/showcases/FormShowcases.tsx +178 -0
  48. package/src/showcases/PageShowcases.tsx +31 -0
  49. package/src/showcases/StructureShowcases.tsx +42 -4
  50. package/src/types/table.types.ts +48 -0
  51. package/src/utils/styles.ts +9 -4
  52. 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` | `{ key: string; label: string; render?: (value, row) => ReactNode; sortable?: boolean; width?: string }[]` | — | Column definitions |
15
- | `data` | `Record<string, any>[]` | — | Row data array |
16
- | `variant` | `'default' \| 'striped' \| 'bordered'` | `'default'` | Visual style variant |
17
- | `sortKey` | `string` | — | Current sort column key |
18
- | `sortDirection` | `'asc' \| 'desc'` | — | Current sort direction |
19
- | `onSort` | `(key: string, direction: 'asc' \| 'desc') => void` | — | Sort handler |
20
- | `onRowClick` | `(row: Record<string, any>) => void` | — | Row click handler |
21
- | `loading` | `boolean` | `false` | Show loading state |
22
- | `emptyMessage` | `string` | `'No data'` | Empty state message |
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', render: (v) => <ITBadge>{v}</ITBadge> },
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: 'Admin', status: 'Active' },
36
- { name: 'Bob', role: 'Editor', status: 'Inactive' },
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={columns} data={data} />
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
- - Sorting controlled via `sortKey`, `sortDirection`, and `onSort`
51
- - `onRowClick` fires with the row data on click
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