@olwiba/ui 0.2.4 → 0.2.6
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 +33 -2
- package/dist/index.js +213 -117
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/app/AppShell.tsx +11 -1
- 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.6",
|
|
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",
|
package/src/app/AppShell.tsx
CHANGED
|
@@ -110,6 +110,15 @@ export interface AppShellProps {
|
|
|
110
110
|
sidebarPosition?: 'viewport' | 'contained';
|
|
111
111
|
/** Which side the sidebar sits on. @default "left" */
|
|
112
112
|
side?: 'left' | 'right';
|
|
113
|
+
/**
|
|
114
|
+
* Class applied to the content wrapper around `children`, between the
|
|
115
|
+
* header and the page content. Every consuming page otherwise has to
|
|
116
|
+
* remember its own padding — some do, some don't, and the ones that don't
|
|
117
|
+
* end up flush against the sidebar/header chrome. Override to `''` for a
|
|
118
|
+
* genuinely edge-to-edge page (a full-width table, a map/canvas).
|
|
119
|
+
* @default "p-6"
|
|
120
|
+
*/
|
|
121
|
+
contentClassName?: string;
|
|
113
122
|
}
|
|
114
123
|
|
|
115
124
|
// ─── Internal sub-components ──────────────────────────────────────────────────
|
|
@@ -377,6 +386,7 @@ export function AppShell({
|
|
|
377
386
|
collapsible = 'icon',
|
|
378
387
|
sidebarPosition = 'viewport',
|
|
379
388
|
side = 'left',
|
|
389
|
+
contentClassName = 'p-6',
|
|
380
390
|
children,
|
|
381
391
|
}: AppShellProps = {}) {
|
|
382
392
|
const isContained = sidebarPosition === 'contained';
|
|
@@ -398,7 +408,7 @@ export function AppShell({
|
|
|
398
408
|
/>
|
|
399
409
|
<SidebarInset className={isContained ? undefined : 'overflow-y-auto'}>
|
|
400
410
|
<ShellHeader pageTitle={pageTitle} headerStart={headerStart} headerEnd={headerEnd} />
|
|
401
|
-
{children}
|
|
411
|
+
<div className={contentClassName}>{children}</div>
|
|
402
412
|
</SidebarInset>
|
|
403
413
|
</SidebarProvider>
|
|
404
414
|
);
|
|
@@ -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';
|