@olwiba/ui 0.2.4 → 0.2.5
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 +23 -1
- package/dist/index.js +211 -116
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/app/UpdateBanner.tsx +130 -0
- package/src/index.ts +2 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@olwiba/ui",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.5",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -64,7 +64,7 @@
|
|
|
64
64
|
"devDependencies": {
|
|
65
65
|
"@content-collections/mdx": "^0.2.2",
|
|
66
66
|
"@olwiba/cn": "0.1.26",
|
|
67
|
-
"@olwiba/docs": "0.1.
|
|
67
|
+
"@olwiba/docs": "0.1.38",
|
|
68
68
|
"@tailwindcss/vite": "^4.1.18",
|
|
69
69
|
"@tanstack/react-router": "1.154.8",
|
|
70
70
|
"@tanstack/react-router-devtools": "1.154.8",
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import * as React from 'react';
|
|
4
|
+
import { RefreshCw, Sparkles } from 'lucide-react';
|
|
5
|
+
import { Button, Card, cn } from '@olwiba/cn';
|
|
6
|
+
|
|
7
|
+
export interface UpdateBannerProps {
|
|
8
|
+
/** Version baked into the running client bundle (e.g. a git short SHA embedded at build time). */
|
|
9
|
+
currentVersion: string;
|
|
10
|
+
/**
|
|
11
|
+
* Fetches the currently deployed version from the server. The host app wires
|
|
12
|
+
* its own endpoint — the component stays app-agnostic.
|
|
13
|
+
*/
|
|
14
|
+
fetchVersion: () => Promise<{ version: string }>;
|
|
15
|
+
/** Poll interval in ms. */
|
|
16
|
+
intervalMs?: number;
|
|
17
|
+
/** Force the banner to show (demos/testing). */
|
|
18
|
+
forceShow?: boolean;
|
|
19
|
+
/** Called on refresh click; defaults to a full page reload. Use it to hook a celebration first. */
|
|
20
|
+
onRefresh?: () => void;
|
|
21
|
+
message?: React.ReactNode;
|
|
22
|
+
buttonLabel?: string;
|
|
23
|
+
className?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function UpdateBanner({
|
|
27
|
+
currentVersion,
|
|
28
|
+
fetchVersion,
|
|
29
|
+
intervalMs = 5 * 60 * 1000,
|
|
30
|
+
forceShow = false,
|
|
31
|
+
onRefresh,
|
|
32
|
+
message = (
|
|
33
|
+
<>
|
|
34
|
+
<span className="font-semibold">App update available!</span> Refresh to get the latest features
|
|
35
|
+
and improvements.
|
|
36
|
+
</>
|
|
37
|
+
),
|
|
38
|
+
buttonLabel = 'Refresh now',
|
|
39
|
+
className,
|
|
40
|
+
}: UpdateBannerProps) {
|
|
41
|
+
const [show, setShow] = React.useState(forceShow);
|
|
42
|
+
const [entered, setEntered] = React.useState(false);
|
|
43
|
+
const [refreshing, setRefreshing] = React.useState(false);
|
|
44
|
+
|
|
45
|
+
React.useEffect(() => {
|
|
46
|
+
if (forceShow) {
|
|
47
|
+
setShow(true);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
setShow(false);
|
|
51
|
+
|
|
52
|
+
let cancelled = false;
|
|
53
|
+
|
|
54
|
+
const check = async () => {
|
|
55
|
+
try {
|
|
56
|
+
const server = await fetchVersion();
|
|
57
|
+
if (!cancelled && server.version && server.version !== currentVersion) {
|
|
58
|
+
setShow(true);
|
|
59
|
+
}
|
|
60
|
+
} catch {
|
|
61
|
+
// Version checks are best-effort; stay quiet on failure.
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
void check();
|
|
66
|
+
const id = setInterval(check, intervalMs);
|
|
67
|
+
return () => {
|
|
68
|
+
cancelled = true;
|
|
69
|
+
clearInterval(id);
|
|
70
|
+
};
|
|
71
|
+
}, [currentVersion, fetchVersion, forceShow, intervalMs]);
|
|
72
|
+
|
|
73
|
+
// Two-frame mount so the enter transition actually plays.
|
|
74
|
+
React.useEffect(() => {
|
|
75
|
+
if (!show) {
|
|
76
|
+
setEntered(false);
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
const id = requestAnimationFrame(() => setEntered(true));
|
|
80
|
+
return () => cancelAnimationFrame(id);
|
|
81
|
+
}, [show]);
|
|
82
|
+
|
|
83
|
+
const handleRefresh = () => {
|
|
84
|
+
setRefreshing(true);
|
|
85
|
+
setTimeout(() => {
|
|
86
|
+
if (onRefresh) {
|
|
87
|
+
onRefresh();
|
|
88
|
+
setRefreshing(false);
|
|
89
|
+
if (forceShow) setShow(false);
|
|
90
|
+
} else {
|
|
91
|
+
window.location.reload();
|
|
92
|
+
}
|
|
93
|
+
}, 400);
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
if (!show) return null;
|
|
97
|
+
|
|
98
|
+
return (
|
|
99
|
+
<div className="pointer-events-none fixed inset-x-0 top-4 z-50 flex justify-center px-4">
|
|
100
|
+
<div
|
|
101
|
+
className={cn(
|
|
102
|
+
'pointer-events-auto transition-all duration-500 ease-out',
|
|
103
|
+
entered ? 'translate-y-0 opacity-100' : '-translate-y-24 opacity-0',
|
|
104
|
+
)}
|
|
105
|
+
>
|
|
106
|
+
<Card
|
|
107
|
+
className={cn(
|
|
108
|
+
'max-w-3xl border-amber-300/60 bg-amber-50 dark:border-amber-700/60 dark:bg-amber-950',
|
|
109
|
+
className,
|
|
110
|
+
)}
|
|
111
|
+
>
|
|
112
|
+
<div className="flex items-center justify-between gap-6 p-4">
|
|
113
|
+
<div className="flex items-center gap-3">
|
|
114
|
+
<Sparkles className="size-5 shrink-0 text-amber-600 dark:text-amber-400" aria-hidden="true" />
|
|
115
|
+
<p className="text-sm text-amber-800 dark:text-amber-200">{message}</p>
|
|
116
|
+
</div>
|
|
117
|
+
<Button
|
|
118
|
+
onClick={handleRefresh}
|
|
119
|
+
disabled={refreshing}
|
|
120
|
+
size="sm"
|
|
121
|
+
className="shrink-0 bg-amber-600 text-white hover:bg-amber-700 dark:bg-amber-600 dark:hover:bg-amber-700"
|
|
122
|
+
>
|
|
123
|
+
{refreshing ? <RefreshCw className="size-4 animate-spin" /> : buttonLabel}
|
|
124
|
+
</Button>
|
|
125
|
+
</div>
|
|
126
|
+
</Card>
|
|
127
|
+
</div>
|
|
128
|
+
</div>
|
|
129
|
+
);
|
|
130
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -76,6 +76,8 @@ export {
|
|
|
76
76
|
type UpgradeComparisonRow,
|
|
77
77
|
} from './app/UpgradePrompt';
|
|
78
78
|
|
|
79
|
+
export { UpdateBanner, type UpdateBannerProps } from './app/UpdateBanner';
|
|
80
|
+
|
|
79
81
|
// ─── Marketing — page sections ────────────────────────────────────────────────
|
|
80
82
|
export { SectionTitle, type SectionTitleProps } from './marketing/SectionTitle';
|
|
81
83
|
export { marketingSectionSpacing, type MarketingSectionSpacing } from './marketing/section-spacing';
|