@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
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.0",
7
+ "version": "1.3.2",
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
@@ -44,6 +45,7 @@ import {
44
45
  FormBuilderShowcase,
45
46
  WysiwygShowcase,
46
47
  FieldShowcase,
48
+ MaskedInputShowcase,
47
49
  MultiSelectShowcase,
48
50
  ChipInputShowcase,
49
51
  } from "./showcases/FormShowcases";
@@ -68,186 +70,165 @@ import {
68
70
  ThemeProviderShowcase,
69
71
  } from "./showcases/FeedbackShowcases";
70
72
 
71
- 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";
72
82
 
73
- // Group definitions for the sidebar (UI System showroom). Estático: se define
74
- // fuera del componente para que su referencia sea estable en los deps del memo.
75
- const categories = [
76
- {
77
- id: "general",
78
- label: "General",
79
- icon: <FaHome />,
80
- subitems: [
81
- { id: "getting-started", label: "Getting Started" },
82
- { id: "sizes", label: "Medidas · sm / md / lg" },
83
- ],
84
- },
85
- {
86
- id: "struc",
87
- label: "Estructura & Layout",
88
- icon: <FaCreditCard />,
89
- subitems: [
90
- { id: "layout", label: "ITLayout & ITNavbar" },
91
- { id: "stack", label: "ITStack" },
92
- { id: "flex", label: "ITFlex" },
93
- { id: "grid", label: "ITGrid" },
94
- { id: "card", label: "ITCard" },
95
- { id: "text", label: "ITText" },
96
- { id: "accordion", label: "ITAccordion" },
97
- { id: "pageheader", label: "ITPageHeader" },
98
- { id: "page", label: "ITPage" },
99
- { id: "screen-dashboard", label: "Dashboard Ejemplo" },
100
- { id: "screen-form", label: "Formulario Ejemplo" },
101
- ],
102
- },
103
- {
104
- id: "forms",
105
- label: "Formularios & Inputs",
106
- icon: <FaKeyboard />,
107
- subitems: [
108
- { id: "button", label: "ITButton" },
109
- { id: "input", label: "ITInput" },
110
- { id: "select", label: "ITSelect" },
111
- { id: "searchselect", label: "ITSearchSelect" },
112
- { id: "multiselect", label: "ITMultiSelect" },
113
- { id: "chipinput", label: "ITChipInput" },
114
- { id: "field", label: "ITField" },
115
- { id: "datepicker", label: "ITDatePicker" },
116
- { id: "timepicker", label: "ITTimePicker" },
117
- { id: "calendar", label: "ITCalendar" },
118
- { id: "slidetoggle", label: "ITSlideToggle" },
119
- { id: "dropfile", label: "ITDropfile" },
120
- { id: "wysiwyg", label: "ITWysiwyg" },
121
- { id: "formbuilder", label: "ITFormBuilder" },
122
- ],
123
- },
124
- {
125
- id: "data",
126
- label: "Visualización Datos",
127
- icon: <FaTable />,
128
- subitems: [
129
- { id: "table", label: "ITTable" },
130
- { id: "datatable", label: "ITDataTable" },
131
- { id: "badget", label: "ITBadget" },
132
- { id: "chip", label: "ITChip" },
133
- { id: "image", label: "ITImage" },
134
- ],
135
- },
136
- {
137
- id: "nav",
138
- label: "Navegación & Control",
139
- icon: <FaSlidersH />,
140
- subitems: [
141
- { id: "tabs", label: "ITTabs" },
142
- { id: "stepper", label: "ITStepper" },
143
- { id: "pagination", label: "ITPagination" },
144
- { id: "triplefilter", label: "ITTripleFilter" },
145
- { id: "dropdownmenu", label: "ITDropdownMenu" },
146
- ],
147
- },
148
- {
149
- id: "feed",
150
- label: "Feedback & Sistema",
151
- icon: <FaRegBell />,
152
- subitems: [
153
- { id: "dialog", label: "ITDialog" },
154
- { id: "toast", label: "ITToast" },
155
- { id: "loader", label: "ITLoader" },
156
- { id: "themeprovider", label: "ITThemeProvider" },
157
- ],
158
- },
159
- ];
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
+ };
160
92
 
161
93
  function App() {
162
- const [view, setView] = useState<ViewMode>(() => {
163
- const hash = window.location.hash.replace("#", "");
164
- return hash.startsWith("ui-system") ? "ui-system" : "home";
165
- });
166
- const [showroomActive, setShowroomActive] = useState("screen-form");
167
- const [searchTerm] = useState("");
168
- const [subitemConnector] = useState<
169
- "dot" | "|" | "none"
170
- >("dot");
94
+ const { route, goToItem, goToLanding, goToPortfolio } = useHashRoute();
95
+ const [subitemConnector] = useState<"dot" | "|" | "none">("dot");
171
96
 
172
- useEffect(() => {
173
- const onHashChange = () => {
174
- const hash = window.location.hash.replace("#", "");
175
- setView(hash.startsWith("ui-system") ? "ui-system" : "home");
176
- };
177
- window.addEventListener("hashchange", onHashChange);
178
- return () => window.removeEventListener("hashchange", onHashChange);
179
- }, []);
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 });
180
104
 
181
- // Filter sidebar navigation items based on search term
182
- const filteredNavigationItems = useMemo(() => {
183
- return categories
184
- .map((cat) => {
185
- if (!cat.subitems) {
186
- const matches =
187
- cat.label.toLowerCase().includes(searchTerm.toLowerCase()) ||
188
- cat.id.toLowerCase().includes(searchTerm.toLowerCase());
189
- if (matches) {
190
- return {
191
- ...cat,
192
- isActive: showroomActive === cat.id,
193
- action: () => setShowroomActive(cat.id),
194
- };
195
- }
196
- return null;
197
- }
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;
198
110
 
199
- const matchingSubitems = cat.subitems.filter(
200
- (sub) =>
201
- sub.label.toLowerCase().includes(searchTerm.toLowerCase()) ||
202
- sub.id.toLowerCase().includes(searchTerm.toLowerCase()),
203
- );
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]);
204
118
 
205
- 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) => ({
206
126
  id: sub.id,
207
127
  label: sub.label,
208
128
  isActive: showroomActive === sub.id,
209
- action: () => setShowroomActive(sub.id),
129
+ action: () => goToItem(group.id, sub.id),
210
130
  }));
211
-
212
- const isAnySubitemActive = mappedSubitems.some((sub) => sub.isActive);
213
-
214
131
  return {
215
- ...cat,
216
- isActive: isAnySubitemActive,
217
- 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,
218
137
  };
219
- })
220
- .filter((cat): cat is Exclude<typeof cat, null> => {
221
- if (!cat) return false;
222
- if (cat.subitems) return cat.subitems.length > 0;
223
- return true;
224
138
  });
225
- }, [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]);
226
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.
227
172
  const sidebarProps = {
228
- navigationItems: filteredNavigationItems,
173
+ navigationItems,
229
174
  subitemConnector,
230
175
  };
231
176
 
232
177
  const topBarProps = {
233
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
+ },
234
217
  userMenu: {
235
218
  userName: "Alex Dev",
236
219
  userEmail: "alex@axzy.dev",
237
220
  menuItems: [
238
221
  {
239
222
  label: "Ir al Portafolio",
240
- onClick: () => {
241
- window.location.hash = "home";
242
- },
223
+ onClick: () => goToPortfolio(),
243
224
  },
244
225
  ],
245
226
  },
246
227
  };
247
228
 
248
229
  // Render correct component based on active showroom navigation
249
- const renderShowcase = () => {
250
- switch (showroomActive) {
230
+ const renderShowcase = (itemId: string | null) => {
231
+ switch (itemId) {
251
232
  case "getting-started":
252
233
  return <GettingStartedShowcase />;
253
234
  // Structure
@@ -294,6 +275,8 @@ function App() {
294
275
  return <DatePickerShowcase />;
295
276
  case "timepicker":
296
277
  return <TimePickerShowcase />;
278
+ case "maskedinput":
279
+ return <MaskedInputShowcase />;
297
280
  case "calendar":
298
281
  return <CalendarShowcase />;
299
282
  case "slidetoggle":
@@ -336,13 +319,19 @@ function App() {
336
319
  case "themeprovider":
337
320
  return <ThemeProviderShowcase />;
338
321
  default:
339
- return <GettingStartedShowcase />;
322
+ return (
323
+ <NotFoundShowcase
324
+ attempted={route.raw}
325
+ onGoLanding={() => goToLanding()}
326
+ onGoPortfolio={goToPortfolio}
327
+ />
328
+ );
340
329
  }
341
330
  };
342
331
 
343
332
  return (
344
333
  <ITThemeProvider showFab={false} density={0.98} radius={10} shadow={2}>
345
- {view === "home" ? (
334
+ {route.view === "home" ? (
346
335
  <div className="min-h-screen bg-slate-50 dark:bg-slate-950 py-10 px-4 sm:px-6 lg:px-8">
347
336
  <div className="max-w-6xl mx-auto">
348
337
  <HomeShowcase />
@@ -350,9 +339,20 @@ function App() {
350
339
  </div>
351
340
  ) : (
352
341
  <ITLayout sidebar={sidebarProps} topBar={topBarProps}>
353
- <div className="max-w-7xl mx-auto">
354
- {renderShowcase()}
355
- </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
+ )}
356
356
  </ITLayout>
357
357
  )}
358
358
  </ITThemeProvider>
@@ -61,7 +61,8 @@ import { ITCalendar } from '@axzydev/axzy_ui_system';
61
61
  - **month** — Traditional month grid with date picker, single and range selection modes.
62
62
  - **week** — 7-column time grid from 6 AM to 10 PM with event overlays and drag-to-select range.
63
63
  - **day** — Single-column time grid for focused daily scheduling.
64
- - **years** — Click the month/year header to switch to a year selection view (12-year grid).
64
+ - **years** — Click the year in the header to switch to a year selection view (12-year grid).
65
+ - **months** — Click the month name in the header (or pick a year) to switch to a month selection view (12-month grid, same style as the year grid). Flow: header → year → month → days.
65
66
  - Uses `date-fns` with Spanish locale (`es`) for month and weekday names.
66
67
  - Month view supports `minDate`/`maxDate` boundaries — out-of-range dates are disabled and dimmed.
67
68
  - Events render as colored bars in week/day views with customizable `color` prop.
@@ -78,7 +78,7 @@ export const ITCalendar: React.FC<ITCalendarProps> = ({
78
78
  const mode = modeProp || (onChange ? 'month' : 'week');
79
79
 
80
80
  const [currentDate, setCurrentDate] = useState(value || new Date());
81
- const [view, setView] = useState<'calendar' | 'years'>('calendar');
81
+ const [view, setView] = useState<'calendar' | 'years' | 'months'>('calendar');
82
82
 
83
83
  // Selection/Accent colors based on variant
84
84
  const getVariantStyles = () => {
@@ -96,6 +96,25 @@ export const ITCalendar: React.FC<ITCalendarProps> = ({
96
96
  if (value) setCurrentDate(value);
97
97
  }, [value]);
98
98
 
99
+ // Shift the view year while keeping the day-of-month safe (clamped to target month).
100
+ const shiftYear = (yearDelta: number) => {
101
+ setCurrentDate((d) => {
102
+ const year = d.getFullYear() + yearDelta;
103
+ const daysInTarget = new Date(year, d.getMonth() + 1, 0).getDate();
104
+ return new Date(year, d.getMonth(), Math.min(d.getDate(), daysInTarget));
105
+ });
106
+ };
107
+
108
+ // Jump to a month of the currently viewed year (day-of-month clamped).
109
+ const pickMonth = (monthIndex: number) => {
110
+ setCurrentDate((d) => {
111
+ const year = d.getFullYear();
112
+ const daysInTarget = new Date(year, monthIndex + 1, 0).getDate();
113
+ return new Date(year, monthIndex, Math.min(d.getDate(), daysInTarget));
114
+ });
115
+ setView('calendar');
116
+ };
117
+
99
118
  // Navigation handlers
100
119
  const handleNext = () => {
101
120
  if (view === 'years') {
@@ -104,6 +123,8 @@ export const ITCalendar: React.FC<ITCalendarProps> = ({
104
123
  newDate.setFullYear(d.getFullYear() + 12);
105
124
  return newDate;
106
125
  });
126
+ } else if (view === 'months') {
127
+ shiftYear(1);
107
128
  } else if (mode === 'month') {
108
129
  setCurrentDate((d) => addMonths(d, 1));
109
130
  } else if (mode === 'day') {
@@ -120,7 +141,9 @@ export const ITCalendar: React.FC<ITCalendarProps> = ({
120
141
  newDate.setFullYear(d.getFullYear() - 12);
121
142
  return newDate;
122
143
  });
123
- } else if (mode === 'month') {
144
+ } else if (view === 'months') {
145
+ shiftYear(-1);
146
+ } else if (mode === 'month') {
124
147
  setCurrentDate((d) => addMonths(d, -1));
125
148
  } else if (mode === 'day') {
126
149
  setCurrentDate((d) => addDays(d, -1));
@@ -249,6 +272,7 @@ export const ITCalendar: React.FC<ITCalendarProps> = ({
249
272
  // ----------------------------------------------------------------------
250
273
  const startYear = currentDate.getFullYear() - 6;
251
274
  const years = Array.from({ length: 12 }, (_, i) => startYear + i);
275
+ const months = Array.from({ length: 12 }, (_, i) => i);
252
276
 
253
277
  // ----------------------------------------------------------------------
254
278
  // Render
@@ -277,25 +301,50 @@ export const ITCalendar: React.FC<ITCalendarProps> = ({
277
301
  borderBottomColor: 'var(--it-calendar-border, #e2e8f0)'
278
302
  }}
279
303
  >
280
- <ITText
281
- as="h2"
282
- className="text-sm font-bold capitalize cursor-pointer transition-colors select-none px-2 py-1 rounded"
283
- style={{
284
- color: 'var(--it-calendar-header-text, #1e293b)',
285
- }}
286
- onClick={() => setView(view === 'calendar' ? 'years' : 'calendar')}
287
- onMouseEnter={(e) => {
288
- e.currentTarget.style.backgroundColor = 'var(--it-calendar-header-hover, #f1f5f9)';
289
- }}
290
- onMouseLeave={(e) => {
291
- e.currentTarget.style.backgroundColor = 'transparent';
292
- }}
293
- >
294
- {view === 'years'
295
- ? `${years[0]} - ${years[years.length - 1]}`
296
- : format(currentDate, 'MMMM yyyy', { locale: es })
297
- }
298
- </ITText>
304
+ {view === 'years' ? (
305
+ <ITText
306
+ as="h2"
307
+ className="text-sm font-bold select-none px-2 py-1"
308
+ style={{
309
+ color: 'var(--it-calendar-header-text, #1e293b)',
310
+ }}
311
+ >
312
+ {`${years[0]} - ${years[years.length - 1]}`}
313
+ </ITText>
314
+ ) : (
315
+ <div className="flex items-center">
316
+ {view !== 'months' && (
317
+ <button
318
+ type="button"
319
+ onClick={() => setView('months')}
320
+ className="text-sm font-bold capitalize select-none px-2 py-1 rounded transition-colors"
321
+ style={{ color: 'var(--it-calendar-header-text, #1e293b)' }}
322
+ onMouseEnter={(e) => {
323
+ e.currentTarget.style.backgroundColor = 'var(--it-calendar-header-hover, #f1f5f9)';
324
+ }}
325
+ onMouseLeave={(e) => {
326
+ e.currentTarget.style.backgroundColor = 'transparent';
327
+ }}
328
+ >
329
+ {format(currentDate, 'MMMM', { locale: es })}
330
+ </button>
331
+ )}
332
+ <button
333
+ type="button"
334
+ onClick={() => setView('years')}
335
+ className="text-sm font-bold select-none px-2 py-1 rounded transition-colors"
336
+ style={{ color: 'var(--it-calendar-header-text, #1e293b)' }}
337
+ onMouseEnter={(e) => {
338
+ e.currentTarget.style.backgroundColor = 'var(--it-calendar-header-hover, #f1f5f9)';
339
+ }}
340
+ onMouseLeave={(e) => {
341
+ e.currentTarget.style.backgroundColor = 'transparent';
342
+ }}
343
+ >
344
+ {format(currentDate, 'yyyy')}
345
+ </button>
346
+ </div>
347
+ )}
299
348
  <div className="flex items-center gap-1">
300
349
  <button
301
350
  onClick={handlePrev}
@@ -359,13 +408,41 @@ export const ITCalendar: React.FC<ITCalendarProps> = ({
359
408
  newDate.setFullYear(year);
360
409
  return newDate;
361
410
  });
362
- setView('calendar');
411
+ setView('months');
363
412
  }}
364
413
  >
365
414
  <ITText as="span">{year}</ITText>
366
415
  </button>
367
416
  ))}
368
417
  </div>
418
+ ) : view === 'months' ? (
419
+ <div className="p-4 grid grid-cols-4 gap-2">
420
+ {months.map(monthIndex => {
421
+ const isActive = monthIndex === currentDate.getMonth();
422
+ return (
423
+ <button
424
+ key={monthIndex}
425
+ type="button"
426
+ className={cn(
427
+ "h-10 rounded-md text-sm font-medium transition-colors border border-transparent",
428
+ isActive
429
+ ? "bg-[var(--it-calendar-selected-bg)] text-[var(--it-calendar-selected-text)]"
430
+ : "hover:bg-[var(--it-calendar-today-bg)] hover:text-[var(--it-calendar-today-text)]"
431
+ )}
432
+ style={{
433
+ color: isActive
434
+ ? 'var(--it-calendar-selected-text, #ffffff)'
435
+ : 'var(--it-calendar-days-text, #334155)'
436
+ }}
437
+ onClick={() => pickMonth(monthIndex)}
438
+ >
439
+ <ITText as="span" className="capitalize">
440
+ {format(new Date(2000, monthIndex, 1), 'MMM', { locale: es })}
441
+ </ITText>
442
+ </button>
443
+ );
444
+ })}
445
+ </div>
369
446
  ) : mode === 'month' ? (
370
447
  <div className="p-4">
371
448
  {/* Weekday Headers */}
@@ -396,6 +473,7 @@ export const ITCalendar: React.FC<ITCalendarProps> = ({
396
473
  onClick={() => onChange && onChange(day)}
397
474
  className={cn(
398
475
  "h-10 w-full flex items-center justify-center rounded-md text-sm transition-colors relative",
476
+ "text-[var(--it-calendar-days-text)] hover:bg-[var(--it-calendar-today-bg)] hover:text-[var(--it-calendar-today-text)]",
399
477
  !isCurrentMonth && "opacity-40",
400
478
  isDisabled && "opacity-20 cursor-not-allowed",
401
479
  )}
@@ -406,12 +484,12 @@ export const ITCalendar: React.FC<ITCalendarProps> = ({
406
484
  ? 'var(--it-calendar-range-bg, #eff6ff)'
407
485
  : isToday(day)
408
486
  ? 'var(--it-calendar-today-bg, #eff6ff)'
409
- : 'transparent',
487
+ : undefined,
410
488
  color: isSelected || isRangeStart || isRangeEnd
411
489
  ? 'var(--it-calendar-selected-text, #ffffff)'
412
490
  : isToday(day)
413
491
  ? 'var(--it-calendar-today-text, #2563eb)'
414
- : 'var(--it-calendar-days-text, #334155)',
492
+ : undefined,
415
493
  fontWeight: isSelected || isRangeStart || isRangeEnd || isToday(day) ? '700' : '400',
416
494
  }}
417
495
  >