@olwiba/ui 0.2.22 → 0.2.24

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.
@@ -9,7 +9,11 @@ export interface PostListProps {
9
9
  posts: PostCardProps[];
10
10
  renderLink?: AppShellRenderLink;
11
11
  emptyMessage?: string;
12
- /** Max columns at the widest breakpoint. */
12
+ /**
13
+ * Max columns at the widest breakpoint. When omitted, one- and two-post
14
+ * collections are balanced automatically instead of leaving an empty third
15
+ * column.
16
+ */
13
17
  columns?: 2 | 3;
14
18
  className?: string;
15
19
  }
@@ -18,20 +22,29 @@ export function PostList({
18
22
  posts,
19
23
  renderLink,
20
24
  emptyMessage = 'No posts published yet.',
21
- columns = 3,
25
+ columns,
22
26
  className,
23
27
  }: PostListProps) {
24
28
  if (posts.length === 0) {
25
- return (
26
- <p className="text-sm text-muted-foreground italic">{emptyMessage}</p>
27
- );
29
+ return <p className="text-sm text-muted-foreground italic">{emptyMessage}</p>;
28
30
  }
29
31
 
32
+ const resolvedColumns = columns ?? (posts.length < 3 ? 2 : 3);
33
+ const balancedShortListClassName =
34
+ columns === undefined
35
+ ? posts.length === 1
36
+ ? 'mx-auto w-full max-w-md sm:grid-cols-1'
37
+ : posts.length === 2
38
+ ? 'mx-auto w-full max-w-3xl'
39
+ : undefined
40
+ : undefined;
41
+
30
42
  return (
31
43
  <div
32
44
  className={cn(
33
45
  'grid grid-cols-1 gap-x-8 gap-y-14 sm:grid-cols-2',
34
- columns === 3 && 'lg:grid-cols-3',
46
+ resolvedColumns === 3 && 'lg:grid-cols-3',
47
+ balancedShortListClassName,
35
48
  className,
36
49
  )}
37
50
  >
@@ -0,0 +1,80 @@
1
+ 'use client';
2
+
3
+ import * as React from 'react';
4
+ import { Button, Spinner, cn } from '@olwiba/cn';
5
+
6
+ export interface LoadMoreProps extends React.HTMLAttributes<HTMLDivElement> {
7
+ hasNextPage: boolean;
8
+ isFetchingNextPage: boolean;
9
+ fetchNextPage: () => void | Promise<unknown>;
10
+ /** Watches the sentinel instead of showing a manual button. */
11
+ auto?: boolean;
12
+ /** Plural noun used by the manual button, for example `properties`. */
13
+ label?: string;
14
+ /** Starts loading before the sentinel reaches the viewport. */
15
+ rootMargin?: string;
16
+ }
17
+
18
+ /** Cursor-pagination footer with either guarded infinite scroll or a manual fallback. */
19
+ export function LoadMore({
20
+ hasNextPage,
21
+ isFetchingNextPage,
22
+ fetchNextPage,
23
+ auto = true,
24
+ label = 'more',
25
+ rootMargin = '400px',
26
+ className,
27
+ ...props
28
+ }: LoadMoreProps) {
29
+ const sentinelRef = React.useRef<HTMLDivElement>(null);
30
+ const stateRef = React.useRef({ hasNextPage, isFetchingNextPage, fetchNextPage });
31
+ stateRef.current = { hasNextPage, isFetchingNextPage, fetchNextPage };
32
+
33
+ React.useEffect(() => {
34
+ if (!auto) return;
35
+ const node = sentinelRef.current;
36
+ if (!node) return;
37
+
38
+ const observer = new IntersectionObserver(
39
+ ([entry]) => {
40
+ if (!entry?.isIntersecting) return;
41
+ const state = stateRef.current;
42
+ if (state.hasNextPage && !state.isFetchingNextPage) void state.fetchNextPage();
43
+ },
44
+ { rootMargin },
45
+ );
46
+
47
+ observer.observe(node);
48
+ return () => observer.disconnect();
49
+ }, [auto, rootMargin]);
50
+
51
+ if (!hasNextPage) return null;
52
+
53
+ return (
54
+ <div ref={sentinelRef} className={cn('flex justify-center py-4', className)} {...props}>
55
+ {auto ? (
56
+ <span className="flex h-9 items-center text-sm text-muted-foreground" aria-live="polite">
57
+ {isFetchingNextPage && (
58
+ <>
59
+ <Spinner className="mr-2 size-4" /> Loading more…
60
+ </>
61
+ )}
62
+ </span>
63
+ ) : (
64
+ <Button
65
+ variant="outline"
66
+ onClick={() => void fetchNextPage()}
67
+ disabled={isFetchingNextPage}
68
+ >
69
+ {isFetchingNextPage ? (
70
+ <>
71
+ <Spinner className="mr-2 size-4" /> Loading…
72
+ </>
73
+ ) : (
74
+ `Load more ${label}`
75
+ )}
76
+ </Button>
77
+ )}
78
+ </div>
79
+ );
80
+ }
package/src/index.ts CHANGED
@@ -26,11 +26,16 @@ export {
26
26
  type AppShellUser,
27
27
  type AppShellAction,
28
28
  type AppShellRenderLink,
29
+ type AppShellBreadcrumb,
30
+ type AppShellHeaderControls,
31
+ type AppShellHeaderStart,
29
32
  type AppNavItem,
30
33
  } from './app/AppShell';
31
34
 
32
35
  export { AppPageHero, type AppPageHeroProps } from './app/AppPageHero';
33
36
 
37
+ export { AppFooter, type AppFooterProps } from './app/AppFooter';
38
+
34
39
  export {
35
40
  AuthSection,
36
41
  type AuthSectionProps,
@@ -142,6 +147,7 @@ export { CommandMenu, type CommandMenuProps, type CommandMenuGroup, type Command
142
147
 
143
148
  // ─── Components — data ───────────────────────────────────────────────────────
144
149
  export { DataTable, type DataTableProps, type DataTableColumn } from './components/DataTable';
150
+ export { LoadMore, type LoadMoreProps } from './components/LoadMore';
145
151
  export { DataView, type DataViewProps } from './app/DataView';
146
152
  export { Chart, type ChartProps, type ChartSeries } from './components/Chart';
147
153
  export { FileUpload, type FileUploadProps, type FileUploadEntry } from './components/FileUpload';