Merge branch 'feature/scroll-clientes-proveedores' into feature/filtros-cliente-proveedor

This commit is contained in:
2026-04-24 07:51:40 -06:00
5 changed files with 124 additions and 123 deletions

View File

@@ -13,6 +13,8 @@
loading: boolean;
hasMore: boolean;
loadMore: () => void;
onRowClick?: (row: TData) => void;
selectedId?: number | null;
};
let {
@@ -20,7 +22,9 @@
columns,
loading,
hasMore,
loadMore
loadMore,
onRowClick,
selectedId = null
}: DataTableProps<TData, TValue> = $props();
const table = createSvelteTable({
@@ -33,9 +37,17 @@
let scrollContainer = $state<HTMLDivElement>();
let loadingTrigger = $state<HTMLDivElement>();
let tableContainer = $state<HTMLDivElement>();
let bottomScrollbar = $state<HTMLDivElement>();
let bottomScrollbarInner = $state<HTMLDivElement>();
let isSyncingHorizontalScroll = false;
// Intersection Observer para detectar cuando el usuario llega al final
onMount(() => {
$effect(() => {
const target = loadingTrigger;
const root = scrollContainer;
if (!target) return;
const observer = new IntersectionObserver(
(entries) => {
const [entry] = entries;
@@ -44,32 +56,77 @@
}
},
{
root: scrollContainer,
root: root,
threshold: 0.1
}
);
if (loadingTrigger) {
observer.observe(loadingTrigger);
}
observer.observe(target);
return () => {
observer.disconnect();
};
});
function syncBottomScrollbarWidth() {
if (!tableContainer || !bottomScrollbarInner) return;
const width = Math.max(tableContainer.scrollWidth || 0, tableContainer.clientWidth + 1);
bottomScrollbarInner.style.width = `${width}px`;
}
function handleTableHorizontalScroll() {
if (!tableContainer || !bottomScrollbar || isSyncingHorizontalScroll) return;
isSyncingHorizontalScroll = true;
bottomScrollbar.scrollLeft = tableContainer.scrollLeft;
isSyncingHorizontalScroll = false;
}
function handleBottomHorizontalScroll() {
if (!tableContainer || !bottomScrollbar || isSyncingHorizontalScroll) return;
isSyncingHorizontalScroll = true;
tableContainer.scrollLeft = bottomScrollbar.scrollLeft;
isSyncingHorizontalScroll = false;
}
onMount(() => {
tableContainer = scrollContainer?.querySelector('[data-slot="table-container"]') ?? undefined;
if (!tableContainer) return;
const resizeObserver = new ResizeObserver(() => {
syncBottomScrollbarWidth();
});
tableContainer.addEventListener('scroll', handleTableHorizontalScroll, { passive: true });
resizeObserver.observe(tableContainer);
const tableEl = tableContainer.querySelector('[data-slot="table"]');
if (tableEl) resizeObserver.observe(tableEl);
syncBottomScrollbarWidth();
return () => {
tableContainer?.removeEventListener('scroll', handleTableHorizontalScroll);
resizeObserver.disconnect();
};
});
$effect(() => {
data;
columns;
queueMicrotask(() => syncBottomScrollbarWidth());
});
</script>
<div class="w-full">
<div
class="rounded-md border min-h-[400px] max-h-[calc(100vh-310px)] overflow-y-auto"
class="catalog-table-shell min-h-[400px] max-h-[calc(100vh-310px)] overflow-y-auto [&_[data-slot=table-container]]:w-full [&_[data-slot=table-container]]:overflow-x-auto"
bind:this={scrollContainer}
>
<Table.Root>
<Table.Header class="bg-background">
<Table.Root class="w-max min-w-[1500px]">
<Table.Header class="catalog-table-header">
{#each table.getHeaderGroups() as headerGroup (headerGroup.id)}
<Table.Row>
{#each headerGroup.headers as header (header.id)}
<Table.Head>
<Table.Head class="whitespace-nowrap">
{#if !header.isPlaceholder}
<FlexRender
content={header.column.columnDef.header}
@@ -83,9 +140,13 @@
</Table.Header>
<Table.Body>
{#each table.getRowModel().rows as row (row.id)}
<Table.Row data-state={row.getIsSelected() && "selected"}>
<Table.Row
data-state={row.getIsSelected() && "selected"}
class="cursor-pointer transition-colors hover:bg-muted/50 {selectedId === (row.original as any).id ? 'bg-muted' : ''}"
onclick={() => onRowClick && onRowClick(row.original)}
>
{#each row.getVisibleCells() as cell (cell.id)}
<Table.Cell>
<Table.Cell class="whitespace-nowrap">
<FlexRender
content={cell.column.columnDef.cell}
context={cell.getContext()}
@@ -123,4 +184,11 @@
</Table.Body>
</Table.Root>
</div>
<div
class="sticky bottom-0 z-30 h-5 overflow-x-scroll overflow-y-hidden border-x border-t bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/80 [scrollbar-gutter:stable]"
bind:this={bottomScrollbar}
onscroll={handleBottomHorizontalScroll}
>
<div class="h-full min-w-full" bind:this={bottomScrollbarInner}></div>
</div>
</div>

View File

@@ -44,8 +44,11 @@
let scrollContainer = $state<HTMLDivElement>();
let loadingTrigger = $state<HTMLDivElement>();
onMount(() => {
$effect(() => {
if (!loadMore) return;
const target = loadingTrigger;
const root = scrollContainer;
if (!target) return;
const observer = new IntersectionObserver(
(entries) => {
@@ -55,14 +58,12 @@
}
},
{
root: null, // Relative to viewport if scrollContainer is used as max-h div
root: root,
threshold: 0.1
}
);
if (loadingTrigger) {
observer.observe(loadingTrigger);
}
observer.observe(target);
return () => {
observer.disconnect();

View File

@@ -72,7 +72,11 @@
}
// Intersection Observer para detectar cuando el usuario llega al final
onMount(() => {
$effect(() => {
const target = loadingTrigger;
const root = scrollContainer;
if (!target) return;
const observer = new IntersectionObserver(
(entries) => {
const [entry] = entries;
@@ -81,14 +85,12 @@
}
},
{
root: scrollContainer,
root: root,
threshold: 0.1
}
);
if (loadingTrigger) {
observer.observe(loadingTrigger);
}
observer.observe(target);
return () => {
observer.disconnect();

View File

@@ -35,7 +35,11 @@
let loadingTrigger = $state<HTMLDivElement>();
// Intersection Observer para detectar cuando el usuario llega al final
onMount(() => {
$effect(() => {
const target = loadingTrigger;
const root = scrollContainer;
if (!target) return;
const observer = new IntersectionObserver(
(entries) => {
const [entry] = entries;
@@ -44,14 +48,12 @@
}
},
{
root: scrollContainer,
root: root,
threshold: 0.1
}
);
if (loadingTrigger) {
observer.observe(loadingTrigger);
}
observer.observe(target);
return () => {
observer.disconnect();