@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.
- package/dist/index.d.ts +106 -10
- package/dist/index.js +341 -103
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
- package/src/app/AppFooter.tsx +39 -0
- package/src/app/AppPageHero.tsx +73 -20
- package/src/app/AppShell.tsx +179 -32
- package/src/app/EmptyState.tsx +63 -12
- package/src/blog/PostList.tsx +19 -6
- package/src/components/LoadMore.tsx +80 -0
- package/src/index.ts +6 -0
package/src/blog/PostList.tsx
CHANGED
|
@@ -9,7 +9,11 @@ export interface PostListProps {
|
|
|
9
9
|
posts: PostCardProps[];
|
|
10
10
|
renderLink?: AppShellRenderLink;
|
|
11
11
|
emptyMessage?: string;
|
|
12
|
-
/**
|
|
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
|
|
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
|
-
|
|
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';
|