@axzydev/axzy_ui_system 1.8.0 → 1.8.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/dist/index.cjs +1819 -1486
- package/dist/index.cjs.map +1 -1
- package/dist/index.css +1 -1
- package/dist/index.css.map +1 -1
- package/dist/index.d.cts +132 -1
- package/dist/index.d.ts +132 -1
- package/dist/index.js +1706 -1384
- package/dist/index.js.map +1 -1
- package/dist/layered.css +1 -1
- package/package.json +1 -1
- package/src/components/organisms/bento/bento.tsx +452 -0
- package/src/components/organisms/data-table/dataTable.tsx +29 -3
- package/src/index.css +262 -1
- package/src/index.ts +15 -0
package/package.json
CHANGED
|
@@ -0,0 +1,452 @@
|
|
|
1
|
+
import type { CSSProperties, ReactNode } from "react";
|
|
2
|
+
import { useEffect, useRef, useState } from "react";
|
|
3
|
+
import clsx from "clsx";
|
|
4
|
+
|
|
5
|
+
/** Tono de superficie del mosaico: blanco por defecto o uno de marca. */
|
|
6
|
+
export type ITBentoTone = "plain" | "sea" | "lavender" | "mint" | "sun";
|
|
7
|
+
|
|
8
|
+
export interface ITBentoTileProps {
|
|
9
|
+
label: ReactNode;
|
|
10
|
+
/** Ícono de línea a la izquierda de la etiqueta. */
|
|
11
|
+
icon?: ReactNode;
|
|
12
|
+
/** Cuántas columnas del mosaico ocupa (1–12). */
|
|
13
|
+
cols?: number;
|
|
14
|
+
/** Cuántas filas de 84 px ocupa. */
|
|
15
|
+
rows?: number;
|
|
16
|
+
tone?: ITBentoTone;
|
|
17
|
+
/** Valor principal, grande y tabular. */
|
|
18
|
+
value?: ReactNode;
|
|
19
|
+
/** Texto de apoyo junto al valor. */
|
|
20
|
+
caption?: ReactNode;
|
|
21
|
+
/** Contenido a la derecha del pie (p. ej. un segundo número). */
|
|
22
|
+
aside?: ReactNode;
|
|
23
|
+
/** Visual al centro (medidor, ola, segmentos…); crece para llenar la tarjeta. */
|
|
24
|
+
children?: ReactNode;
|
|
25
|
+
/** En pantallas chicas ocupa las 6 columnas en vez de 3. */
|
|
26
|
+
wide?: boolean;
|
|
27
|
+
onClick?: () => void;
|
|
28
|
+
className?: string;
|
|
29
|
+
style?: CSSProperties;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Tarjeta del mosaico bento: superficie de radio 20 px con etiqueta, un visual
|
|
34
|
+
* opcional que llena el centro y un pie con el valor grande y su contexto.
|
|
35
|
+
* El tono elige la superficie (`sea` oscura, `lavender`, `mint`, `sun` o blanca);
|
|
36
|
+
* el mosaico lo arma `ITBentoGrid`.
|
|
37
|
+
*/
|
|
38
|
+
export function ITBentoTile({
|
|
39
|
+
label,
|
|
40
|
+
icon,
|
|
41
|
+
cols = 3,
|
|
42
|
+
rows = 1,
|
|
43
|
+
tone = "plain",
|
|
44
|
+
value,
|
|
45
|
+
caption,
|
|
46
|
+
aside,
|
|
47
|
+
children,
|
|
48
|
+
wide = false,
|
|
49
|
+
onClick,
|
|
50
|
+
className,
|
|
51
|
+
style,
|
|
52
|
+
}: ITBentoTileProps) {
|
|
53
|
+
const interactive = typeof onClick === "function";
|
|
54
|
+
return (
|
|
55
|
+
<section
|
|
56
|
+
className={clsx(
|
|
57
|
+
"it-bento-tile",
|
|
58
|
+
tone !== "plain" && `it-bento-tile--${tone}`,
|
|
59
|
+
wide && "it-bento-tile--wide",
|
|
60
|
+
interactive && "cursor-pointer transition-transform hover:-translate-y-0.5",
|
|
61
|
+
className,
|
|
62
|
+
)}
|
|
63
|
+
style={{ gridColumn: `span ${cols}`, gridRow: `span ${rows}`, ...style }}
|
|
64
|
+
onClick={onClick}
|
|
65
|
+
role={interactive ? "button" : undefined}
|
|
66
|
+
tabIndex={interactive ? 0 : undefined}
|
|
67
|
+
onKeyDown={
|
|
68
|
+
interactive
|
|
69
|
+
? (e) => {
|
|
70
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
71
|
+
e.preventDefault();
|
|
72
|
+
onClick?.();
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
: undefined
|
|
76
|
+
}
|
|
77
|
+
>
|
|
78
|
+
<h3 className="it-bento-tile__label">
|
|
79
|
+
{icon}
|
|
80
|
+
{label}
|
|
81
|
+
</h3>
|
|
82
|
+
{children}
|
|
83
|
+
{(value !== undefined || caption || aside) && (
|
|
84
|
+
<div className="it-bento-tile__foot">
|
|
85
|
+
<div className="flex items-baseline gap-2">
|
|
86
|
+
{value !== undefined && <div className="it-bento-tile__value">{value}</div>}
|
|
87
|
+
{caption && <small className="it-bento-tile__caption">{caption}</small>}
|
|
88
|
+
</div>
|
|
89
|
+
{aside}
|
|
90
|
+
</div>
|
|
91
|
+
)}
|
|
92
|
+
</section>
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export interface ITBentoGridProps {
|
|
97
|
+
children: ReactNode;
|
|
98
|
+
className?: string;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Mosaico de 12 columnas con filas de 84 px (6 columnas y alto automático en móvil). */
|
|
102
|
+
export function ITBentoGrid({ children, className }: ITBentoGridProps) {
|
|
103
|
+
return <div className={clsx("it-bento-grid", className)}>{children}</div>;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export interface ITBentoGaugeProps {
|
|
107
|
+
/** Proporción 0..1 del arco. */
|
|
108
|
+
value: number;
|
|
109
|
+
color?: string;
|
|
110
|
+
trackColor?: string;
|
|
111
|
+
/** Ancho del SVG (el alto es proporcional). */
|
|
112
|
+
size?: number;
|
|
113
|
+
/** Tamaño del número de los deciles (para reducir el ruido). */
|
|
114
|
+
strokeWidth?: number;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Medidor semicircular con el arco dibujándose al montar. */
|
|
118
|
+
export function ITBentoGauge({
|
|
119
|
+
value,
|
|
120
|
+
color = "var(--color-warning, #f2c14e)",
|
|
121
|
+
trackColor = "rgb(255 255 255 / 0.14)",
|
|
122
|
+
size = 200,
|
|
123
|
+
strokeWidth = 13,
|
|
124
|
+
}: ITBentoGaugeProps) {
|
|
125
|
+
const v = Math.min(1, Math.max(0, Number.isFinite(value) ? value : 0));
|
|
126
|
+
const [go, setGo] = useState(false);
|
|
127
|
+
useEffect(() => {
|
|
128
|
+
const frame = requestAnimationFrame(() => setGo(true));
|
|
129
|
+
return () => cancelAnimationFrame(frame);
|
|
130
|
+
}, []);
|
|
131
|
+
return (
|
|
132
|
+
<svg
|
|
133
|
+
viewBox="0 0 200 108"
|
|
134
|
+
width={size}
|
|
135
|
+
height={size * 0.54}
|
|
136
|
+
className="mx-auto block h-auto max-h-full w-full max-w-[230px]"
|
|
137
|
+
aria-hidden="true"
|
|
138
|
+
>
|
|
139
|
+
<path
|
|
140
|
+
d="M16 98 A84 84 0 0 1 184 98"
|
|
141
|
+
fill="none"
|
|
142
|
+
strokeLinecap="round"
|
|
143
|
+
strokeWidth={strokeWidth}
|
|
144
|
+
stroke={trackColor}
|
|
145
|
+
/>
|
|
146
|
+
<path
|
|
147
|
+
d="M16 98 A84 84 0 0 1 184 98"
|
|
148
|
+
pathLength={100}
|
|
149
|
+
fill="none"
|
|
150
|
+
strokeLinecap="round"
|
|
151
|
+
strokeWidth={strokeWidth}
|
|
152
|
+
stroke={color}
|
|
153
|
+
strokeDasharray={100}
|
|
154
|
+
strokeDashoffset={go ? 100 - v * 100 : 100}
|
|
155
|
+
style={{ transition: "stroke-dashoffset 1.6s cubic-bezier(.2,.8,.2,1) .2s" }}
|
|
156
|
+
/>
|
|
157
|
+
</svg>
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export interface ITBentoWavesProps {
|
|
162
|
+
/** Proporción 0..1 que define la altura de las bandas. */
|
|
163
|
+
level: number;
|
|
164
|
+
color?: string;
|
|
165
|
+
/** Altura de la zona de olas (%). */
|
|
166
|
+
height?: number;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** Dos bandas de agua animadas, ancladas al pie de la tarjeta. */
|
|
170
|
+
export function ITBentoWaves({ level, color = "var(--color-violet-400, #a895e0)", height = 62 }: ITBentoWavesProps) {
|
|
171
|
+
const v = Math.min(1, Math.max(0, Number.isFinite(level) ? level : 0));
|
|
172
|
+
const h = Math.max(18, Math.min(90, v * 100));
|
|
173
|
+
return (
|
|
174
|
+
<div className="it-bento-wave" style={{ height: `${Math.min(height, 30 + h)}%` }} aria-hidden="true">
|
|
175
|
+
<svg viewBox="0 0 800 100" preserveAspectRatio="none" className="it-bento-wave__b" style={{ opacity: 0.85 }}>
|
|
176
|
+
<path d="M0 55 Q100 30 200 55 T400 55 T600 55 T800 55 V100 H0z" fill={color} />
|
|
177
|
+
</svg>
|
|
178
|
+
<svg viewBox="0 0 800 100" preserveAspectRatio="none" className="it-bento-wave__a" style={{ opacity: 0.5 }}>
|
|
179
|
+
<path d="M0 40 Q100 10 200 40 T400 40 T600 40 T800 40 V100 H0z" fill={color} />
|
|
180
|
+
</svg>
|
|
181
|
+
</div>
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export interface ITBentoSparkProps {
|
|
186
|
+
/** Serie real de valores; con menos de 2 puntos no se dibuja. */
|
|
187
|
+
data: number[];
|
|
188
|
+
color?: string;
|
|
189
|
+
height?: number;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** Línea de tendencia que se dibuja al montar. */
|
|
193
|
+
export function ITBentoSpark({ data, color = "#c9951c", height = 44 }: ITBentoSparkProps) {
|
|
194
|
+
const [drawn, setDrawn] = useState(false);
|
|
195
|
+
useEffect(() => {
|
|
196
|
+
const frame = requestAnimationFrame(() => setDrawn(true));
|
|
197
|
+
return () => cancelAnimationFrame(frame);
|
|
198
|
+
}, []);
|
|
199
|
+
if (!data || data.length < 2) return null;
|
|
200
|
+
const min = Math.min(...data);
|
|
201
|
+
const max = Math.max(...data);
|
|
202
|
+
const span = max - min || 1;
|
|
203
|
+
const pts = data.map((d, i) => {
|
|
204
|
+
const x = (i / (data.length - 1)) * 100;
|
|
205
|
+
const y = 36 - ((d - min) / span) * 30;
|
|
206
|
+
return `${x.toFixed(2)} ${y.toFixed(2)}`;
|
|
207
|
+
});
|
|
208
|
+
return (
|
|
209
|
+
<svg viewBox="0 0 100 40" preserveAspectRatio="none" style={{ height, width: "100%" }} aria-hidden="true">
|
|
210
|
+
<polyline
|
|
211
|
+
points={pts.join(" ")}
|
|
212
|
+
fill="none"
|
|
213
|
+
stroke={color}
|
|
214
|
+
strokeWidth={2.5}
|
|
215
|
+
strokeLinecap="round"
|
|
216
|
+
strokeLinejoin="round"
|
|
217
|
+
pathLength={100}
|
|
218
|
+
strokeDasharray={100}
|
|
219
|
+
strokeDashoffset={drawn ? 0 : 100}
|
|
220
|
+
style={{ transition: "stroke-dashoffset 1.4s ease .3s", vectorEffect: "non-scaling-stroke" }}
|
|
221
|
+
/>
|
|
222
|
+
</svg>
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export interface ITBentoSegmentsProps {
|
|
227
|
+
/** Total de segmentos. */
|
|
228
|
+
total: number;
|
|
229
|
+
/** Segmentos encendidos. */
|
|
230
|
+
done: number;
|
|
231
|
+
color?: string;
|
|
232
|
+
idleColor?: string;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/** Tira de segmentos que se levantan uno tras otro (tareas, piezas, avance). */
|
|
236
|
+
export function ITBentoSegments({
|
|
237
|
+
total,
|
|
238
|
+
done,
|
|
239
|
+
color = "var(--color-success, #2fa374)",
|
|
240
|
+
idleColor = "color-mix(in srgb, currentColor 16%, transparent)",
|
|
241
|
+
}: ITBentoSegmentsProps) {
|
|
242
|
+
const n = Math.max(1, Math.min(60, Math.round(total)));
|
|
243
|
+
const on = Math.max(0, Math.min(n, Math.round(done)));
|
|
244
|
+
return (
|
|
245
|
+
<div className="it-bento-seg" style={{ gridTemplateColumns: `repeat(${n}, 1fr)` }} aria-hidden="true">
|
|
246
|
+
{Array.from({ length: n }, (_, i) => (
|
|
247
|
+
<i
|
|
248
|
+
key={i}
|
|
249
|
+
className={i < on ? "is-on" : undefined}
|
|
250
|
+
style={{ background: i < on ? color : idleColor, ["--it-bento-i" as string]: i }}
|
|
251
|
+
/>
|
|
252
|
+
))}
|
|
253
|
+
</div>
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
export interface ITBentoWeekBarsProps {
|
|
258
|
+
items: { label: string; value: number; tone?: "today" | "future" | "past" }[];
|
|
259
|
+
color?: string;
|
|
260
|
+
todayColor?: string;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/** Barras por día de la semana; el día de hoy va en el color de acento. */
|
|
264
|
+
export function ITBentoWeekBars({
|
|
265
|
+
items,
|
|
266
|
+
color = "var(--it-bento-ink, #0c3b52)",
|
|
267
|
+
todayColor = "var(--color-warning, #f2c14e)",
|
|
268
|
+
}: ITBentoWeekBarsProps) {
|
|
269
|
+
const max = Math.max(1, ...items.map((i) => i.value));
|
|
270
|
+
return (
|
|
271
|
+
<div className="mt-1 flex min-h-0 flex-1 items-end gap-1.5">
|
|
272
|
+
{items.map((item, i) => (
|
|
273
|
+
<div key={item.label} className="flex h-full flex-1 flex-col items-center justify-end gap-1 text-[10px]" style={{ color: "var(--it-bento-muted)" }}>
|
|
274
|
+
<i
|
|
275
|
+
className="w-full rounded-t-[5px] rounded-b-[2px] transition-[height] duration-700"
|
|
276
|
+
style={{
|
|
277
|
+
height: `${Math.max(6, (item.value / max) * 100)}%`,
|
|
278
|
+
background:
|
|
279
|
+
item.tone === "today" ? todayColor : item.tone === "future" ? "color-mix(in srgb, currentColor 14%, transparent)" : color,
|
|
280
|
+
transitionDelay: `${i * 60 + 150}ms`,
|
|
281
|
+
}}
|
|
282
|
+
/>
|
|
283
|
+
{item.label}
|
|
284
|
+
</div>
|
|
285
|
+
))}
|
|
286
|
+
</div>
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
export interface ITBentoDonutProps {
|
|
291
|
+
segments: { label: string; value: number; color: string }[];
|
|
292
|
+
/** Texto central y su bajada. */
|
|
293
|
+
center?: ReactNode;
|
|
294
|
+
caption?: string;
|
|
295
|
+
size?: number;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/** Dona de segmentos con texto al centro (la leyenda va aparte). */
|
|
299
|
+
export function ITBentoDonut({ segments, center, caption, size = 112 }: ITBentoDonutProps) {
|
|
300
|
+
const total = segments.reduce((acc, s) => acc + s.value, 0);
|
|
301
|
+
if (total <= 0) return null;
|
|
302
|
+
const r = 38;
|
|
303
|
+
const circ = 2 * Math.PI * r;
|
|
304
|
+
let offset = 0;
|
|
305
|
+
return (
|
|
306
|
+
<svg width={size} height={size} viewBox="0 0 100 100" role="img" aria-label={caption}>
|
|
307
|
+
<circle cx="50" cy="50" r={r} fill="none" strokeWidth={13} stroke="color-mix(in srgb, currentColor 10%, transparent)" />
|
|
308
|
+
{segments.map((s) => {
|
|
309
|
+
const len = (s.value / total) * circ;
|
|
310
|
+
const el = (
|
|
311
|
+
<circle
|
|
312
|
+
key={s.label}
|
|
313
|
+
cx="50"
|
|
314
|
+
cy="50"
|
|
315
|
+
r={r}
|
|
316
|
+
fill="none"
|
|
317
|
+
strokeWidth={13}
|
|
318
|
+
stroke={s.color}
|
|
319
|
+
strokeLinecap="butt"
|
|
320
|
+
strokeDasharray={`${Math.max(0, len - 1)} ${circ - Math.max(0, len - 1)}`}
|
|
321
|
+
strokeDashoffset={-offset}
|
|
322
|
+
transform="rotate(-90 50 50)"
|
|
323
|
+
/>
|
|
324
|
+
);
|
|
325
|
+
offset += len;
|
|
326
|
+
return el;
|
|
327
|
+
})}
|
|
328
|
+
{center !== undefined && (
|
|
329
|
+
<text x="50" y="52" textAnchor="middle" fontSize="24" fontWeight={800} fill="currentColor">
|
|
330
|
+
{center}
|
|
331
|
+
</text>
|
|
332
|
+
)}
|
|
333
|
+
{caption && (
|
|
334
|
+
<text x="50" y="65" textAnchor="middle" fontSize="7.5" fontWeight={500} fill="var(--it-bento-muted)">
|
|
335
|
+
{caption}
|
|
336
|
+
</text>
|
|
337
|
+
)}
|
|
338
|
+
</svg>
|
|
339
|
+
);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
export interface ITBentoFeedItem {
|
|
343
|
+
id: string;
|
|
344
|
+
/** Ícono dentro del cuadro de color. */
|
|
345
|
+
icon?: ReactNode;
|
|
346
|
+
title: string;
|
|
347
|
+
meta?: string;
|
|
348
|
+
tone?: "success" | "violet" | "neutral";
|
|
349
|
+
href?: string;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
export interface ITBentoFeedProps {
|
|
353
|
+
groups: { key: string; label: string; items: ITBentoFeedItem[] }[];
|
|
354
|
+
onItemClick?: (item: ITBentoFeedItem) => void;
|
|
355
|
+
/** Alto máximo del hilo; hace scroll a partir de ahí. */
|
|
356
|
+
maxHeight?: number;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/** Actividad agrupada por día, con cuadro de ícono y hora. */
|
|
360
|
+
export function ITBentoFeed({ groups, onItemClick, maxHeight = 330 }: ITBentoFeedProps) {
|
|
361
|
+
const toneClass = {
|
|
362
|
+
success: "bg-[var(--color-success-soft)] text-[color:var(--color-success)]",
|
|
363
|
+
violet: "bg-[var(--color-primary-50)] text-[color:var(--color-primary-600)]",
|
|
364
|
+
neutral: "bg-[var(--color-secondary-100)] text-[color:var(--color-text-muted)]",
|
|
365
|
+
} as const;
|
|
366
|
+
return (
|
|
367
|
+
<ul className="m-0 list-none overflow-y-auto p-0" style={{ maxHeight }}>
|
|
368
|
+
{groups.map((group) => (
|
|
369
|
+
<li key={group.key} className="list-none">
|
|
370
|
+
<p className="m-0 px-1.5 pb-1 pt-2.5 text-[11px] font-semibold" style={{ color: "var(--it-bento-muted)" }}>
|
|
371
|
+
{group.label}
|
|
372
|
+
</p>
|
|
373
|
+
<ul className="m-0 list-none p-0">
|
|
374
|
+
{group.items.map((item) => (
|
|
375
|
+
<li key={item.id} className="list-none">
|
|
376
|
+
<button
|
|
377
|
+
type="button"
|
|
378
|
+
onClick={() => onItemClick?.(item)}
|
|
379
|
+
className={clsx(
|
|
380
|
+
"flex w-full items-center gap-2.5 rounded-[11px] px-1.5 py-1.5 text-left",
|
|
381
|
+
onItemClick ? "cursor-pointer hover:bg-[color-mix(in_srgb,currentColor_6%,transparent)]" : "cursor-default",
|
|
382
|
+
)}
|
|
383
|
+
>
|
|
384
|
+
<span className={clsx("grid h-7 w-7 shrink-0 place-items-center rounded-[9px]", toneClass[item.tone ?? "neutral"])}>
|
|
385
|
+
{item.icon}
|
|
386
|
+
</span>
|
|
387
|
+
<span className="min-w-0 flex-1">
|
|
388
|
+
<span className="block truncate text-[12.5px] font-semibold">{item.title}</span>
|
|
389
|
+
{item.meta && (
|
|
390
|
+
<span className="block text-[11px]" style={{ color: "var(--it-bento-muted)" }}>
|
|
391
|
+
{item.meta}
|
|
392
|
+
</span>
|
|
393
|
+
)}
|
|
394
|
+
</span>
|
|
395
|
+
</button>
|
|
396
|
+
</li>
|
|
397
|
+
))}
|
|
398
|
+
</ul>
|
|
399
|
+
</li>
|
|
400
|
+
))}
|
|
401
|
+
</ul>
|
|
402
|
+
);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
export interface ITBentoLiveProps {
|
|
406
|
+
label: string;
|
|
407
|
+
className?: string;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/** Píldora "En vivo" con punto que late. */
|
|
411
|
+
export function ITBentoLive({ label, className }: ITBentoLiveProps) {
|
|
412
|
+
return (
|
|
413
|
+
<span
|
|
414
|
+
className={clsx(
|
|
415
|
+
"inline-flex items-center gap-1.5 rounded-full bg-[var(--color-success-soft)] px-2.5 py-1 text-[12px] font-semibold text-[color:var(--color-success)]",
|
|
416
|
+
className,
|
|
417
|
+
)}
|
|
418
|
+
>
|
|
419
|
+
<i
|
|
420
|
+
className="h-1.5 w-1.5 rounded-full bg-[color:var(--color-success)]"
|
|
421
|
+
style={{ boxShadow: "0 0 0 0 color-mix(in srgb, var(--color-success) 55%, transparent)", animation: "it-bento-pulse 2.4s ease-out infinite" }}
|
|
422
|
+
/>
|
|
423
|
+
{label}
|
|
424
|
+
</span>
|
|
425
|
+
);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/** Contador que sube de 0 al valor al montar (para el número grande de la tarjeta). */
|
|
429
|
+
export function useITBentoCountUp(target: number, duration = 1400, decimals = 0) {
|
|
430
|
+
const [shown, setShown] = useState(0);
|
|
431
|
+
const ref = useRef(0);
|
|
432
|
+
useEffect(() => {
|
|
433
|
+
if (!Number.isFinite(target)) return;
|
|
434
|
+
if (typeof window === "undefined" || window.matchMedia?.("(prefers-reduced-motion: reduce)").matches) {
|
|
435
|
+
setShown(target);
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
const start = performance.now();
|
|
439
|
+
let raf = 0;
|
|
440
|
+
const tick = (now: number) => {
|
|
441
|
+
const p = Math.min(Math.max((now - start) / duration, 0), 1);
|
|
442
|
+
const eased = 1 - Math.pow(1 - p, 4);
|
|
443
|
+
ref.current = target * eased;
|
|
444
|
+
setShown(Number(ref.current.toFixed(decimals)));
|
|
445
|
+
if (p < 1) raf = requestAnimationFrame(tick);
|
|
446
|
+
else setShown(target);
|
|
447
|
+
};
|
|
448
|
+
raf = requestAnimationFrame(tick);
|
|
449
|
+
return () => cancelAnimationFrame(raf);
|
|
450
|
+
}, [target, duration, decimals]);
|
|
451
|
+
return shown;
|
|
452
|
+
}
|
|
@@ -147,6 +147,29 @@ export default function ITDataTable<T extends Record<string, unknown>>({
|
|
|
147
147
|
const isTableVirtual = virtualized && effectiveView === "table";
|
|
148
148
|
const isStickyHeader = stickyHeader && isTableVirtual;
|
|
149
149
|
const resolvedRowHeight = rowHeight ?? getRowHeight(size, density);
|
|
150
|
+
|
|
151
|
+
// La virtualización espacios filas de `resolvedRowHeight`; si el contenedor
|
|
152
|
+
// corta a una altura arbitraria, la última fila sale partida. Medimos el
|
|
153
|
+
// encabezado y recortamos a un múltiplo exacto de fila.
|
|
154
|
+
const headRef = useRef<HTMLTableSectionElement | null>(null);
|
|
155
|
+
const [headHeight, setHeadHeight] = useState(0);
|
|
156
|
+
useEffect(() => {
|
|
157
|
+
if (!isTableVirtual) return;
|
|
158
|
+
const el = headRef.current;
|
|
159
|
+
if (!el) return;
|
|
160
|
+
const measure = () => setHeadHeight(el.getBoundingClientRect().height);
|
|
161
|
+
measure();
|
|
162
|
+
if (typeof ResizeObserver === "undefined") return;
|
|
163
|
+
const ro = new ResizeObserver(measure);
|
|
164
|
+
ro.observe(el);
|
|
165
|
+
return () => ro.disconnect();
|
|
166
|
+
}, [isTableVirtual, resolvedRowHeight]);
|
|
167
|
+
|
|
168
|
+
const snappedMaxHeight =
|
|
169
|
+
isTableVirtual && headHeight > 0
|
|
170
|
+
? headHeight +
|
|
171
|
+
Math.max(1, Math.floor((virtualizedMaxHeight - headHeight) / resolvedRowHeight)) * resolvedRowHeight
|
|
172
|
+
: virtualizedMaxHeight;
|
|
150
173
|
const columnCount = columns.length;
|
|
151
174
|
const pinnedMeta = React.useMemo(() => getPinnedColumnMeta(columns), [columns]);
|
|
152
175
|
const scrollOuterClass = isTableVirtual
|
|
@@ -507,7 +530,7 @@ export default function ITDataTable<T extends Record<string, unknown>>({
|
|
|
507
530
|
))}
|
|
508
531
|
</colgroup>
|
|
509
532
|
)}
|
|
510
|
-
<thead>
|
|
533
|
+
<thead ref={headRef}>
|
|
511
534
|
<tr
|
|
512
535
|
className={clsx(tableHeaderRow, "dark:text-slate-200", headerSlot.className)}
|
|
513
536
|
style={headerSlot.style}
|
|
@@ -590,7 +613,10 @@ export default function ITDataTable<T extends Record<string, unknown>>({
|
|
|
590
613
|
onRowClick && !isLoading && tableRowClickable,
|
|
591
614
|
rowSlot.className
|
|
592
615
|
)}
|
|
593
|
-
style={
|
|
616
|
+
style={{
|
|
617
|
+
...(isTableVirtual ? { height: resolvedRowHeight } : null),
|
|
618
|
+
...rowSlot.style,
|
|
619
|
+
}}
|
|
594
620
|
tabIndex={onRowClick && !isLoading ? 0 : undefined}
|
|
595
621
|
onClick={onRowClick && !isLoading ? handleRowClick(row) : undefined}
|
|
596
622
|
onKeyDown={onRowClick && !isLoading ? handleRowKeyDown(row) : undefined}
|
|
@@ -740,7 +766,7 @@ export default function ITDataTable<T extends Record<string, unknown>>({
|
|
|
740
766
|
<div
|
|
741
767
|
ref={scrollRef}
|
|
742
768
|
className="overflow-x-auto overflow-y-auto"
|
|
743
|
-
style={{ maxHeight:
|
|
769
|
+
style={{ maxHeight: snappedMaxHeight }}
|
|
744
770
|
>
|
|
745
771
|
{tableEl}
|
|
746
772
|
</div>
|