@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 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 test`: pruebas unitarias y de renderizado.
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)(async (value) => {
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
- try {
364
- await saveRef.current(value);
365
- if (saveId !== latestSaveId.current) return;
366
- lastSaved.current = serializeRef.current(value);
367
- setStatus("saved");
368
- } catch (cause) {
369
- if (saveId !== latestSaveId.current) return;
370
- setError(cause instanceof Error ? cause : new Error("No se pudo guardar."));
371
- setStatus("error");
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
- const timeout = window.setTimeout(() => void save(data), debounceMs);
383
- return () => window.clearTimeout(timeout);
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: cn("max-h-64 overflow-y-auto", 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: cn("group/combobox flex w-full flex-col gap-1.5", 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: siblingCount * 2 + 1 }, (_, index) => page - siblingCount + index),
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 === page ? "secondary" : "ghost",
3902
+ variant: item === currentPage ? "secondary" : "ghost",
3870
3903
  children: item
3871
3904
  }
3872
3905
  )