@olwiba/ui 0.2.24 → 0.2.26
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/content/docs/app/app-shell.mdx +71 -0
- package/content/docs/app/auth-section.mdx +73 -0
- package/content/docs/app/billing-panel.mdx +38 -0
- package/content/docs/app/empty-state.mdx +47 -0
- package/content/docs/app/error-page.mdx +31 -0
- package/content/docs/app/index.mdx +13 -0
- package/content/docs/app/meta.json +4 -0
- package/content/docs/app/onboarding-wizard.mdx +41 -0
- package/content/docs/app/settings-section.mdx +37 -0
- package/content/docs/app/team-members-panel.mdx +35 -0
- package/content/docs/app/update-banner.mdx +43 -0
- package/content/docs/components/activity-feed.mdx +42 -0
- package/content/docs/components/chart.mdx +47 -0
- package/content/docs/components/command-menu.mdx +41 -0
- package/content/docs/components/confirm-dialog.mdx +26 -0
- package/content/docs/components/context-menu.mdx +26 -0
- package/content/docs/components/data-table.mdx +40 -0
- package/content/docs/components/dock.mdx +26 -0
- package/content/docs/components/feature-card.mdx +26 -0
- package/content/docs/components/file-upload.mdx +35 -0
- package/content/docs/components/full-page-spinner.mdx +44 -0
- package/content/docs/components/glass-card.mdx +26 -0
- package/content/docs/components/image-card.mdx +26 -0
- package/content/docs/components/index.mdx +49 -0
- package/content/docs/components/load-more.mdx +34 -0
- package/content/docs/components/meta.json +35 -0
- package/content/docs/components/notifications-popover.mdx +34 -0
- package/content/docs/components/notify.mdx +40 -0
- package/content/docs/components/olwiba-ui-context.mdx +69 -0
- package/content/docs/components/page-header.mdx +42 -0
- package/content/docs/components/pricing-card.mdx +26 -0
- package/content/docs/components/register-hotkeys.mdx +50 -0
- package/content/docs/components/root-error-fallback.mdx +39 -0
- package/content/docs/components/spotlight.mdx +26 -0
- package/content/docs/components/stat-card.mdx +26 -0
- package/content/docs/components/suspensed.mdx +42 -0
- package/content/docs/components/testimonial-card.mdx +26 -0
- package/content/docs/components/theme-color-updater.mdx +40 -0
- package/content/docs/components/theme-switch-minimal.mdx +32 -0
- package/content/docs/components/version-banner.mdx +44 -0
- package/content/docs/email/action-email.mdx +62 -0
- package/content/docs/email/email-layout.mdx +58 -0
- package/content/docs/email/email-link-fallback.mdx +46 -0
- package/content/docs/email/index.mdx +32 -0
- package/content/docs/email/meta.json +10 -0
- package/content/docs/email/notice-email.mdx +51 -0
- package/content/docs/hooks/index.mdx +21 -0
- package/content/docs/hooks/meta.json +16 -0
- package/content/docs/hooks/use-confirm.mdx +81 -0
- package/content/docs/hooks/use-controlled-open.mdx +53 -0
- package/content/docs/hooks/use-copy-to-clipboard.mdx +26 -0
- package/content/docs/hooks/use-debounce.mdx +26 -0
- package/content/docs/hooks/use-intersection-observer.mdx +26 -0
- package/content/docs/hooks/use-local-storage.mdx +26 -0
- package/content/docs/hooks/use-media-query.mdx +26 -0
- package/content/docs/hooks/use-mounted.mdx +39 -0
- package/content/docs/hooks/use-pagination.mdx +26 -0
- package/content/docs/hooks/use-scrolled-past.mdx +44 -0
- package/content/docs/index.mdx +68 -0
- package/content/docs/marketing/contact-section.mdx +16 -0
- package/content/docs/marketing/cta-section.mdx +31 -0
- package/content/docs/marketing/faq-section.mdx +16 -0
- package/content/docs/marketing/features-section.mdx +23 -0
- package/content/docs/marketing/footer.mdx +16 -0
- package/content/docs/marketing/hero-section.mdx +16 -0
- package/content/docs/marketing/index.mdx +22 -0
- package/content/docs/marketing/meta.json +20 -0
- package/content/docs/marketing/navbar.mdx +16 -0
- package/content/docs/marketing/newsletter-section.mdx +16 -0
- package/content/docs/marketing/post-list.mdx +32 -0
- package/content/docs/marketing/pricing-section.mdx +16 -0
- package/content/docs/marketing/stats-section.mdx +16 -0
- package/content/docs/marketing/steps-section.mdx +60 -0
- package/content/docs/marketing/team-section.mdx +16 -0
- package/content/docs/marketing/testimonials-section.mdx +16 -0
- package/content/docs/mechanics/carousel.mdx +42 -0
- package/content/docs/mechanics/index.mdx +11 -0
- package/content/docs/mechanics/meta.json +4 -0
- package/content/docs/mechanics/sortable.mdx +44 -0
- package/content/docs/meta.json +4 -0
- package/content/docs/motion/animated-swap.mdx +75 -0
- package/content/docs/motion/count-up.mdx +31 -0
- package/content/docs/motion/fade-in.mdx +34 -0
- package/content/docs/motion/index.mdx +12 -0
- package/content/docs/motion/meta.json +4 -0
- package/content/docs/motion/page-transition.mdx +29 -0
- package/content/docs/motion/stagger-children.mdx +31 -0
- package/content/docs/overlays/index.mdx +9 -0
- package/content/docs/overlays/meta.json +4 -0
- package/content/docs/overlays/overlay.mdx +60 -0
- package/content/docs/overlays/underlay.mdx +56 -0
- package/dist/email/index.d.ts +8 -1
- package/dist/email/index.js +3 -3
- package/dist/email/index.js.map +1 -1
- package/dist/index.d.ts +51 -3
- package/dist/index.js +40 -16
- package/dist/index.js.map +1 -1
- package/docs-manifest.json +253 -0
- package/package.json +17 -9
- package/site/demos/action-email.tsx +6 -0
- package/site/demos/activity-feed.tsx +42 -0
- package/site/demos/animated-swap.tsx +42 -0
- package/site/demos/app-shell.tsx +47 -0
- package/site/demos/app-ui-page.tsx +9 -0
- package/site/demos/application-sidebar-block.tsx +9 -0
- package/site/demos/auth-section.tsx +15 -0
- package/site/demos/auth-split-block.tsx +9 -0
- package/site/demos/billing-page.tsx +157 -0
- package/site/demos/billing-panel.tsx +26 -0
- package/site/demos/carousel.tsx +34 -0
- package/site/demos/chart.tsx +55 -0
- package/site/demos/command-menu.tsx +39 -0
- package/site/demos/confirm-dialog.tsx +27 -0
- package/site/demos/contact-form-block.tsx +3 -0
- package/site/demos/contact-section.tsx +2 -0
- package/site/demos/context-menu.tsx +30 -0
- package/site/demos/count-up.tsx +23 -0
- package/site/demos/cta-section.tsx +6 -0
- package/site/demos/dashboard-overview-block.tsx +9 -0
- package/site/demos/dashboard-shell-block.tsx +10 -0
- package/site/demos/data-table.tsx +47 -0
- package/site/demos/demo-brand.tsx +23 -0
- package/site/demos/dock.tsx +20 -0
- package/site/demos/document-sidebar-block.tsx +9 -0
- package/site/demos/email-examples.tsx +58 -0
- package/site/demos/email-frame.tsx +50 -0
- package/site/demos/email-layout.tsx +6 -0
- package/site/demos/email-link-fallback.tsx +6 -0
- package/site/demos/email-previews.generated.ts +11 -0
- package/site/demos/empty-state-block.tsx +3 -0
- package/site/demos/empty-state.tsx +23 -0
- package/site/demos/error-page.tsx +5 -0
- package/site/demos/fade-in.tsx +18 -0
- package/site/demos/faq-section.tsx +6 -0
- package/site/demos/feature-card.tsx +13 -0
- package/site/demos/features-section.tsx +6 -0
- package/site/demos/file-upload.tsx +16 -0
- package/site/demos/footer.tsx +42 -0
- package/site/demos/full-page-spinner.tsx +9 -0
- package/site/demos/glass-card.tsx +14 -0
- package/site/demos/glow-effect.tsx +3 -0
- package/site/demos/gradient-background.tsx +3 -0
- package/site/demos/grid-pattern.tsx +3 -0
- package/site/demos/hero-section.tsx +6 -0
- package/site/demos/image-card.tsx +17 -0
- package/site/demos/login-block.tsx +9 -0
- package/site/demos/marketing-cta-block.tsx +3 -0
- package/site/demos/marketing-demo-data.tsx +114 -0
- package/site/demos/marketing-faq-block.tsx +3 -0
- package/site/demos/marketing-features-block.tsx +3 -0
- package/site/demos/marketing-footer-block.tsx +3 -0
- package/site/demos/marketing-hero-block.tsx +10 -0
- package/site/demos/marketing-nav-block.tsx +3 -0
- package/site/demos/marketing-newsletter-block.tsx +3 -0
- package/site/demos/marketing-page.tsx +52 -0
- package/site/demos/marketing-pricing-block.tsx +3 -0
- package/site/demos/marketing-stats-block.tsx +3 -0
- package/site/demos/marketing-team-block.tsx +3 -0
- package/site/demos/marketing-testimonials-block.tsx +3 -0
- package/site/demos/navbar.tsx +32 -0
- package/site/demos/newsletter-section.tsx +2 -0
- package/site/demos/noise-overlay.tsx +3 -0
- package/site/demos/not-found-block.tsx +3 -0
- package/site/demos/notice-email.tsx +6 -0
- package/site/demos/notifications-popover.tsx +27 -0
- package/site/demos/notify.tsx +44 -0
- package/site/demos/olwiba-ui-context.tsx +51 -0
- package/site/demos/onboarding-block.tsx +3 -0
- package/site/demos/onboarding-wizard.tsx +43 -0
- package/site/demos/overlay.tsx +72 -0
- package/site/demos/page-header.tsx +15 -0
- package/site/demos/page-transition.tsx +34 -0
- package/site/demos/post-list.tsx +24 -0
- package/site/demos/pricing-card.tsx +27 -0
- package/site/demos/pricing-section.tsx +6 -0
- package/site/demos/register-hotkeys.tsx +39 -0
- package/site/demos/root-error-fallback.tsx +28 -0
- package/site/demos/settings-layout-block.tsx +3 -0
- package/site/demos/settings-section.tsx +22 -0
- package/site/demos/sign-in-page.tsx +10 -0
- package/site/demos/sortable.tsx +30 -0
- package/site/demos/spotlight.tsx +29 -0
- package/site/demos/stagger-children.tsx +16 -0
- package/site/demos/stat-card.tsx +12 -0
- package/site/demos/stats-section.tsx +6 -0
- package/site/demos/steps-section.tsx +66 -0
- package/site/demos/suspensed.tsx +31 -0
- package/site/demos/team-members-panel.tsx +26 -0
- package/site/demos/team-section.tsx +2 -0
- package/site/demos/testimonial-card.tsx +17 -0
- package/site/demos/testimonials-section.tsx +6 -0
- package/site/demos/theme-color-updater.tsx +32 -0
- package/site/demos/theme-switch-minimal.tsx +10 -0
- package/site/demos/underlay.tsx +74 -0
- package/site/demos/update-banner.tsx +31 -0
- package/site/demos/use-copy-to-clipboard.tsx +21 -0
- package/site/demos/use-debounce.tsx +25 -0
- package/site/demos/use-intersection-observer.tsx +32 -0
- package/site/demos/use-local-storage.tsx +22 -0
- package/site/demos/use-media-query.tsx +31 -0
- package/site/demos/use-pagination.tsx +31 -0
- package/site/demos/user-profile-block.tsx +3 -0
- package/site/demos/version-banner.tsx +27 -0
- package/src/components/Chart.tsx +38 -7
- package/src/components/DataTable.tsx +56 -8
- package/src/components/Notify.tsx +32 -1
- package/src/email/EmailLinkFallback.tsx +7 -1
- package/src/email/index.ts +3 -0
- package/src/marketing/Footer.tsx +7 -1
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import * as React from 'react';
|
|
4
|
+
import { Underlay, type UnderlayVariant } from '@olwiba/ui';
|
|
5
|
+
import { DemoControls, useUsageCode } from '~/components/ComponentPreview';
|
|
6
|
+
import { Button, Badge } from '@olwiba/cn';
|
|
7
|
+
|
|
8
|
+
const variants: UnderlayVariant[] = ['dots', 'lines', 'cross', 'glow'];
|
|
9
|
+
|
|
10
|
+
function MockContent() {
|
|
11
|
+
return (
|
|
12
|
+
<div className="relative z-10 flex w-full flex-col gap-4 p-8">
|
|
13
|
+
<div className="flex items-center justify-between">
|
|
14
|
+
<div>
|
|
15
|
+
<p className="text-xs text-muted-foreground">Total revenue</p>
|
|
16
|
+
<p className="text-2xl font-semibold">$48,295</p>
|
|
17
|
+
</div>
|
|
18
|
+
<Badge variant="secondary">+12.4%</Badge>
|
|
19
|
+
</div>
|
|
20
|
+
<div className="flex gap-3">
|
|
21
|
+
{['Mon', 'Tue', 'Wed', 'Thu', 'Fri'].map((d, i) => (
|
|
22
|
+
<div key={d} className="flex flex-1 flex-col items-center gap-1">
|
|
23
|
+
<div
|
|
24
|
+
className="w-full rounded bg-primary/20"
|
|
25
|
+
style={{ height: `${[40, 65, 50, 80, 55][i]}px` }}
|
|
26
|
+
/>
|
|
27
|
+
<span className="text-[10px] text-muted-foreground">{d}</span>
|
|
28
|
+
</div>
|
|
29
|
+
))}
|
|
30
|
+
</div>
|
|
31
|
+
</div>
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export default function UnderlayDemo() {
|
|
36
|
+
const [variant, setVariant] = React.useState<UnderlayVariant>('dots');
|
|
37
|
+
|
|
38
|
+
const extraProps = variant === 'glow' ? ` size={320} followCursor blur="xl"` : '';
|
|
39
|
+
|
|
40
|
+
useUsageCode(
|
|
41
|
+
`<div className="relative overflow-hidden rounded-xl">\n <Underlay variant="${variant}"${extraProps} />\n <div className="relative z-10 p-8">Content</div>\n</div>`,
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<>
|
|
46
|
+
<div className="relative w-full overflow-hidden bg-card">
|
|
47
|
+
<Underlay
|
|
48
|
+
key={variant}
|
|
49
|
+
variant={variant}
|
|
50
|
+
{...(variant === 'glow' ? { size: 360, followCursor: true, blur: 'xl' as const } : {})}
|
|
51
|
+
/>
|
|
52
|
+
<MockContent />
|
|
53
|
+
</div>
|
|
54
|
+
|
|
55
|
+
<DemoControls>
|
|
56
|
+
<div className="space-y-1.5">
|
|
57
|
+
<span className="text-xs font-medium text-muted-foreground">Variant</span>
|
|
58
|
+
<div className="flex flex-wrap gap-1.5">
|
|
59
|
+
{variants.map((v) => (
|
|
60
|
+
<Button
|
|
61
|
+
key={v}
|
|
62
|
+
variant={variant === v ? 'default' : 'secondary'}
|
|
63
|
+
size="sm"
|
|
64
|
+
onClick={() => setVariant(v)}
|
|
65
|
+
>
|
|
66
|
+
{v}
|
|
67
|
+
</Button>
|
|
68
|
+
))}
|
|
69
|
+
</div>
|
|
70
|
+
</div>
|
|
71
|
+
</DemoControls>
|
|
72
|
+
</>
|
|
73
|
+
);
|
|
74
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { UpdateBanner } from '@olwiba/ui';
|
|
2
|
+
import * as React from 'react';
|
|
3
|
+
|
|
4
|
+
export default function UpdateBannerDemo() {
|
|
5
|
+
const [show, setShow] = React.useState(true);
|
|
6
|
+
|
|
7
|
+
return (
|
|
8
|
+
<div className="relative min-h-[260px] overflow-hidden rounded-2xl border bg-background p-6">
|
|
9
|
+
<p className="text-sm text-muted-foreground">
|
|
10
|
+
The banner polls a version endpoint and slides in when the deployed version no longer matches
|
|
11
|
+
the running bundle. This demo forces it visible.
|
|
12
|
+
</p>
|
|
13
|
+
<button
|
|
14
|
+
className="mt-4 rounded-md border px-3 py-1.5 text-sm"
|
|
15
|
+
onClick={() => setShow((value) => !value)}
|
|
16
|
+
>
|
|
17
|
+
Toggle banner
|
|
18
|
+
</button>
|
|
19
|
+
|
|
20
|
+
{/* Demo container overrides the fixed positioning context */}
|
|
21
|
+
<div className="absolute inset-0 [&>div]:absolute [&>div]:top-4">
|
|
22
|
+
<UpdateBanner
|
|
23
|
+
forceShow={show}
|
|
24
|
+
currentVersion="dev-local"
|
|
25
|
+
fetchVersion={async () => ({ version: 'dev-local' })}
|
|
26
|
+
onRefresh={() => setShow(false)}
|
|
27
|
+
/>
|
|
28
|
+
</div>
|
|
29
|
+
</div>
|
|
30
|
+
);
|
|
31
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { useCopyToClipboard } from '@olwiba/ui';
|
|
3
|
+
import { Button } from '@olwiba/cn';
|
|
4
|
+
import { Check, Copy } from 'lucide-react';
|
|
5
|
+
|
|
6
|
+
const snippet = 'bun add @olwiba/ui @olwiba/cn';
|
|
7
|
+
|
|
8
|
+
export default function Demo() {
|
|
9
|
+
const [copied, copy] = useCopyToClipboard();
|
|
10
|
+
|
|
11
|
+
return (
|
|
12
|
+
<div className="flex items-center justify-center p-8">
|
|
13
|
+
<div className="flex items-center gap-2 rounded-xl border bg-muted px-4 py-2">
|
|
14
|
+
<code className="font-mono text-sm">{snippet}</code>
|
|
15
|
+
<Button size="icon" variant="ghost" className="size-7 shrink-0" onClick={() => copy(snippet)}>
|
|
16
|
+
{copied ? <Check className="size-3.5 text-emerald-500" /> : <Copy className="size-3.5" />}
|
|
17
|
+
</Button>
|
|
18
|
+
</div>
|
|
19
|
+
</div>
|
|
20
|
+
);
|
|
21
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import * as React from 'react';
|
|
3
|
+
import { useDebounce } from '@olwiba/ui';
|
|
4
|
+
import { Input } from '@olwiba/cn';
|
|
5
|
+
|
|
6
|
+
export default function Demo() {
|
|
7
|
+
const [value, setValue] = React.useState('');
|
|
8
|
+
const debounced = useDebounce(value, 400);
|
|
9
|
+
|
|
10
|
+
return (
|
|
11
|
+
<div className="mx-auto flex max-w-sm flex-col gap-4 p-8">
|
|
12
|
+
<Input placeholder="Type something..." value={value} onChange={(e) => setValue(e.target.value)} />
|
|
13
|
+
<div className="space-y-1 rounded-xl border bg-card p-4 text-sm">
|
|
14
|
+
<div className="flex justify-between gap-4">
|
|
15
|
+
<span className="text-muted-foreground">Live</span>
|
|
16
|
+
<span className="font-mono">{value || '—'}</span>
|
|
17
|
+
</div>
|
|
18
|
+
<div className="flex justify-between gap-4">
|
|
19
|
+
<span className="text-muted-foreground">Debounced (400ms)</span>
|
|
20
|
+
<span className="font-mono">{debounced || '—'}</span>
|
|
21
|
+
</div>
|
|
22
|
+
</div>
|
|
23
|
+
</div>
|
|
24
|
+
);
|
|
25
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import * as React from 'react';
|
|
3
|
+
import { useIntersectionObserver } from '@olwiba/ui';
|
|
4
|
+
import { cn } from '@olwiba/cn';
|
|
5
|
+
|
|
6
|
+
export default function Demo() {
|
|
7
|
+
const [ref, isVisible] = useIntersectionObserver({ threshold: 0.5 });
|
|
8
|
+
|
|
9
|
+
return (
|
|
10
|
+
<div className="space-y-3 p-6">
|
|
11
|
+
<div className="text-sm text-center">
|
|
12
|
+
Status:{' '}
|
|
13
|
+
<strong className={isVisible ? 'text-emerald-500' : 'text-muted-foreground'}>
|
|
14
|
+
{isVisible ? 'intersecting' : 'not intersecting'}
|
|
15
|
+
</strong>
|
|
16
|
+
</div>
|
|
17
|
+
<div className="h-44 overflow-y-auto rounded-xl border">
|
|
18
|
+
<div className="flex h-16 items-center justify-center text-xs text-muted-foreground">↓ scroll</div>
|
|
19
|
+
<div
|
|
20
|
+
ref={ref as unknown as React.RefObject<HTMLDivElement>}
|
|
21
|
+
className={cn(
|
|
22
|
+
'mx-4 rounded-xl border p-4 text-sm text-center transition-all duration-500',
|
|
23
|
+
isVisible ? 'border-primary bg-primary/5 text-primary' : 'bg-muted text-muted-foreground',
|
|
24
|
+
)}
|
|
25
|
+
>
|
|
26
|
+
Observed element
|
|
27
|
+
</div>
|
|
28
|
+
<div className="flex h-16 items-center justify-center text-xs text-muted-foreground">↑ scroll</div>
|
|
29
|
+
</div>
|
|
30
|
+
</div>
|
|
31
|
+
);
|
|
32
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { useLocalStorage } from '@olwiba/ui';
|
|
3
|
+
import { Button } from '@olwiba/cn';
|
|
4
|
+
|
|
5
|
+
export default function Demo() {
|
|
6
|
+
const [count, setCount] = useLocalStorage('demo-count', 0);
|
|
7
|
+
|
|
8
|
+
return (
|
|
9
|
+
<div className="flex flex-col items-center gap-4 p-8">
|
|
10
|
+
<div className="w-40 rounded-2xl border bg-card p-6 text-center">
|
|
11
|
+
<div className="text-4xl font-bold tabular-nums">{count}</div>
|
|
12
|
+
<div className="mt-1 text-xs text-muted-foreground">persisted in localStorage</div>
|
|
13
|
+
</div>
|
|
14
|
+
<div className="flex gap-2">
|
|
15
|
+
<Button variant="outline" size="sm" onClick={() => setCount(count - 1)}>−</Button>
|
|
16
|
+
<Button variant="outline" size="sm" onClick={() => setCount(0)}>Reset</Button>
|
|
17
|
+
<Button size="sm" onClick={() => setCount(count + 1)}>+</Button>
|
|
18
|
+
</div>
|
|
19
|
+
<p className="text-xs text-muted-foreground">Value persists across page reloads.</p>
|
|
20
|
+
</div>
|
|
21
|
+
);
|
|
22
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { useMediaQuery } from '@olwiba/ui';
|
|
3
|
+
import { cn } from '@olwiba/cn';
|
|
4
|
+
|
|
5
|
+
const breakpoints = [
|
|
6
|
+
{ label: 'sm', query: '(min-width: 640px)' },
|
|
7
|
+
{ label: 'md', query: '(min-width: 768px)' },
|
|
8
|
+
{ label: 'lg', query: '(min-width: 1024px)' },
|
|
9
|
+
{ label: 'xl', query: '(min-width: 1280px)' },
|
|
10
|
+
{ label: 'dark', query: '(prefers-color-scheme: dark)' },
|
|
11
|
+
];
|
|
12
|
+
|
|
13
|
+
function Row({ label, query }: { label: string; query: string }) {
|
|
14
|
+
const matches = useMediaQuery(query);
|
|
15
|
+
return (
|
|
16
|
+
<div className={cn('flex items-center justify-between rounded-xl border px-4 py-2.5 text-sm transition-colors', matches ? 'border-primary bg-primary/5' : 'bg-card')}>
|
|
17
|
+
<code className="font-mono text-xs">{query}</code>
|
|
18
|
+
<span className={cn('text-xs font-medium', matches ? 'text-primary' : 'text-muted-foreground')}>
|
|
19
|
+
{matches ? 'matches' : 'no match'}
|
|
20
|
+
</span>
|
|
21
|
+
</div>
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export default function Demo() {
|
|
26
|
+
return (
|
|
27
|
+
<div className="flex flex-col gap-2 p-6">
|
|
28
|
+
{breakpoints.map((bp) => <Row key={bp.label} {...bp} />)}
|
|
29
|
+
</div>
|
|
30
|
+
);
|
|
31
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { usePagination } from '@olwiba/ui';
|
|
3
|
+
import { Button } from '@olwiba/cn';
|
|
4
|
+
|
|
5
|
+
const ALL_ITEMS = Array.from({ length: 23 }, (_, i) => `Item ${i + 1}`);
|
|
6
|
+
|
|
7
|
+
export default function Demo() {
|
|
8
|
+
const { page, totalPages, offset, pageSize, hasPrev, hasNext, prev, next, goTo } = usePagination(ALL_ITEMS.length, 5);
|
|
9
|
+
const pageItems = ALL_ITEMS.slice(offset, offset + pageSize);
|
|
10
|
+
|
|
11
|
+
return (
|
|
12
|
+
<div className="flex flex-col gap-4 p-6">
|
|
13
|
+
<div className="space-y-1.5">
|
|
14
|
+
{pageItems.map((item) => (
|
|
15
|
+
<div key={item} className="rounded-xl border bg-card px-4 py-2.5 text-sm">{item}</div>
|
|
16
|
+
))}
|
|
17
|
+
</div>
|
|
18
|
+
<div className="flex items-center justify-between gap-2">
|
|
19
|
+
<Button variant="outline" size="sm" disabled={!hasPrev} onClick={prev}>Previous</Button>
|
|
20
|
+
<div className="flex gap-1">
|
|
21
|
+
{Array.from({ length: totalPages }, (_, i) => (
|
|
22
|
+
<Button key={i} size="sm" variant={page === i + 1 ? 'default' : 'outline'} className="size-8 p-0" onClick={() => goTo(i + 1)}>
|
|
23
|
+
{i + 1}
|
|
24
|
+
</Button>
|
|
25
|
+
))}
|
|
26
|
+
</div>
|
|
27
|
+
<Button variant="outline" size="sm" disabled={!hasNext} onClick={next}>Next</Button>
|
|
28
|
+
</div>
|
|
29
|
+
</div>
|
|
30
|
+
);
|
|
31
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { VersionBanner } from '@olwiba/ui';
|
|
2
|
+
import * as React from 'react';
|
|
3
|
+
|
|
4
|
+
export default function VersionBannerDemo() {
|
|
5
|
+
const [show, setShow] = React.useState(true);
|
|
6
|
+
|
|
7
|
+
return (
|
|
8
|
+
<div className="w-full max-w-lg rounded-lg border overflow-hidden">
|
|
9
|
+
{show ? (
|
|
10
|
+
<VersionBanner
|
|
11
|
+
version="2.1.0"
|
|
12
|
+
message="New components available — see what's changed."
|
|
13
|
+
onDismiss={() => setShow(false)}
|
|
14
|
+
/>
|
|
15
|
+
) : (
|
|
16
|
+
<div className="flex items-center justify-center py-4">
|
|
17
|
+
<button
|
|
18
|
+
onClick={() => setShow(true)}
|
|
19
|
+
className="text-sm text-muted-foreground underline"
|
|
20
|
+
>
|
|
21
|
+
Reset demo
|
|
22
|
+
</button>
|
|
23
|
+
</div>
|
|
24
|
+
)}
|
|
25
|
+
</div>
|
|
26
|
+
);
|
|
27
|
+
}
|
package/src/components/Chart.tsx
CHANGED
|
@@ -42,6 +42,27 @@ export interface ChartProps {
|
|
|
42
42
|
grid?: boolean;
|
|
43
43
|
/** Legend. @default true for multiple series or donut, false for one series */
|
|
44
44
|
legend?: boolean;
|
|
45
|
+
/**
|
|
46
|
+
* Stack series instead of overlaying them. Area and bar only — a stacked
|
|
47
|
+
* line is a shape people misread as absolute values.
|
|
48
|
+
*
|
|
49
|
+
* Off by default: stacking answers "what does the total split into", and
|
|
50
|
+
* silently switching a two-series comparison to it would change what an
|
|
51
|
+
* existing chart claims.
|
|
52
|
+
*/
|
|
53
|
+
stacked?: boolean;
|
|
54
|
+
/**
|
|
55
|
+
* Formats x-axis ticks. Separate from `valueFormatter`, which formats the
|
|
56
|
+
* measured value — an axis of ISO dates wants shortening without changing
|
|
57
|
+
* what the tooltip reports.
|
|
58
|
+
*/
|
|
59
|
+
xTickFormatter?: (value: string) => string;
|
|
60
|
+
/**
|
|
61
|
+
* Fixes the y-axis range. Without it recharts scales to the data, which for
|
|
62
|
+
* a percentage series makes a 98-100%% band fill the plot and read as
|
|
63
|
+
* volatility.
|
|
64
|
+
*/
|
|
65
|
+
yDomain?: [number, number];
|
|
45
66
|
/** Formats values in tooltips and the y-axis, e.g. `(v) => \`$${v}\``. */
|
|
46
67
|
valueFormatter?: (value: number) => string;
|
|
47
68
|
className?: string;
|
|
@@ -121,6 +142,9 @@ export function Chart({
|
|
|
121
142
|
height = 300,
|
|
122
143
|
grid = true,
|
|
123
144
|
legend,
|
|
145
|
+
stacked = false,
|
|
146
|
+
xTickFormatter,
|
|
147
|
+
yDomain,
|
|
124
148
|
valueFormatter = (v) => String(v),
|
|
125
149
|
className,
|
|
126
150
|
}: ChartProps) {
|
|
@@ -170,11 +194,12 @@ export function Chart({
|
|
|
170
194
|
chart = (
|
|
171
195
|
<BarChart data={data} barCategoryGap="25%">
|
|
172
196
|
{gridEl}
|
|
173
|
-
<XAxis dataKey={xKey} {...axisProps} />
|
|
174
|
-
<YAxis {...axisProps} width={48} tickFormatter={valueFormatter} />
|
|
197
|
+
<XAxis dataKey={xKey} {...axisProps} tickFormatter={xTickFormatter} />
|
|
198
|
+
<YAxis {...axisProps} width={48} domain={yDomain} tickFormatter={valueFormatter} />
|
|
175
199
|
{tooltip}
|
|
176
200
|
{series.map((s, i) => (
|
|
177
201
|
<Bar
|
|
202
|
+
stackId={stacked ? 'stack' : undefined}
|
|
178
203
|
key={s.key}
|
|
179
204
|
dataKey={s.key}
|
|
180
205
|
name={s.label ?? s.key}
|
|
@@ -189,8 +214,8 @@ export function Chart({
|
|
|
189
214
|
chart = (
|
|
190
215
|
<AreaChart data={data}>
|
|
191
216
|
{gridEl}
|
|
192
|
-
<XAxis dataKey={xKey} {...axisProps} />
|
|
193
|
-
<YAxis {...axisProps} width={48} tickFormatter={valueFormatter} />
|
|
217
|
+
<XAxis dataKey={xKey} {...axisProps} tickFormatter={xTickFormatter} />
|
|
218
|
+
<YAxis {...axisProps} width={48} domain={yDomain} tickFormatter={valueFormatter} />
|
|
194
219
|
{tooltip}
|
|
195
220
|
{series.map((s, i) => (
|
|
196
221
|
<Area
|
|
@@ -198,10 +223,16 @@ export function Chart({
|
|
|
198
223
|
type="monotone"
|
|
199
224
|
dataKey={s.key}
|
|
200
225
|
name={s.label ?? s.key}
|
|
226
|
+
// One shared id is what makes recharts stack rather than
|
|
227
|
+
// overlay; undefined leaves the existing behaviour untouched.
|
|
228
|
+
stackId={stacked ? 'stack' : undefined}
|
|
201
229
|
stroke={seriesColor(s, i)}
|
|
202
230
|
strokeWidth={2}
|
|
203
231
|
fill={seriesColor(s, i)}
|
|
204
|
-
|
|
232
|
+
// Overlaid areas must stay translucent so the ones behind show
|
|
233
|
+
// through. Stacked bands never overlap, and at 0.12 they are too
|
|
234
|
+
// faint to tell apart.
|
|
235
|
+
fillOpacity={stacked ? 0.35 : 0.12}
|
|
205
236
|
dot={false}
|
|
206
237
|
activeDot={{ r: 4 }}
|
|
207
238
|
/>
|
|
@@ -212,8 +243,8 @@ export function Chart({
|
|
|
212
243
|
chart = (
|
|
213
244
|
<LineChart data={data}>
|
|
214
245
|
{gridEl}
|
|
215
|
-
<XAxis dataKey={xKey} {...axisProps} />
|
|
216
|
-
<YAxis {...axisProps} width={48} tickFormatter={valueFormatter} />
|
|
246
|
+
<XAxis dataKey={xKey} {...axisProps} tickFormatter={xTickFormatter} />
|
|
247
|
+
<YAxis {...axisProps} width={48} domain={yDomain} tickFormatter={valueFormatter} />
|
|
217
248
|
{tooltip}
|
|
218
249
|
{series.map((s, i) => (
|
|
219
250
|
<Line
|
|
@@ -26,6 +26,23 @@ export interface DataTableProps<TData> {
|
|
|
26
26
|
onSelectionChange?: (rows: TData[]) => void;
|
|
27
27
|
/** Rows per page. Set to `0` to disable pagination entirely. @default 10 */
|
|
28
28
|
pageSize?: number;
|
|
29
|
+
/**
|
|
30
|
+
* Hands pagination to the caller, for data the client does not hold all of.
|
|
31
|
+
*
|
|
32
|
+
* With this set, `data` is treated as the current page rather than the whole
|
|
33
|
+
* set: the table stops slicing, and the footer reports and drives the
|
|
34
|
+
* caller's page instead of its own. Without it nothing changes — a table
|
|
35
|
+
* given every row keeps paginating locally, which is right for the common
|
|
36
|
+
* case and avoids making every consumer implement paging to get a table.
|
|
37
|
+
*/
|
|
38
|
+
pagination?: {
|
|
39
|
+
pageIndex: number;
|
|
40
|
+
/** Total pages. Omit when unknown — the footer then relies on `hasNext`. */
|
|
41
|
+
pageCount?: number;
|
|
42
|
+
hasNext?: boolean;
|
|
43
|
+
hasPrevious?: boolean;
|
|
44
|
+
onPageChange: (pageIndex: number) => void;
|
|
45
|
+
};
|
|
29
46
|
/** Slot rendered top-right of the toolbar — e.g. an "Add" button. */
|
|
30
47
|
toolbar?: React.ReactNode;
|
|
31
48
|
onRowClick?: (row: TData) => void;
|
|
@@ -66,6 +83,7 @@ export function DataTable<TData>({
|
|
|
66
83
|
selectable = false,
|
|
67
84
|
onSelectionChange,
|
|
68
85
|
pageSize = 10,
|
|
86
|
+
pagination,
|
|
69
87
|
toolbar,
|
|
70
88
|
onRowClick,
|
|
71
89
|
emptyMessage = 'No results.',
|
|
@@ -94,8 +112,13 @@ export function DataTable<TData>({
|
|
|
94
112
|
},
|
|
95
113
|
getCoreRowModel: getCoreRowModel(),
|
|
96
114
|
getSortedRowModel: getSortedRowModel(),
|
|
97
|
-
|
|
98
|
-
|
|
115
|
+
// Server-paginated tables must not slice: `data` is already the page, and
|
|
116
|
+
// running the client model over it would paginate a single page again.
|
|
117
|
+
...(pageSize > 0 && !pagination
|
|
118
|
+
? { getPaginationRowModel: getPaginationRowModel() }
|
|
119
|
+
: {}),
|
|
120
|
+
...(pagination ? { manualPagination: true } : {}),
|
|
121
|
+
initialState: pageSize > 0 && !pagination ? { pagination: { pageSize } } : undefined,
|
|
99
122
|
enableRowSelection: selectable,
|
|
100
123
|
});
|
|
101
124
|
|
|
@@ -107,6 +130,31 @@ export function DataTable<TData>({
|
|
|
107
130
|
|
|
108
131
|
const rows = table.getRowModel().rows;
|
|
109
132
|
|
|
133
|
+
// One set of derivations so the footer does not branch on `pagination`
|
|
134
|
+
// three separate times and drift between them.
|
|
135
|
+
const canPrevious = pagination
|
|
136
|
+
? (pagination.hasPrevious ?? pagination.pageIndex > 0)
|
|
137
|
+
: table.getCanPreviousPage();
|
|
138
|
+
const canNext = pagination
|
|
139
|
+
? (pagination.hasNext ??
|
|
140
|
+
(pagination.pageCount !== undefined && pagination.pageIndex < pagination.pageCount - 1))
|
|
141
|
+
: table.getCanNextPage();
|
|
142
|
+
const goPrevious = () =>
|
|
143
|
+
pagination ? pagination.onPageChange(pagination.pageIndex - 1) : table.previousPage();
|
|
144
|
+
const goNext = () =>
|
|
145
|
+
pagination ? pagination.onPageChange(pagination.pageIndex + 1) : table.nextPage();
|
|
146
|
+
// A server-paginated table shows its pager whenever paging is possible.
|
|
147
|
+
// Total pages are often unknown, so "more than one page" is not a question
|
|
148
|
+
// it can answer up front the way the local model can.
|
|
149
|
+
const showPager = pagination
|
|
150
|
+
? canPrevious || canNext
|
|
151
|
+
: pageSize > 0 && table.getPageCount() > 1;
|
|
152
|
+
const pageLabel = pagination
|
|
153
|
+
? pagination.pageCount !== undefined
|
|
154
|
+
? `Page ${pagination.pageIndex + 1} of ${pagination.pageCount}`
|
|
155
|
+
: `Page ${pagination.pageIndex + 1}`
|
|
156
|
+
: `Page ${table.getState().pagination.pageIndex + 1} of ${table.getPageCount()}`;
|
|
157
|
+
|
|
110
158
|
return (
|
|
111
159
|
<div className={cn('space-y-4', className)}>
|
|
112
160
|
{(searchKey || toolbar) && (
|
|
@@ -186,19 +234,19 @@ export function DataTable<TData>({
|
|
|
186
234
|
</Table>
|
|
187
235
|
</div>
|
|
188
236
|
|
|
189
|
-
{(selectable ||
|
|
237
|
+
{(selectable || showPager) && (
|
|
190
238
|
<div className="flex items-center justify-between">
|
|
191
239
|
<p className="text-sm text-muted-foreground">
|
|
192
240
|
{selectable && `${table.getFilteredSelectedRowModel().rows.length} of ${table.getFilteredRowModel().rows.length} selected`}
|
|
193
|
-
{selectable &&
|
|
194
|
-
{
|
|
241
|
+
{selectable && showPager && ' · '}
|
|
242
|
+
{showPager && pageLabel}
|
|
195
243
|
</p>
|
|
196
|
-
{
|
|
244
|
+
{showPager && (
|
|
197
245
|
<div className="flex gap-2">
|
|
198
|
-
<Button variant="outline" size="sm" onClick={
|
|
246
|
+
<Button variant="outline" size="sm" onClick={goPrevious} disabled={!canPrevious}>
|
|
199
247
|
<ChevronLeft className="size-4" /> Previous
|
|
200
248
|
</Button>
|
|
201
|
-
<Button variant="outline" size="sm" onClick={
|
|
249
|
+
<Button variant="outline" size="sm" onClick={goNext} disabled={!canNext}>
|
|
202
250
|
Next <ChevronRight className="size-4" />
|
|
203
251
|
</Button>
|
|
204
252
|
</div>
|
|
@@ -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
|
}
|
|
@@ -17,7 +17,13 @@ export function EmailLinkFallback({
|
|
|
17
17
|
{label}
|
|
18
18
|
</EmailText>
|
|
19
19
|
<EmailText style={{ margin: 0 }}>
|
|
20
|
-
|
|
20
|
+
{/*
|
|
21
|
+
Set through `style` rather than CN's `brandColor` prop, which this
|
|
22
|
+
package's pinned @olwiba/cn does not have yet. CN applies that prop as
|
|
23
|
+
`color` anyway, and `style` wins over its default, so the result is
|
|
24
|
+
identical without coupling this file to a release.
|
|
25
|
+
*/}
|
|
26
|
+
<EmailLink href={actionUrl} style={brandColor ? { color: brandColor } : undefined}>
|
|
21
27
|
{actionUrl}
|
|
22
28
|
</EmailLink>
|
|
23
29
|
</EmailText>
|
package/src/email/index.ts
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
1
|
export { ActionEmail, type ActionEmailProps } from './ActionEmail';
|
|
2
2
|
export { NoticeEmail, type NoticeEmailProps } from './NoticeEmail';
|
|
3
3
|
export { EmailLayout, type EmailLayoutProps } from './EmailLayout';
|
|
4
|
+
// Used internally by ActionEmail and useful on its own: any email with a button
|
|
5
|
+
// wants a copyable link for the clients that strip them.
|
|
6
|
+
export { EmailLinkFallback, type EmailLinkFallbackProps } from './EmailLinkFallback';
|
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
|
|