@doscientos/ui 0.1.33 → 0.1.34
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 +25 -1
- package/dist/index.cjs +60 -27
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +60 -27
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -76,7 +76,19 @@ Las primitivas de composición no conocen rutas, entidades ni transporte: cada f
|
|
|
76
76
|
## Estado asíncrono, errores y copia
|
|
77
77
|
|
|
78
78
|
- `useAsyncAction(action)` evita dobles ejecuciones mientras está pendiente y devuelve `run`, `status`, `isPending`, `data`, `error` y `reset`.
|
|
79
|
+
- `reset` no cancela peticiones y no oculta una operación pendiente. Cada ejecución
|
|
80
|
+
limpia el resultado anterior para que un fallo no presente datos de un éxito antiguo.
|
|
81
|
+
- `useAutosave` serializa las escrituras de una instancia para que una petición
|
|
82
|
+
antigua no termine sobrescribiendo la siguiente. `saveNow` cancela el debounce
|
|
83
|
+
pendiente y fuerza un guardado explícito; los errores quedan en `status`/`error`.
|
|
84
|
+
Usa datos inmutables y una instancia por documento (remonta con `key` al cambiar
|
|
85
|
+
de entidad). El servidor todavía necesita control de versión para varios usuarios
|
|
86
|
+
o pestañas; un timeout no prueba que una escritura remota no se haya confirmado.
|
|
87
|
+
Al desmontar se descartan guardados en cola, no se cancelan escrituras ya enviadas.
|
|
79
88
|
- `ErrorBoundary` aporta fallback recuperable, `resetKeys` y `onError`; `AsyncBoundary` combina error boundary con `Suspense`.
|
|
89
|
+
- `fallback={null}` oculta el contenido explícitamente. Los errores lanzados que no
|
|
90
|
+
sean instancias de `Error` se normalizan. No sustituye los errores de loaders,
|
|
91
|
+
promesas o eventos: esos se gestionan en el router o en la acción correspondiente.
|
|
80
92
|
- `ErrorState` es el fallback visual componible. La aplicación inyecta su acción de reintento, no el paquete.
|
|
81
93
|
- `useClipboard` y `CopyButton` resuelven copia, feedback y errores sin acoplarse a Sileo; usa `onCopied` u `onCopyError` para analytics/toasts de producto.
|
|
82
94
|
|
|
@@ -91,9 +103,21 @@ router.replace(`?${next}`)
|
|
|
91
103
|
|
|
92
104
|
No añadas hooks de Next, React Router ni TanStack Router al paquete. Las stories documentan componentes visuales; los hooks y utilidades puras se documentan aquí y se cubren con pruebas unitarias.
|
|
93
105
|
|
|
106
|
+
## Contratos de estilos y navegación
|
|
107
|
+
|
|
108
|
+
- Los slots composables de Combobox conservan tanto clases estáticas como funciones
|
|
109
|
+
`className(state)` de React Aria, combinándolas con los estilos base.
|
|
110
|
+
- `Pagination` normaliza página y vecinos; `siblingCount` se limita a 0–10 para no
|
|
111
|
+
generar listas de botones sin límite. Estado visual y `aria-current` coinciden.
|
|
112
|
+
- Para que los overlays portaled hereden la marca y el tema, aplica los tokens y
|
|
113
|
+
`.dark` en la raíz del documento, no solo en un contenedor del contenido.
|
|
114
|
+
- No hay integración con un router implícita: la aplicación configura navegación
|
|
115
|
+
SPA mediante las APIs del router o del proveedor de React Aria según corresponda.
|
|
116
|
+
|
|
94
117
|
## Desarrollo
|
|
95
118
|
|
|
96
|
-
- `pnpm
|
|
119
|
+
- `pnpm quality`: formato, lint, tipos y tests; es el control del paquete en CI.
|
|
120
|
+
- `pnpm test`: pruebas del contrato de empaquetado, unitarias y de renderizado.
|
|
97
121
|
- `pnpm test:storybook`: renderizado, accesibilidad e interacciones de todas las stories en Chromium.
|
|
98
122
|
- `pnpm typecheck`: contrato TypeScript.
|
|
99
123
|
- `pnpm build`: distribución JS, tipos y CSS Tailwind compilado.
|
package/dist/index.cjs
CHANGED
|
@@ -314,6 +314,7 @@ function useAsyncAction(action) {
|
|
|
314
314
|
pendingRef.current = true;
|
|
315
315
|
setStatus("pending");
|
|
316
316
|
setError(null);
|
|
317
|
+
setData(null);
|
|
317
318
|
try {
|
|
318
319
|
const result = await action(...args);
|
|
319
320
|
setData(result);
|
|
@@ -330,6 +331,7 @@ function useAsyncAction(action) {
|
|
|
330
331
|
[action]
|
|
331
332
|
);
|
|
332
333
|
const reset = (0, import_react.useCallback)(() => {
|
|
334
|
+
if (pendingRef.current) return;
|
|
333
335
|
setData(null);
|
|
334
336
|
setError(null);
|
|
335
337
|
setStatus("idle");
|
|
@@ -352,24 +354,45 @@ function useAutosave({
|
|
|
352
354
|
const saveRef = (0, import_react2.useRef)(onSave);
|
|
353
355
|
const serializeRef = (0, import_react2.useRef)(serialize);
|
|
354
356
|
const latestSaveId = (0, import_react2.useRef)(0);
|
|
357
|
+
const queue = (0, import_react2.useRef)(Promise.resolve());
|
|
358
|
+
const queuedCount = (0, import_react2.useRef)(0);
|
|
359
|
+
const timeoutRef = (0, import_react2.useRef)(void 0);
|
|
360
|
+
const mounted = (0, import_react2.useRef)(true);
|
|
361
|
+
(0, import_react2.useEffect)(() => {
|
|
362
|
+
mounted.current = true;
|
|
363
|
+
return () => {
|
|
364
|
+
mounted.current = false;
|
|
365
|
+
clearTimeout(timeoutRef.current);
|
|
366
|
+
};
|
|
367
|
+
}, []);
|
|
355
368
|
(0, import_react2.useEffect)(() => {
|
|
356
369
|
saveRef.current = onSave;
|
|
357
370
|
serializeRef.current = serialize;
|
|
358
371
|
}, [onSave, serialize]);
|
|
359
|
-
const save = (0, import_react2.useCallback)(
|
|
372
|
+
const save = (0, import_react2.useCallback)((value, force = false) => {
|
|
373
|
+
clearTimeout(timeoutRef.current);
|
|
360
374
|
const saveId = ++latestSaveId.current;
|
|
375
|
+
const snapshot = serializeRef.current(value);
|
|
376
|
+
const write = saveRef.current;
|
|
377
|
+
queuedCount.current += 1;
|
|
361
378
|
setStatus("saving");
|
|
362
379
|
setError(null);
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
380
|
+
const pending = queue.current.then(async () => {
|
|
381
|
+
try {
|
|
382
|
+
if (!mounted.current) return;
|
|
383
|
+
if (force || lastSaved.current !== snapshot) await write(value);
|
|
384
|
+
lastSaved.current = snapshot;
|
|
385
|
+
if (mounted.current && saveId === latestSaveId.current) setStatus("saved");
|
|
386
|
+
} catch (cause) {
|
|
387
|
+
if (!mounted.current || saveId !== latestSaveId.current) return;
|
|
388
|
+
setError(cause instanceof Error ? cause : new Error("No se pudo guardar."));
|
|
389
|
+
setStatus("error");
|
|
390
|
+
} finally {
|
|
391
|
+
queuedCount.current -= 1;
|
|
392
|
+
}
|
|
393
|
+
});
|
|
394
|
+
queue.current = pending;
|
|
395
|
+
return pending;
|
|
373
396
|
}, []);
|
|
374
397
|
(0, import_react2.useEffect)(() => {
|
|
375
398
|
if (!enabled) return;
|
|
@@ -378,11 +401,11 @@ function useAutosave({
|
|
|
378
401
|
lastSaved.current = snapshot;
|
|
379
402
|
return;
|
|
380
403
|
}
|
|
381
|
-
if (snapshot === lastSaved.current) return;
|
|
382
|
-
|
|
383
|
-
return () =>
|
|
404
|
+
if (snapshot === lastSaved.current && queuedCount.current === 0) return;
|
|
405
|
+
timeoutRef.current = setTimeout(() => void save(data), debounceMs);
|
|
406
|
+
return () => clearTimeout(timeoutRef.current);
|
|
384
407
|
}, [data, debounceMs, enabled, save]);
|
|
385
|
-
return { status, error, saveNow: () => save(data) };
|
|
408
|
+
return { status, error, saveNow: () => save(data, true) };
|
|
386
409
|
}
|
|
387
410
|
|
|
388
411
|
// src/hooks/use-clipboard.ts
|
|
@@ -1783,10 +1806,10 @@ function ComboboxContent({ className, ...props }) {
|
|
|
1783
1806
|
import_react_aria_components9.Popover,
|
|
1784
1807
|
{
|
|
1785
1808
|
"data-slot": "combobox-content",
|
|
1786
|
-
className: cn(
|
|
1809
|
+
className: (state) => cn(
|
|
1787
1810
|
floatingSurfaceClassName,
|
|
1788
1811
|
"max-h-72 w-(--trigger-width) overflow-hidden rounded-xl border border-border bg-background p-1.5 text-foreground",
|
|
1789
|
-
className
|
|
1812
|
+
typeof className === "function" ? className(state) : className
|
|
1790
1813
|
),
|
|
1791
1814
|
...props
|
|
1792
1815
|
}
|
|
@@ -1801,7 +1824,10 @@ function ComboboxList({
|
|
|
1801
1824
|
import_react_aria_components9.ListBox,
|
|
1802
1825
|
{
|
|
1803
1826
|
"data-slot": "combobox-list",
|
|
1804
|
-
className:
|
|
1827
|
+
className: (state) => cn(
|
|
1828
|
+
"max-h-64 overflow-y-auto",
|
|
1829
|
+
typeof className === "function" ? className(state) : className
|
|
1830
|
+
),
|
|
1805
1831
|
renderEmptyState: emptyState ? () => emptyState : void 0,
|
|
1806
1832
|
...props
|
|
1807
1833
|
}
|
|
@@ -1816,9 +1842,9 @@ function ComboboxItem({
|
|
|
1816
1842
|
import_react_aria_components9.ListBoxItem,
|
|
1817
1843
|
{
|
|
1818
1844
|
"data-slot": "combobox-item",
|
|
1819
|
-
className: cn(
|
|
1845
|
+
className: (state) => cn(
|
|
1820
1846
|
"flex w-full cursor-default items-center justify-between rounded-md px-2 py-2 text-sm outline-none transition-colors data-focused:bg-muted data-focused:text-foreground data-hovered:bg-muted data-disabled:pointer-events-none data-disabled:opacity-50",
|
|
1821
|
-
className
|
|
1847
|
+
typeof className === "function" ? className(state) : className
|
|
1822
1848
|
),
|
|
1823
1849
|
...props,
|
|
1824
1850
|
children
|
|
@@ -1893,7 +1919,10 @@ function AutocompleteCombobox({
|
|
|
1893
1919
|
import_react_aria_components9.ComboBox,
|
|
1894
1920
|
{
|
|
1895
1921
|
...props,
|
|
1896
|
-
className:
|
|
1922
|
+
className: (state) => cn(
|
|
1923
|
+
"group/combobox flex w-full flex-col gap-1.5",
|
|
1924
|
+
typeof className === "function" ? className(state) : className
|
|
1925
|
+
),
|
|
1897
1926
|
items: filteredItems,
|
|
1898
1927
|
selectedKey,
|
|
1899
1928
|
inputValue,
|
|
@@ -2721,16 +2750,19 @@ function ErrorStateActions({ className, ...props }) {
|
|
|
2721
2750
|
|
|
2722
2751
|
// src/ui/error-boundary/error-boundary.tsx
|
|
2723
2752
|
var import_jsx_runtime28 = require("react/jsx-runtime");
|
|
2753
|
+
function normalizeError(cause) {
|
|
2754
|
+
return cause instanceof Error ? cause : new Error("No se pudo renderizar este contenido.", { cause });
|
|
2755
|
+
}
|
|
2724
2756
|
function changedResetKeys(previous = [], next = []) {
|
|
2725
2757
|
return previous.length !== next.length || previous.some((value, index) => !Object.is(value, next[index]));
|
|
2726
2758
|
}
|
|
2727
2759
|
var Boundary = class extends import_react9.Component {
|
|
2728
2760
|
state = { error: null };
|
|
2729
2761
|
static getDerivedStateFromError(error) {
|
|
2730
|
-
return { error };
|
|
2762
|
+
return { error: normalizeError(error) };
|
|
2731
2763
|
}
|
|
2732
2764
|
componentDidCatch(error, info) {
|
|
2733
|
-
this.props.onError?.(error, info);
|
|
2765
|
+
this.props.onError?.(this.state.error ?? normalizeError(error), info);
|
|
2734
2766
|
}
|
|
2735
2767
|
componentDidUpdate(previousProps) {
|
|
2736
2768
|
if (this.state.error && changedResetKeys(previousProps.resetKeys, this.props.resetKeys))
|
|
@@ -2742,7 +2774,7 @@ var Boundary = class extends import_react9.Component {
|
|
|
2742
2774
|
if (!this.state.error) return children;
|
|
2743
2775
|
if (typeof fallback === "function")
|
|
2744
2776
|
return fallback({ error: this.state.error, reset: this.reset });
|
|
2745
|
-
if (fallback) return fallback;
|
|
2777
|
+
if (fallback !== void 0) return fallback;
|
|
2746
2778
|
return /* @__PURE__ */ (0, import_jsx_runtime28.jsxs)(ErrorState, { children: [
|
|
2747
2779
|
/* @__PURE__ */ (0, import_jsx_runtime28.jsx)(ErrorStateIcon, {}),
|
|
2748
2780
|
/* @__PURE__ */ (0, import_jsx_runtime28.jsx)(ErrorStateTitle, { children: "No se pudo cargar este contenido" }),
|
|
@@ -3808,16 +3840,17 @@ var import_lucide_react12 = require("lucide-react");
|
|
|
3808
3840
|
var React6 = __toESM(require("react"), 1);
|
|
3809
3841
|
var import_jsx_runtime47 = require("react/jsx-runtime");
|
|
3810
3842
|
function visiblePages(page, pageCount, siblingCount) {
|
|
3843
|
+
const siblings = Number.isFinite(siblingCount) ? Math.min(10, Math.max(0, Math.floor(siblingCount))) : 1;
|
|
3811
3844
|
return [
|
|
3812
3845
|
.../* @__PURE__ */ new Set([
|
|
3813
3846
|
1,
|
|
3814
|
-
...Array.from({ length:
|
|
3847
|
+
...Array.from({ length: siblings * 2 + 1 }, (_, index) => page - siblings + index),
|
|
3815
3848
|
pageCount
|
|
3816
3849
|
])
|
|
3817
3850
|
].filter((item) => item >= 1 && item <= pageCount).sort((left, right) => left - right);
|
|
3818
3851
|
}
|
|
3819
3852
|
function normalizedPageCount(pageCount) {
|
|
3820
|
-
return Number.isFinite(pageCount) ? Math.max(0, Math.floor(pageCount)) : 0;
|
|
3853
|
+
return Number.isFinite(pageCount) ? Math.min(Number.MAX_SAFE_INTEGER, Math.max(0, Math.floor(pageCount))) : 0;
|
|
3821
3854
|
}
|
|
3822
3855
|
function normalizedPage(page, pageCount) {
|
|
3823
3856
|
if (!Number.isFinite(page)) return 1;
|
|
@@ -3866,7 +3899,7 @@ function Pagination({
|
|
|
3866
3899
|
"aria-label": `P\xE1gina ${item}`,
|
|
3867
3900
|
onPress: () => onPageChange(item),
|
|
3868
3901
|
size: "icon",
|
|
3869
|
-
variant: item ===
|
|
3902
|
+
variant: item === currentPage ? "secondary" : "ghost",
|
|
3870
3903
|
children: item
|
|
3871
3904
|
}
|
|
3872
3905
|
)
|