@axzydev/axzy_ui_system 1.3.1 → 1.3.3

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 (50) hide show
  1. package/README.md +6 -2
  2. package/dist/index.cjs +957 -554
  3. package/dist/index.cjs.map +1 -1
  4. package/dist/index.css +1 -1
  5. package/dist/index.d.cts +191 -9
  6. package/dist/index.d.ts +191 -9
  7. package/dist/index.js +908 -505
  8. package/dist/index.js.map +1 -1
  9. package/package.json +1 -1
  10. package/src/App.tsx +150 -154
  11. package/src/components/molecules/table/table.doc.mdx +64 -21
  12. package/src/components/molecules/table/table.props.ts +81 -2
  13. package/src/components/molecules/table/table.stories.tsx +155 -0
  14. package/src/components/molecules/table/table.tsx +178 -36
  15. package/src/components/organisms/data-table/ITDataTable.doc.mdx +37 -3
  16. package/src/components/organisms/data-table/ITDataTable.stories.tsx +43 -0
  17. package/src/components/organisms/data-table/dataTable.props.ts +64 -0
  18. package/src/components/organisms/data-table/dataTable.tsx +241 -82
  19. package/src/components/organisms/sidebar/sidebar.doc.mdx +5 -0
  20. package/src/components/organisms/sidebar/sidebar.props.ts +5 -0
  21. package/src/components/organisms/sidebar/sidebar.stories.tsx +1 -1
  22. package/src/components/organisms/sidebar/sidebar.tsx +27 -7
  23. package/src/components/organisms/topbar/topbar.doc.mdx +3 -0
  24. package/src/components/organisms/topbar/topbar.props.ts +16 -0
  25. package/src/components/organisms/topbar/topbar.tsx +133 -98
  26. package/src/components/templates/layout/layout.doc.mdx +2 -0
  27. package/src/components/templates/layout/layout.tsx +7 -1
  28. package/src/components/templates/page/page.doc.mdx +8 -2
  29. package/src/components/templates/page/page.props.ts +11 -1
  30. package/src/components/templates/page/page.tsx +11 -1
  31. package/src/components/theme-provider/themePresets.ts +6 -0
  32. package/src/components/theme-provider/themeProvider.doc.mdx +1 -0
  33. package/src/components/theme-provider/themeProvider.props.ts +2 -0
  34. package/src/components/theme-provider/themeProvider.tsx +13 -2
  35. package/src/dev.css +58 -1
  36. package/src/hooks/useElementSize.ts +46 -0
  37. package/src/hooks/useVirtualRows.ts +123 -0
  38. package/src/index.ts +2 -0
  39. package/src/sandbox/LandingShowcase.tsx +654 -0
  40. package/src/sandbox/NotFoundShowcase.tsx +70 -0
  41. package/src/sandbox/TopbarSearch.tsx +85 -0
  42. package/src/sandbox/navigation.ts +148 -0
  43. package/src/sandbox/useHashRoute.ts +149 -0
  44. package/src/showcases/DataShowcases.tsx +321 -24
  45. package/src/showcases/PageShowcases.tsx +31 -0
  46. package/src/showcases/StructureShowcases.tsx +42 -4
  47. package/src/theme/theme.types.ts +2 -0
  48. package/src/types/table.types.ts +48 -0
  49. package/src/utils/styles.ts +12 -5
  50. package/src/utils/table.utils.ts +31 -0
package/package.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "packageManager": "pnpm@10.34.0",
5
5
  "private": false,
6
6
  "license": "MIT",
7
- "version": "1.3.1",
7
+ "version": "1.3.3",
8
8
  "type": "module",
9
9
  "sideEffects": [
10
10
  "*.css"
package/src/App.tsx CHANGED
@@ -7,7 +7,8 @@ import {
7
7
  FaSlidersH,
8
8
  FaTable,
9
9
  } from "react-icons/fa";
10
- import { ITLayout, ITThemeProvider } from "./index";
10
+ import { ITLayout, ITThemeProvider, useDebouncedSearch } from "./index";
11
+ import type { ITNavigationItem } from "./index";
11
12
  import "./index.css";
12
13
 
13
14
  // Import Showcases
@@ -69,187 +70,165 @@ import {
69
70
  ThemeProviderShowcase,
70
71
  } from "./showcases/FeedbackShowcases";
71
72
 
72
- type ViewMode = "home" | "ui-system";
73
+ // Sandbox routing / landing (dev-only, never published)
74
+ import {
75
+ SANDBOX_GROUPS,
76
+ firstItemOf,
77
+ } from "./sandbox/navigation";
78
+ import { useHashRoute } from "./sandbox/useHashRoute";
79
+ import TopbarSearch from "./sandbox/TopbarSearch";
80
+ import { LandingShowcase } from "./sandbox/LandingShowcase";
81
+ import NotFoundShowcase from "./sandbox/NotFoundShowcase";
73
82
 
74
- // Group definitions for the sidebar (UI System showroom). Estático: se define
75
- // fuera del componente para que su referencia sea estable en los deps del memo.
76
- const categories = [
77
- {
78
- id: "general",
79
- label: "General",
80
- icon: <FaHome />,
81
- subitems: [
82
- { id: "getting-started", label: "Getting Started" },
83
- { id: "sizes", label: "Medidas · sm / md / lg" },
84
- ],
85
- },
86
- {
87
- id: "struc",
88
- label: "Estructura & Layout",
89
- icon: <FaCreditCard />,
90
- subitems: [
91
- { id: "layout", label: "ITLayout & ITNavbar" },
92
- { id: "stack", label: "ITStack" },
93
- { id: "flex", label: "ITFlex" },
94
- { id: "grid", label: "ITGrid" },
95
- { id: "card", label: "ITCard" },
96
- { id: "text", label: "ITText" },
97
- { id: "accordion", label: "ITAccordion" },
98
- { id: "pageheader", label: "ITPageHeader" },
99
- { id: "page", label: "ITPage" },
100
- { id: "screen-dashboard", label: "Dashboard Ejemplo" },
101
- { id: "screen-form", label: "Formulario Ejemplo" },
102
- ],
103
- },
104
- {
105
- id: "forms",
106
- label: "Formularios & Inputs",
107
- icon: <FaKeyboard />,
108
- subitems: [
109
- { id: "button", label: "ITButton" },
110
- { id: "input", label: "ITInput" },
111
- { id: "select", label: "ITSelect" },
112
- { id: "searchselect", label: "ITSearchSelect" },
113
- { id: "multiselect", label: "ITMultiSelect" },
114
- { id: "chipinput", label: "ITChipInput" },
115
- { id: "field", label: "ITField" },
116
- { id: "datepicker", label: "ITDatePicker" },
117
- { id: "timepicker", label: "ITTimePicker" },
118
- { id: "maskedinput", label: "ITMaskedInput" },
119
- { id: "calendar", label: "ITCalendar" },
120
- { id: "slidetoggle", label: "ITSlideToggle" },
121
- { id: "dropfile", label: "ITDropfile" },
122
- { id: "wysiwyg", label: "ITWysiwyg" },
123
- { id: "formbuilder", label: "ITFormBuilder" },
124
- ],
125
- },
126
- {
127
- id: "data",
128
- label: "Visualización Datos",
129
- icon: <FaTable />,
130
- subitems: [
131
- { id: "table", label: "ITTable" },
132
- { id: "datatable", label: "ITDataTable" },
133
- { id: "badget", label: "ITBadget" },
134
- { id: "chip", label: "ITChip" },
135
- { id: "image", label: "ITImage" },
136
- ],
137
- },
138
- {
139
- id: "nav",
140
- label: "Navegación & Control",
141
- icon: <FaSlidersH />,
142
- subitems: [
143
- { id: "tabs", label: "ITTabs" },
144
- { id: "stepper", label: "ITStepper" },
145
- { id: "pagination", label: "ITPagination" },
146
- { id: "triplefilter", label: "ITTripleFilter" },
147
- { id: "dropdownmenu", label: "ITDropdownMenu" },
148
- ],
149
- },
150
- {
151
- id: "feed",
152
- label: "Feedback & Sistema",
153
- icon: <FaRegBell />,
154
- subitems: [
155
- { id: "dialog", label: "ITDialog" },
156
- { id: "toast", label: "ITToast" },
157
- { id: "loader", label: "ITLoader" },
158
- { id: "themeprovider", label: "ITThemeProvider" },
159
- ],
160
- },
161
- ];
83
+ // Icons per sandbox group (icons stay here; navigation.ts is plain data).
84
+ const GROUP_ICONS: Record<string, React.ReactNode> = {
85
+ general: <FaHome />,
86
+ struc: <FaCreditCard />,
87
+ forms: <FaKeyboard />,
88
+ data: <FaTable />,
89
+ nav: <FaSlidersH />,
90
+ feed: <FaRegBell />,
91
+ };
162
92
 
163
93
  function App() {
164
- const [view, setView] = useState<ViewMode>(() => {
165
- const hash = window.location.hash.replace("#", "");
166
- return hash.startsWith("ui-system") ? "ui-system" : "home";
167
- });
168
- const [showroomActive, setShowroomActive] = useState("screen-form");
169
- const [searchTerm] = useState("");
170
- const [subitemConnector] = useState<
171
- "dot" | "|" | "none"
172
- >("dot");
94
+ const { route, goToItem, goToLanding, goToPortfolio } = useHashRoute();
95
+ const [subitemConnector] = useState<"dot" | "|" | "none">("dot");
173
96
 
174
- useEffect(() => {
175
- const onHashChange = () => {
176
- const hash = window.location.hash.replace("#", "");
177
- setView(hash.startsWith("ui-system") ? "ui-system" : "home");
178
- };
179
- window.addEventListener("hashchange", onHashChange);
180
- return () => window.removeEventListener("hashchange", onHashChange);
181
- }, []);
97
+ // Debounced term drives filtering; `searchValue` is the live input value.
98
+ const [debouncedTerm, setDebouncedTerm] = useState("");
99
+ const {
100
+ searchTerm: searchValue,
101
+ handleSearchChange,
102
+ handleClearSearch,
103
+ } = useDebouncedSearch({ debounceMs: 250, onSearch: setDebouncedTerm });
182
104
 
183
- // Filter sidebar navigation items based on search term
184
- const filteredNavigationItems = useMemo(() => {
185
- return categories
186
- .map((cat) => {
187
- if (!cat.subitems) {
188
- const matches =
189
- cat.label.toLowerCase().includes(searchTerm.toLowerCase()) ||
190
- cat.id.toLowerCase().includes(searchTerm.toLowerCase());
191
- if (matches) {
192
- return {
193
- ...cat,
194
- isActive: showroomActive === cat.id,
195
- action: () => setShowroomActive(cat.id),
196
- };
197
- }
198
- return null;
199
- }
105
+ // The active component page (group-only routes fall back to the first item).
106
+ const showroomActive =
107
+ route.view === "ui-system" && route.valid
108
+ ? route.item ?? (route.group ? firstItemOf(route.group) : null)
109
+ : null;
200
110
 
201
- const matchingSubitems = cat.subitems.filter(
202
- (sub) =>
203
- sub.label.toLowerCase().includes(searchTerm.toLowerCase()) ||
204
- sub.id.toLowerCase().includes(searchTerm.toLowerCase()),
205
- );
111
+ // Redirect `#ui-system/<group>` to the group's first item (replace, no history spam).
112
+ useEffect(() => {
113
+ if (route.view === "ui-system" && route.valid && route.group && !route.item) {
114
+ const first = firstItemOf(route.group);
115
+ if (first) goToItem(route.group, first, { replace: true });
116
+ }
117
+ }, [route, goToItem]);
206
118
 
207
- const mappedSubitems = matchingSubitems.map((sub) => ({
119
+ // Sidebar navigation: grouped when idle, flat filtered leaves when searching.
120
+ const navigationItems = useMemo<ITNavigationItem[]>(() => {
121
+ const term = debouncedTerm.trim().toLowerCase();
122
+
123
+ if (!term) {
124
+ return SANDBOX_GROUPS.map((group) => {
125
+ const subitems = group.subitems.map((sub) => ({
208
126
  id: sub.id,
209
127
  label: sub.label,
210
128
  isActive: showroomActive === sub.id,
211
- action: () => setShowroomActive(sub.id),
129
+ action: () => goToItem(group.id, sub.id),
212
130
  }));
213
-
214
- const isAnySubitemActive = mappedSubitems.some((sub) => sub.isActive);
215
-
216
131
  return {
217
- ...cat,
218
- isActive: isAnySubitemActive,
219
- subitems: mappedSubitems,
132
+ id: group.id,
133
+ label: group.label,
134
+ icon: GROUP_ICONS[group.id],
135
+ isActive: subitems.some((sub) => sub.isActive),
136
+ subitems,
220
137
  };
221
- })
222
- .filter((cat): cat is Exclude<typeof cat, null> => {
223
- if (!cat) return false;
224
- if (cat.subitems) return cat.subitems.length > 0;
225
- return true;
226
138
  });
227
- }, [searchTerm, showroomActive]);
139
+ }
140
+
141
+ const flat: ITNavigationItem[] = [];
142
+ SANDBOX_GROUPS.forEach((group) => {
143
+ const groupMatches =
144
+ group.label.toLowerCase().includes(term) ||
145
+ group.id.toLowerCase().includes(term);
146
+ const groupBadge = group.label.split(" ")[0];
147
+
148
+ group.subitems
149
+ .filter(
150
+ (sub) =>
151
+ groupMatches ||
152
+ sub.label.toLowerCase().includes(term) ||
153
+ sub.id.toLowerCase().includes(term),
154
+ )
155
+ .forEach((sub) => {
156
+ flat.push({
157
+ id: sub.id,
158
+ label: sub.label,
159
+ icon: GROUP_ICONS[group.id],
160
+ badge: groupBadge,
161
+ isActive: showroomActive === sub.id,
162
+ action: () => goToItem(group.id, sub.id),
163
+ });
164
+ });
165
+ });
166
+ return flat;
167
+ }, [debouncedTerm, showroomActive, goToItem]);
228
168
 
169
+ // Sidebar is UNCONTROLLED: ITLayout's internal default is the 88px rail and the
170
+ // rail expands automatically on hover. No toggle button — the hover is the control.
171
+ // The component search lives in the topbar because a hover-only rail can't host it.
229
172
  const sidebarProps = {
230
- navigationItems: filteredNavigationItems,
173
+ navigationItems,
231
174
  subitemConnector,
232
175
  };
233
176
 
234
177
  const topBarProps = {
235
178
  logoText: "AXZY UI System",
179
+ centerContent: (
180
+ <TopbarSearch
181
+ value={searchValue}
182
+ onChange={handleSearchChange}
183
+ onClear={handleClearSearch}
184
+ resultCount={navigationItems.length}
185
+ pending={searchValue.trim() !== debouncedTerm.trim()}
186
+ />
187
+ ),
188
+ // The topbar places `children` in its right area, right before the user menu.
189
+ children: (
190
+ <button
191
+ type="button"
192
+ aria-label="Notificaciones"
193
+ title="Notificaciones"
194
+ className="relative flex items-center justify-center p-2.5 rounded-xl transition-colors duration-200 hover:bg-[var(--it-topbar-user-hover,#f1f5f9)] focus:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-primary-400"
195
+ style={{ color: "var(--it-topbar-icon, #94a3b8)" }}
196
+ >
197
+ <FaRegBell className="w-5 h-5" aria-hidden="true" />
198
+ <span className="absolute top-1.5 right-1.5 w-2 h-2 rounded-full bg-red-500 ring-2 ring-white" />
199
+ </button>
200
+ ),
201
+ navItems: [
202
+ {
203
+ id: "ui-system",
204
+ label: "UI System",
205
+ action: () => goToLanding(),
206
+ },
207
+ {
208
+ id: "portfolio",
209
+ label: "Portafolio",
210
+ action: () => goToPortfolio(),
211
+ },
212
+ ],
213
+ onNavItemClick: (id: string) => {
214
+ if (id === "ui-system") goToLanding();
215
+ else if (id === "portfolio") goToPortfolio();
216
+ },
236
217
  userMenu: {
237
218
  userName: "Alex Dev",
238
219
  userEmail: "alex@axzy.dev",
239
220
  menuItems: [
240
221
  {
241
222
  label: "Ir al Portafolio",
242
- onClick: () => {
243
- window.location.hash = "home";
244
- },
223
+ onClick: () => goToPortfolio(),
245
224
  },
246
225
  ],
247
226
  },
248
227
  };
249
228
 
250
229
  // Render correct component based on active showroom navigation
251
- const renderShowcase = () => {
252
- switch (showroomActive) {
230
+ const renderShowcase = (itemId: string | null) => {
231
+ switch (itemId) {
253
232
  case "getting-started":
254
233
  return <GettingStartedShowcase />;
255
234
  // Structure
@@ -340,13 +319,19 @@ function App() {
340
319
  case "themeprovider":
341
320
  return <ThemeProviderShowcase />;
342
321
  default:
343
- return <GettingStartedShowcase />;
322
+ return (
323
+ <NotFoundShowcase
324
+ attempted={route.raw}
325
+ onGoLanding={() => goToLanding()}
326
+ onGoPortfolio={goToPortfolio}
327
+ />
328
+ );
344
329
  }
345
330
  };
346
331
 
347
332
  return (
348
333
  <ITThemeProvider showFab={false} density={0.98} radius={10} shadow={2}>
349
- {view === "home" ? (
334
+ {route.view === "home" ? (
350
335
  <div className="min-h-screen bg-slate-50 dark:bg-slate-950 py-10 px-4 sm:px-6 lg:px-8">
351
336
  <div className="max-w-6xl mx-auto">
352
337
  <HomeShowcase />
@@ -354,9 +339,20 @@ function App() {
354
339
  </div>
355
340
  ) : (
356
341
  <ITLayout sidebar={sidebarProps} topBar={topBarProps}>
357
- <div className="max-w-7xl mx-auto">
358
- {renderShowcase()}
359
- </div>
342
+ {!route.valid ? (
343
+ <NotFoundShowcase
344
+ attempted={route.raw}
345
+ onGoLanding={() => goToLanding()}
346
+ onGoPortfolio={goToPortfolio}
347
+ />
348
+ ) : !route.group && !route.item ? (
349
+ <LandingShowcase
350
+ onOpenItem={goToItem}
351
+ onOpenPortfolio={goToPortfolio}
352
+ />
353
+ ) : (
354
+ renderShowcase(showroomActive)
355
+ )}
360
356
  </ITLayout>
361
357
  )}
362
358
  </ITThemeProvider>
@@ -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
@@ -1,4 +1,4 @@
1
- import { TableSize, TableVariants } from "@/types/table.types";
1
+ import { TableDensity, TableSize, TableVariants } from "@/types/table.types";
2
2
 
3
3
  /** Data type of a table column, controls default rendering, filter UI, and sort comparison. */
4
4
  export type ColumnType = "string" | "date" | "number" | "boolean" | "actions" | "catalog";
@@ -46,6 +46,22 @@ export interface Column<T = any> {
46
46
  /** Shows an error state in the filter UI when the catalog failed to load. @default false */
47
47
  error?: boolean;
48
48
  };
49
+ /**
50
+ * Fixed column width. A `number` is treated as pixels (`width: 120` → `120px`);
51
+ * a `string` is passed through as any CSS length (`"20%"`, `"12rem"`).
52
+ * A `<colgroup>` is only rendered when at least one column defines a width.
53
+ */
54
+ width?: number | string;
55
+ /** Minimum column width in pixels, applied to the header and body cells via inline style. */
56
+ minWidth?: number;
57
+ /** Horizontal alignment of this column's header and body cells. @default "left" */
58
+ align?: "left" | "center" | "right";
59
+ /**
60
+ * Truncates overflowing cell content with an ellipsis and exposes the raw
61
+ * string/number value through the native `title` attribute.
62
+ * @default false
63
+ */
64
+ truncate?: boolean;
49
65
  }
50
66
 
51
67
  export interface ITTableProps<T> {
@@ -55,7 +71,7 @@ export interface ITTableProps<T> {
55
71
  containerClassName?: string;
56
72
  /** The data array to render in the table body. */
57
73
  data: T[];
58
- /** Visual variant: "default", "striped", "bordered", "borderless". */
74
+ /** Visual variant: "default", "striped", "bordered". */
59
75
  variant?: TableVariants;
60
76
  /** Additional CSS classes for the root table wrapper. */
61
77
  className?: string;
@@ -75,4 +91,67 @@ export interface ITTableProps<T> {
75
91
  showVerticalBorder?: boolean;
76
92
  /** Custom class for vertical borders (overrides the default subtle gray). */
77
93
  verticalBorderClassname?: string;
94
+ /**
95
+ * Callback fired when a row (table view) or card (cards view) is activated,
96
+ * receiving the row item and the originating event. The event is a
97
+ * `MouseEvent` for pointer interaction and a `KeyboardEvent` for Enter/Space
98
+ * activation. Not fired when the interaction starts inside an interactive
99
+ * child (button, link, input, or any `[data-row-click-ignore]` element).
100
+ */
101
+ onRowClick?: (
102
+ row: T,
103
+ event: React.MouseEvent<HTMLElement> | React.KeyboardEvent<HTMLElement>
104
+ ) => void;
105
+ /**
106
+ * Table layout algorithm. `"fixed"` respects column `width` values and drops
107
+ * the `min-w-max` / `min-w-[150px]` floors so content no longer forces
108
+ * horizontal scrolling; `"auto"` keeps the content-driven layout.
109
+ * @default "auto"
110
+ */
111
+ layout?: "auto" | "fixed";
112
+ /**
113
+ * Cell padding and row-height preset. Independent of `size`, which only
114
+ * controls font-size: `"compact"` | `"normal"` | `"comfortable"`.
115
+ * @default "normal"
116
+ */
117
+ density?: TableDensity;
118
+ /**
119
+ * Container width (in px) below which the table automatically falls back to
120
+ * the cards view. `0` disables the fallback.
121
+ * @default 0
122
+ */
123
+ autoCardBreakpoint?: number;
124
+ /**
125
+ * Enables row virtualization for the table view: only the visible window of
126
+ * rows is rendered, with spacer rows preserving the full scroll height.
127
+ * The cards view is never virtualized.
128
+ * @default false
129
+ */
130
+ virtualized?: boolean;
131
+ /**
132
+ * Max height (px) of the virtualized table's scroll container.
133
+ * Only used when `virtualized` is true.
134
+ * @default 400
135
+ */
136
+ virtualizedMaxHeight?: number;
137
+ /**
138
+ * Assumed uniform row height (px) used by the virtualizer. Defaults to
139
+ * `getRowHeight(size, density)`, which is size-aware: the `size="md"`
140
+ * baseline is 33 / 45 / 53 (compact / normal / comfortable) and `size`
141
+ * shifts it by the font-size line box (sm 16px / md 20px / lg 28px).
142
+ * Only used when `virtualized` is true.
143
+ */
144
+ rowHeight?: number;
145
+ /**
146
+ * Number of extra rows rendered above and below the viewport to avoid blank
147
+ * gaps while scrolling. Only used when `virtualized` is true.
148
+ * @default 5
149
+ */
150
+ overscan?: number;
151
+ /**
152
+ * Makes the header cells sticky while the virtualized body scrolls. Only
153
+ * effective when `virtualized` is true.
154
+ * @default false
155
+ */
156
+ stickyHeader?: boolean;
78
157
  }