@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
package/package.json
CHANGED
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
|
-
|
|
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
|
-
//
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
|
163
|
-
|
|
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
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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
|
-
//
|
|
182
|
-
const
|
|
183
|
-
|
|
184
|
-
.
|
|
185
|
-
|
|
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
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
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
|
-
|
|
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: () =>
|
|
129
|
+
action: () => goToItem(group.id, sub.id),
|
|
210
130
|
}));
|
|
211
|
-
|
|
212
|
-
const isAnySubitemActive = mappedSubitems.some((sub) => sub.isActive);
|
|
213
|
-
|
|
214
131
|
return {
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
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
|
-
|
|
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
|
|
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 (
|
|
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
|
|
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
|
-
|
|
354
|
-
|
|
355
|
-
|
|
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
|
|
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 (
|
|
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
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
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('
|
|
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
|
-
:
|
|
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
|
-
:
|
|
492
|
+
: undefined,
|
|
415
493
|
fontWeight: isSelected || isRangeStart || isRangeEnd || isToday(day) ? '700' : '400',
|
|
416
494
|
}}
|
|
417
495
|
>
|