@olwiba/ui 0.2.23 → 0.2.25
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 +104 -12
- package/dist/index.js +231 -84
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/app/AppFooter.tsx +4 -3
- package/src/app/AppShell.tsx +72 -26
- package/src/app/EmptyState.tsx +63 -12
- package/src/blog/PostList.tsx +19 -6
- package/src/components/Chart.tsx +38 -7
- package/src/components/DataTable.tsx +56 -8
- package/src/components/LoadMore.tsx +80 -0
- package/src/components/Notify.tsx +32 -1
- package/src/index.ts +3 -0
- package/src/marketing/Footer.tsx +7 -1
|
@@ -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
|
+
}
|
|
@@ -86,11 +86,42 @@ export function NotificationToast({
|
|
|
86
86
|
}
|
|
87
87
|
|
|
88
88
|
export interface NotifyOptions extends Omit<NotificationToastProps, 'onDismiss'> {
|
|
89
|
+
/**
|
|
90
|
+
* Overrides the per-variant default below. Explicit always wins — a caller
|
|
91
|
+
* that has thought about the timing knows more than this heuristic does.
|
|
92
|
+
*/
|
|
89
93
|
duration?: number;
|
|
90
94
|
}
|
|
91
95
|
|
|
96
|
+
/**
|
|
97
|
+
* How long a toast stays, when the caller has not said.
|
|
98
|
+
*
|
|
99
|
+
* The Toaster's own default suits a confirmation: long enough to read "Saved",
|
|
100
|
+
* short enough not to linger. Two cases need longer, and both were previously
|
|
101
|
+
* left to whoever remembered to pass a number.
|
|
102
|
+
*
|
|
103
|
+
* A failure has to be *read* — often it is the only account of what went
|
|
104
|
+
* wrong, and a message that vanishes before it is understood is barely better
|
|
105
|
+
* than none. A toast carrying an action has to be read *and* decided on, and a
|
|
106
|
+
* button that disappears mid-reach is worse than no button, because the reader
|
|
107
|
+
* knows they missed something.
|
|
108
|
+
*
|
|
109
|
+
* Returning undefined defers to the Toaster, so a product that has set its own
|
|
110
|
+
* default keeps it rather than being silently overridden here.
|
|
111
|
+
*/
|
|
112
|
+
function defaultDuration(variant: NotificationToastProps['variant'], hasAction: boolean) {
|
|
113
|
+
if (variant === 'error') return 10_000;
|
|
114
|
+
if (variant === 'warning') return 8_000;
|
|
115
|
+
if (hasAction) return 8_000;
|
|
116
|
+
return undefined;
|
|
117
|
+
}
|
|
118
|
+
|
|
92
119
|
/** Fires a `NotificationToast` through sonner. Requires `<Toaster />` from `@olwiba/cn` mounted once in your app. */
|
|
93
120
|
export function notify(options: NotifyOptions) {
|
|
94
121
|
const { duration, ...toastProps } = options;
|
|
95
|
-
|
|
122
|
+
const resolved =
|
|
123
|
+
duration ?? defaultDuration(toastProps.variant, Boolean(toastProps.action || toastProps.secondaryAction));
|
|
124
|
+
return toast.custom((id) => <NotificationToast {...toastProps} onDismiss={() => toast.dismiss(id)} />, {
|
|
125
|
+
duration: resolved,
|
|
126
|
+
});
|
|
96
127
|
}
|
package/src/index.ts
CHANGED
|
@@ -27,6 +27,8 @@ export {
|
|
|
27
27
|
type AppShellAction,
|
|
28
28
|
type AppShellRenderLink,
|
|
29
29
|
type AppShellBreadcrumb,
|
|
30
|
+
type AppShellHeaderControls,
|
|
31
|
+
type AppShellHeaderStart,
|
|
30
32
|
type AppNavItem,
|
|
31
33
|
} from './app/AppShell';
|
|
32
34
|
|
|
@@ -145,6 +147,7 @@ export { CommandMenu, type CommandMenuProps, type CommandMenuGroup, type Command
|
|
|
145
147
|
|
|
146
148
|
// ─── Components — data ───────────────────────────────────────────────────────
|
|
147
149
|
export { DataTable, type DataTableProps, type DataTableColumn } from './components/DataTable';
|
|
150
|
+
export { LoadMore, type LoadMoreProps } from './components/LoadMore';
|
|
148
151
|
export { DataView, type DataViewProps } from './app/DataView';
|
|
149
152
|
export { Chart, type ChartProps, type ChartSeries } from './components/Chart';
|
|
150
153
|
export { FileUpload, type FileUploadProps, type FileUploadEntry } from './components/FileUpload';
|
package/src/marketing/Footer.tsx
CHANGED
|
@@ -9,7 +9,13 @@ export interface FooterProps {
|
|
|
9
9
|
navLinks?: Array<{ label: string; href: string }>;
|
|
10
10
|
socialLinks?: Array<{ label: string; href: string; icon: ReactNode }>;
|
|
11
11
|
legal?: string;
|
|
12
|
-
|
|
12
|
+
/**
|
|
13
|
+
* Live status pill. `label` is a ReactNode, not a string: a real status
|
|
14
|
+
* pill often carries per-service detail in a tooltip, and typing it as
|
|
15
|
+
* string forced consumers to smuggle that through `as unknown as string`.
|
|
16
|
+
* A plain string still works and is the common case.
|
|
17
|
+
*/
|
|
18
|
+
status?: { label: ReactNode; operational?: boolean };
|
|
13
19
|
renderLink?: AppShellRenderLink;
|
|
14
20
|
}
|
|
15
21
|
|