@olwiba/ui 0.0.28
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/LICENSE +21 -0
- package/README.md +98 -0
- package/dist/index.d.ts +762 -0
- package/dist/index.js +2102 -0
- package/dist/index.js.map +1 -0
- package/package.json +88 -0
- package/src/app/AppShell.tsx +365 -0
- package/src/app/AuthSection.tsx +141 -0
- package/src/app/EmptyState.tsx +34 -0
- package/src/app/ErrorPage.tsx +78 -0
- package/src/app/UpgradePrompt.tsx +203 -0
- package/src/blog/PostCard.tsx +66 -0
- package/src/blog/PostList.tsx +27 -0
- package/src/components/ConfirmDialog.tsx +43 -0
- package/src/components/ContextMenu.tsx +86 -0
- package/src/components/DevBanner.tsx +22 -0
- package/src/components/Dock.tsx +94 -0
- package/src/components/FeatureCard.tsx +45 -0
- package/src/components/GlassCard.tsx +30 -0
- package/src/components/ImageCard.tsx +60 -0
- package/src/components/PageHeader.tsx +101 -0
- package/src/components/PricingCard.tsx +90 -0
- package/src/components/RegisterHotkeys.tsx +43 -0
- package/src/components/RootErrorFallback.tsx +29 -0
- package/src/components/Spinner.tsx +14 -0
- package/src/components/Spotlight.tsx +104 -0
- package/src/components/StatCard.tsx +44 -0
- package/src/components/Suspensed.tsx +17 -0
- package/src/components/TestimonialCard.tsx +64 -0
- package/src/components/ThemeColorUpdater.tsx +23 -0
- package/src/components/ThemeSwitchMinimal.tsx +28 -0
- package/src/components/VersionBanner.tsx +38 -0
- package/src/context/OlwibaUIContext.tsx +58 -0
- package/src/hooks/use-confirm.ts +64 -0
- package/src/hooks/use-controlled-open.ts +33 -0
- package/src/hooks/use-copy-to-clipboard.ts +20 -0
- package/src/hooks/use-debounce.ts +14 -0
- package/src/hooks/use-intersection-observer.ts +25 -0
- package/src/hooks/use-local-storage.ts +31 -0
- package/src/hooks/use-media-query.ts +21 -0
- package/src/hooks/use-mounted.ts +11 -0
- package/src/hooks/use-pagination.ts +31 -0
- package/src/hooks/use-scrolled-past.ts +27 -0
- package/src/index.ts +113 -0
- package/src/lib/utils.ts +6 -0
- package/src/marketing/ContactSection.tsx +110 -0
- package/src/marketing/CtaSection.tsx +74 -0
- package/src/marketing/FaqSection.tsx +44 -0
- package/src/marketing/FeaturesSection.tsx +42 -0
- package/src/marketing/Footer.tsx +87 -0
- package/src/marketing/HeroSection.tsx +112 -0
- package/src/marketing/LogoStrip.tsx +94 -0
- package/src/marketing/Navbar.tsx +142 -0
- package/src/marketing/NewsletterSection.tsx +64 -0
- package/src/marketing/PricingSection.tsx +120 -0
- package/src/marketing/SectionTitle.tsx +30 -0
- package/src/marketing/StatsSection.tsx +63 -0
- package/src/marketing/TeamSection.tsx +110 -0
- package/src/marketing/TestimonialsSection.tsx +56 -0
- package/src/motion/CountUp.tsx +64 -0
- package/src/motion/FadeIn.tsx +69 -0
- package/src/motion/PageTransition.tsx +49 -0
- package/src/motion/StaggerChildren.tsx +74 -0
- package/src/overlays/Overlay.tsx +88 -0
- package/src/overlays/Underlay.tsx +114 -0
- package/src/primitives/Badge.tsx +11 -0
- package/src/primitives/Button.tsx +16 -0
- package/src/primitives/Card.tsx +22 -0
- package/src/primitives/Checkbox.tsx +17 -0
- package/src/primitives/Input.tsx +16 -0
- package/src/primitives/Switch.tsx +16 -0
- package/src/primitives/Textarea.tsx +16 -0
- package/src/primitives/index.ts +7 -0
- package/src/types/external-packages.d.ts +79 -0
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import * as React from 'react';
|
|
4
|
+
import { X } from 'lucide-react';
|
|
5
|
+
import { cn } from '../lib/utils';
|
|
6
|
+
|
|
7
|
+
interface VersionBannerProps {
|
|
8
|
+
version: string;
|
|
9
|
+
message?: string;
|
|
10
|
+
className?: string;
|
|
11
|
+
onDismiss?: () => void;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function VersionBanner({ version, message, className, onDismiss }: VersionBannerProps) {
|
|
15
|
+
const [dismissed, setDismissed] = React.useState(false);
|
|
16
|
+
|
|
17
|
+
if (dismissed) return null;
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<div className={cn('flex items-center justify-between gap-4 bg-primary/10 px-4 py-2 text-sm', className)}>
|
|
21
|
+
<div className="flex items-center gap-2">
|
|
22
|
+
<span className="rounded bg-primary/20 px-1.5 py-0.5 font-mono text-xs font-semibold text-primary">
|
|
23
|
+
v{version}
|
|
24
|
+
</span>
|
|
25
|
+
{message && <span className="text-muted-foreground">{message}</span>}
|
|
26
|
+
</div>
|
|
27
|
+
{onDismiss && (
|
|
28
|
+
<button
|
|
29
|
+
onClick={() => { setDismissed(true); onDismiss(); }}
|
|
30
|
+
className="text-muted-foreground transition-colors hover:text-foreground"
|
|
31
|
+
aria-label="Dismiss"
|
|
32
|
+
>
|
|
33
|
+
<X className="size-4" />
|
|
34
|
+
</button>
|
|
35
|
+
)}
|
|
36
|
+
</div>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import * as React from 'react';
|
|
4
|
+
import { useIsMobile, UIVariantProvider, type UIVariant } from '@olwiba/cn';
|
|
5
|
+
|
|
6
|
+
export type UIMode = 'default' | 'playful' | 'smooth';
|
|
7
|
+
|
|
8
|
+
interface OlwibaUIContextValue {
|
|
9
|
+
isMobile: boolean;
|
|
10
|
+
mode: UIMode;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const OlwibaUIContext = React.createContext<OlwibaUIContextValue>({
|
|
14
|
+
isMobile: false,
|
|
15
|
+
mode: 'default',
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
export interface OlwibaUIProviderProps {
|
|
19
|
+
children: React.ReactNode;
|
|
20
|
+
/**
|
|
21
|
+
* Override the mobile detection. If omitted, uses useIsMobile() from @olwiba/cn
|
|
22
|
+
* which detects based on a 768px viewport breakpoint.
|
|
23
|
+
*/
|
|
24
|
+
isMobile?: boolean;
|
|
25
|
+
/**
|
|
26
|
+
* Global component mode. Primitives imported from @olwiba/ui will automatically
|
|
27
|
+
* apply this mode unless overridden at the component level.
|
|
28
|
+
*
|
|
29
|
+
* - `'default'` — standard square shadcn/Radix appearance
|
|
30
|
+
* - `'playful'` — slight rotation + offset drop shadow backdrop
|
|
31
|
+
* - `'smooth'` — softer, larger border-radius across all components
|
|
32
|
+
*/
|
|
33
|
+
mode?: UIMode;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function OlwibaUIProvider({ children, isMobile: isMobileProp, mode = 'default' }: OlwibaUIProviderProps) {
|
|
37
|
+
const detectedMobile = useIsMobile();
|
|
38
|
+
const isMobile = isMobileProp ?? detectedMobile;
|
|
39
|
+
|
|
40
|
+
const variant = mode !== 'default' ? (mode as UIVariant) : undefined;
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<OlwibaUIContext.Provider value={{ isMobile, mode }}>
|
|
44
|
+
<UIVariantProvider mode={variant}>
|
|
45
|
+
{children}
|
|
46
|
+
</UIVariantProvider>
|
|
47
|
+
</OlwibaUIContext.Provider>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function useOlwibaUI() {
|
|
52
|
+
return React.useContext(OlwibaUIContext);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Returns just the current UI mode from context. */
|
|
56
|
+
export function useUIMode(): UIMode {
|
|
57
|
+
return React.useContext(OlwibaUIContext).mode;
|
|
58
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import * as React from 'react';
|
|
4
|
+
|
|
5
|
+
export interface ConfirmOptions {
|
|
6
|
+
title?: string;
|
|
7
|
+
description?: string;
|
|
8
|
+
confirmLabel?: string;
|
|
9
|
+
cancelLabel?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface UseConfirmReturn {
|
|
13
|
+
confirm: (options?: ConfirmOptions) => Promise<boolean>;
|
|
14
|
+
isOpen: boolean;
|
|
15
|
+
options: ConfirmOptions;
|
|
16
|
+
handleConfirm: () => void;
|
|
17
|
+
handleCancel: () => void;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Headless confirmation hook. Returns state and handlers for building a confirm dialog.
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* const { confirm, isOpen, options, handleConfirm, handleCancel } = useConfirm();
|
|
25
|
+
*
|
|
26
|
+
* // Trigger confirmation
|
|
27
|
+
* const ok = await confirm({ title: 'Delete item?', description: 'This cannot be undone.' });
|
|
28
|
+
*
|
|
29
|
+
* // Wire up your dialog
|
|
30
|
+
* <AlertDialog open={isOpen}>
|
|
31
|
+
* <AlertDialogContent>
|
|
32
|
+
* <AlertDialogTitle>{options.title}</AlertDialogTitle>
|
|
33
|
+
* <AlertDialogFooter>
|
|
34
|
+
* <AlertDialogCancel onClick={handleCancel} />
|
|
35
|
+
* <AlertDialogAction onClick={handleConfirm} />
|
|
36
|
+
* </AlertDialogFooter>
|
|
37
|
+
* </AlertDialogContent>
|
|
38
|
+
* </AlertDialog>
|
|
39
|
+
*/
|
|
40
|
+
export function useConfirm(): UseConfirmReturn {
|
|
41
|
+
const [isOpen, setIsOpen] = React.useState(false);
|
|
42
|
+
const [options, setOptions] = React.useState<ConfirmOptions>({});
|
|
43
|
+
const resolveRef = React.useRef<((value: boolean) => void) | null>(null);
|
|
44
|
+
|
|
45
|
+
const confirm = React.useCallback((opts: ConfirmOptions = {}): Promise<boolean> => {
|
|
46
|
+
setOptions(opts);
|
|
47
|
+
setIsOpen(true);
|
|
48
|
+
return new Promise<boolean>((resolve) => {
|
|
49
|
+
resolveRef.current = resolve;
|
|
50
|
+
});
|
|
51
|
+
}, []);
|
|
52
|
+
|
|
53
|
+
const handleConfirm = React.useCallback(() => {
|
|
54
|
+
resolveRef.current?.(true);
|
|
55
|
+
setIsOpen(false);
|
|
56
|
+
}, []);
|
|
57
|
+
|
|
58
|
+
const handleCancel = React.useCallback(() => {
|
|
59
|
+
resolveRef.current?.(false);
|
|
60
|
+
setIsOpen(false);
|
|
61
|
+
}, []);
|
|
62
|
+
|
|
63
|
+
return { confirm, isOpen, options, handleConfirm, handleCancel };
|
|
64
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import * as React from 'react';
|
|
4
|
+
|
|
5
|
+
export interface UseControlledOpenReturn {
|
|
6
|
+
isOpen: boolean;
|
|
7
|
+
open: () => void;
|
|
8
|
+
close: () => void;
|
|
9
|
+
toggle: () => void;
|
|
10
|
+
setIsOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Simple open/close state hook for modals, drawers, dropdowns, and any toggleable UI.
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* const { isOpen, open, close } = useControlledOpen();
|
|
18
|
+
* return (
|
|
19
|
+
* <>
|
|
20
|
+
* <Button onClick={open}>Open</Button>
|
|
21
|
+
* <Dialog open={isOpen} onOpenChange={setIsOpen}>...</Dialog>
|
|
22
|
+
* </>
|
|
23
|
+
* );
|
|
24
|
+
*/
|
|
25
|
+
export function useControlledOpen(defaultOpen = false): UseControlledOpenReturn {
|
|
26
|
+
const [isOpen, setIsOpen] = React.useState(defaultOpen);
|
|
27
|
+
|
|
28
|
+
const open = React.useCallback(() => setIsOpen(true), []);
|
|
29
|
+
const close = React.useCallback(() => setIsOpen(false), []);
|
|
30
|
+
const toggle = React.useCallback(() => setIsOpen((prev) => !prev), []);
|
|
31
|
+
|
|
32
|
+
return { isOpen, open, close, toggle, setIsOpen };
|
|
33
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import * as React from 'react';
|
|
4
|
+
|
|
5
|
+
export function useCopyToClipboard(): [boolean, (text: string) => void] {
|
|
6
|
+
const [copied, setCopied] = React.useState(false);
|
|
7
|
+
const timeoutRef = React.useRef<ReturnType<typeof setTimeout>>(undefined);
|
|
8
|
+
|
|
9
|
+
const copy = React.useCallback((text: string) => {
|
|
10
|
+
navigator.clipboard.writeText(text).then(() => {
|
|
11
|
+
setCopied(true);
|
|
12
|
+
clearTimeout(timeoutRef.current);
|
|
13
|
+
timeoutRef.current = setTimeout(() => setCopied(false), 2000);
|
|
14
|
+
});
|
|
15
|
+
}, []);
|
|
16
|
+
|
|
17
|
+
React.useEffect(() => () => clearTimeout(timeoutRef.current), []);
|
|
18
|
+
|
|
19
|
+
return [copied, copy];
|
|
20
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import * as React from 'react';
|
|
4
|
+
|
|
5
|
+
export function useDebounce<T>(value: T, delay: number): T {
|
|
6
|
+
const [debounced, setDebounced] = React.useState(value);
|
|
7
|
+
|
|
8
|
+
React.useEffect(() => {
|
|
9
|
+
const timer = setTimeout(() => setDebounced(value), delay);
|
|
10
|
+
return () => clearTimeout(timer);
|
|
11
|
+
}, [value, delay]);
|
|
12
|
+
|
|
13
|
+
return debounced;
|
|
14
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import * as React from 'react';
|
|
4
|
+
|
|
5
|
+
export function useIntersectionObserver(
|
|
6
|
+
options?: IntersectionObserverInit,
|
|
7
|
+
): [React.RefObject<Element | null>, boolean] {
|
|
8
|
+
const ref = React.useRef<Element | null>(null);
|
|
9
|
+
const [isIntersecting, setIsIntersecting] = React.useState(false);
|
|
10
|
+
|
|
11
|
+
React.useEffect(() => {
|
|
12
|
+
const el = ref.current;
|
|
13
|
+
if (!el) return;
|
|
14
|
+
|
|
15
|
+
const observer = new IntersectionObserver(([entry]) => {
|
|
16
|
+
setIsIntersecting(entry.isIntersecting);
|
|
17
|
+
}, options);
|
|
18
|
+
|
|
19
|
+
observer.observe(el);
|
|
20
|
+
return () => observer.disconnect();
|
|
21
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
22
|
+
}, []);
|
|
23
|
+
|
|
24
|
+
return [ref, isIntersecting];
|
|
25
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import * as React from 'react';
|
|
4
|
+
|
|
5
|
+
export function useLocalStorage<T>(key: string, initialValue: T): [T, (value: T) => void] {
|
|
6
|
+
const [storedValue, setStoredValue] = React.useState<T>(() => {
|
|
7
|
+
if (typeof window === 'undefined') return initialValue;
|
|
8
|
+
try {
|
|
9
|
+
const item = window.localStorage.getItem(key);
|
|
10
|
+
return item ? (JSON.parse(item) as T) : initialValue;
|
|
11
|
+
} catch {
|
|
12
|
+
return initialValue;
|
|
13
|
+
}
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
const setValue = React.useCallback(
|
|
17
|
+
(value: T) => {
|
|
18
|
+
try {
|
|
19
|
+
setStoredValue(value);
|
|
20
|
+
if (typeof window !== 'undefined') {
|
|
21
|
+
window.localStorage.setItem(key, JSON.stringify(value));
|
|
22
|
+
}
|
|
23
|
+
} catch {
|
|
24
|
+
// Silently ignore — private browsing may block localStorage writes
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
[key],
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
return [storedValue, setValue];
|
|
31
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import * as React from 'react';
|
|
4
|
+
|
|
5
|
+
export function useMediaQuery(query: string): boolean {
|
|
6
|
+
const [matches, setMatches] = React.useState(() => {
|
|
7
|
+
if (typeof window === 'undefined') return false;
|
|
8
|
+
return window.matchMedia(query).matches;
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
React.useEffect(() => {
|
|
12
|
+
if (typeof window === 'undefined') return;
|
|
13
|
+
const mql = window.matchMedia(query);
|
|
14
|
+
setMatches(mql.matches);
|
|
15
|
+
const handler = (e: MediaQueryListEvent) => setMatches(e.matches);
|
|
16
|
+
mql.addEventListener('change', handler);
|
|
17
|
+
return () => mql.removeEventListener('change', handler);
|
|
18
|
+
}, [query]);
|
|
19
|
+
|
|
20
|
+
return matches;
|
|
21
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import * as React from 'react';
|
|
4
|
+
|
|
5
|
+
export interface UsePaginationReturn {
|
|
6
|
+
page: number;
|
|
7
|
+
pageSize: number;
|
|
8
|
+
totalPages: number;
|
|
9
|
+
offset: number;
|
|
10
|
+
hasPrev: boolean;
|
|
11
|
+
hasNext: boolean;
|
|
12
|
+
goTo: (page: number) => void;
|
|
13
|
+
next: () => void;
|
|
14
|
+
prev: () => void;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function usePagination(total: number, pageSize: number): UsePaginationReturn {
|
|
18
|
+
const [page, setPage] = React.useState(1);
|
|
19
|
+
const totalPages = Math.max(1, Math.ceil(total / pageSize));
|
|
20
|
+
const offset = (page - 1) * pageSize;
|
|
21
|
+
|
|
22
|
+
const goTo = React.useCallback(
|
|
23
|
+
(p: number) => setPage(Math.min(Math.max(1, p), totalPages)),
|
|
24
|
+
[totalPages],
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
const next = React.useCallback(() => goTo(page + 1), [page, goTo]);
|
|
28
|
+
const prev = React.useCallback(() => goTo(page - 1), [page, goTo]);
|
|
29
|
+
|
|
30
|
+
return { page, pageSize, totalPages, offset, hasPrev: page > 1, hasNext: page < totalPages, goTo, next, prev };
|
|
31
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import * as React from 'react';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Returns true when the page has scrolled past a given Y threshold in pixels.
|
|
7
|
+
* Useful for showing/hiding sticky headers, back-to-top buttons, etc.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* const scrolledPast = useScrolledPast(100);
|
|
11
|
+
* return <header className={scrolledPast ? 'shadow-md' : ''}>...</header>;
|
|
12
|
+
*/
|
|
13
|
+
export function useScrolledPast(threshold: number): boolean {
|
|
14
|
+
const [scrolledPast, setScrolledPast] = React.useState(false);
|
|
15
|
+
|
|
16
|
+
React.useEffect(() => {
|
|
17
|
+
const handleScroll = () => {
|
|
18
|
+
setScrolledPast(window.scrollY > threshold);
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
handleScroll();
|
|
22
|
+
window.addEventListener('scroll', handleScroll, { passive: true });
|
|
23
|
+
return () => window.removeEventListener('scroll', handleScroll);
|
|
24
|
+
}, [threshold]);
|
|
25
|
+
|
|
26
|
+
return scrolledPast;
|
|
27
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
// @olwiba/ui — App-level components, sections, and hooks built on @olwiba/cn
|
|
2
|
+
|
|
3
|
+
export { cn } from './lib/utils';
|
|
4
|
+
|
|
5
|
+
// ─── Context ──────────────────────────────────────────────────────────────────
|
|
6
|
+
export {
|
|
7
|
+
OlwibaUIProvider,
|
|
8
|
+
useOlwibaUI,
|
|
9
|
+
useUIMode,
|
|
10
|
+
type OlwibaUIProviderProps,
|
|
11
|
+
type UIMode,
|
|
12
|
+
} from './context/OlwibaUIContext';
|
|
13
|
+
|
|
14
|
+
// ─── Primitives — mode-aware re-exports of @olwiba/cn components ─────────────
|
|
15
|
+
export * from './primitives';
|
|
16
|
+
|
|
17
|
+
// ─── App — shells, auth, feedback ────────────────────────────────────────────
|
|
18
|
+
export {
|
|
19
|
+
AppShell,
|
|
20
|
+
type AppShellProps,
|
|
21
|
+
type AppShellBrand,
|
|
22
|
+
type AppShellUser,
|
|
23
|
+
type AppShellAction,
|
|
24
|
+
type AppShellRenderLink,
|
|
25
|
+
type AppNavItem,
|
|
26
|
+
} from './app/AppShell';
|
|
27
|
+
|
|
28
|
+
export {
|
|
29
|
+
AuthSection,
|
|
30
|
+
type AuthSectionProps,
|
|
31
|
+
type AuthFormProps,
|
|
32
|
+
} from './app/AuthSection';
|
|
33
|
+
|
|
34
|
+
export {
|
|
35
|
+
EmptyState,
|
|
36
|
+
type EmptyStateProps,
|
|
37
|
+
} from './app/EmptyState';
|
|
38
|
+
|
|
39
|
+
export {
|
|
40
|
+
ErrorPage,
|
|
41
|
+
type ErrorPageProps,
|
|
42
|
+
} from './app/ErrorPage';
|
|
43
|
+
|
|
44
|
+
export {
|
|
45
|
+
UpgradePrompt,
|
|
46
|
+
type UpgradePromptProps,
|
|
47
|
+
type UpgradeComparisonRow,
|
|
48
|
+
} from './app/UpgradePrompt';
|
|
49
|
+
|
|
50
|
+
// ─── Marketing — page sections ────────────────────────────────────────────────
|
|
51
|
+
export { SectionTitle, type SectionTitleProps } from './marketing/SectionTitle';
|
|
52
|
+
export { HeroSection, type HeroSectionProps } from './marketing/HeroSection';
|
|
53
|
+
export { FeaturesSection, type FeaturesSectionProps } from './marketing/FeaturesSection';
|
|
54
|
+
export { CtaSection, type CtaSectionProps } from './marketing/CtaSection';
|
|
55
|
+
export { PricingSection, type PricingSectionProps, type PricingPlan } from './marketing/PricingSection';
|
|
56
|
+
export { TestimonialsSection, type TestimonialsSectionProps } from './marketing/TestimonialsSection';
|
|
57
|
+
export { TeamSection } from './marketing/TeamSection';
|
|
58
|
+
export { FaqSection, type FaqSectionProps } from './marketing/FaqSection';
|
|
59
|
+
export { StatsSection, type StatsSectionProps } from './marketing/StatsSection';
|
|
60
|
+
export { NewsletterSection } from './marketing/NewsletterSection';
|
|
61
|
+
export { ContactSection } from './marketing/ContactSection';
|
|
62
|
+
|
|
63
|
+
// ─── Marketing — elements ─────────────────────────────────────────────────────
|
|
64
|
+
export { Navbar, type NavbarProps } from './marketing/Navbar';
|
|
65
|
+
export { Footer, type FooterProps } from './marketing/Footer';
|
|
66
|
+
export { LogoStrip, type LogoStripProps } from './marketing/LogoStrip';
|
|
67
|
+
|
|
68
|
+
// ─── Layering ─────────────────────────────────────────────────────────────────
|
|
69
|
+
export { Underlay, type UnderlayProps, type UnderlayVariant } from './overlays/Underlay';
|
|
70
|
+
export { Overlay, type OverlayProps, type OverlayVariant } from './overlays/Overlay';
|
|
71
|
+
|
|
72
|
+
// ─── Motion ───────────────────────────────────────────────────────────────────
|
|
73
|
+
export { FadeIn, type FadeInProps } from './motion/FadeIn';
|
|
74
|
+
export { StaggerChildren, type StaggerChildrenProps } from './motion/StaggerChildren';
|
|
75
|
+
export { CountUp, type CountUpProps } from './motion/CountUp';
|
|
76
|
+
export { PageTransition, type PageTransitionProps } from './motion/PageTransition';
|
|
77
|
+
|
|
78
|
+
// ─── Components — interactive ────────────────────────────────────────────────
|
|
79
|
+
export { Spotlight, type SpotlightProps, type SpotlightGroup, type SpotlightItem } from './components/Spotlight';
|
|
80
|
+
export { Dock, type DockProps, type DockItem } from './components/Dock';
|
|
81
|
+
export { ContextMenu, type ContextMenuProps, type ContextMenuDef } from './components/ContextMenu';
|
|
82
|
+
export { ConfirmDialog, type ConfirmDialogProps } from './components/ConfirmDialog';
|
|
83
|
+
|
|
84
|
+
// ─── Components — cards ───────────────────────────────────────────────────────
|
|
85
|
+
export { GlassCard, type GlassCardProps } from './components/GlassCard';
|
|
86
|
+
export { FeatureCard, type FeatureCardProps } from './components/FeatureCard';
|
|
87
|
+
export { StatCard, type StatCardProps } from './components/StatCard';
|
|
88
|
+
export { TestimonialCard, type TestimonialCardProps } from './components/TestimonialCard';
|
|
89
|
+
export { PricingCard, type PricingCardProps, type PricingFeature } from './components/PricingCard';
|
|
90
|
+
export { ImageCard, type ImageCardProps } from './components/ImageCard';
|
|
91
|
+
|
|
92
|
+
// ─── Components — utility ────────────────────────────────────────────────────
|
|
93
|
+
export { FullPageSpinner } from './components/Spinner';
|
|
94
|
+
export { PageHeader, type PageHeaderProps, type PageHeaderBreadcrumb, type PageHeaderBackButton } from './components/PageHeader';
|
|
95
|
+
export { Suspensed } from './components/Suspensed';
|
|
96
|
+
export { ThemeSwitchMinimal } from './components/ThemeSwitchMinimal';
|
|
97
|
+
export { ThemeColorUpdater } from './components/ThemeColorUpdater';
|
|
98
|
+
export { VersionBanner } from './components/VersionBanner';
|
|
99
|
+
export { DevBanner, type DevBannerProps } from './components/DevBanner';
|
|
100
|
+
export { RegisterHotkeys, type Hotkey } from './components/RegisterHotkeys';
|
|
101
|
+
export { RootErrorFallback } from './components/RootErrorFallback';
|
|
102
|
+
|
|
103
|
+
// ─── Hooks ────────────────────────────────────────────────────────────────────
|
|
104
|
+
export { useMounted } from './hooks/use-mounted';
|
|
105
|
+
export { useConfirm, type ConfirmOptions, type UseConfirmReturn } from './hooks/use-confirm';
|
|
106
|
+
export { useControlledOpen, type UseControlledOpenReturn } from './hooks/use-controlled-open';
|
|
107
|
+
export { useScrolledPast } from './hooks/use-scrolled-past';
|
|
108
|
+
export { useCopyToClipboard } from './hooks/use-copy-to-clipboard';
|
|
109
|
+
export { useDebounce } from './hooks/use-debounce';
|
|
110
|
+
export { useIntersectionObserver } from './hooks/use-intersection-observer';
|
|
111
|
+
export { useLocalStorage } from './hooks/use-local-storage';
|
|
112
|
+
export { useMediaQuery } from './hooks/use-media-query';
|
|
113
|
+
export { usePagination, type UsePaginationReturn } from './hooks/use-pagination';
|
package/src/lib/utils.ts
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import * as React from 'react';
|
|
4
|
+
import { Mail, MapPin, MessageSquare, Phone, Send } from 'lucide-react';
|
|
5
|
+
import { Badge, Button, Input, Label, Separator, Textarea } from '@olwiba/cn';
|
|
6
|
+
|
|
7
|
+
const contactInfo = [
|
|
8
|
+
{ Icon: Mail, label: 'Email', value: 'hello@olwiba.com' },
|
|
9
|
+
{ Icon: Phone, label: 'Phone', value: '+1 (555) 000-0000' },
|
|
10
|
+
{ Icon: MapPin, label: 'Office', value: 'San Francisco, CA' },
|
|
11
|
+
];
|
|
12
|
+
|
|
13
|
+
export function ContactSection() {
|
|
14
|
+
const [submitted, setSubmitted] = React.useState(false);
|
|
15
|
+
|
|
16
|
+
function handleSubmit(e: React.FormEvent) {
|
|
17
|
+
e.preventDefault();
|
|
18
|
+
setSubmitted(true);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
<section className="overflow-hidden rounded-2xl border bg-card">
|
|
23
|
+
<div className="grid lg:grid-cols-[1fr_1.4fr]">
|
|
24
|
+
{/* Left panel */}
|
|
25
|
+
<div className="relative overflow-hidden bg-primary px-8 py-12 text-primary-foreground">
|
|
26
|
+
<div className="absolute inset-0 bg-[radial-gradient(circle_at_top_right,hsl(var(--primary-foreground)/0.1),transparent_60%)]" />
|
|
27
|
+
<div className="relative space-y-6">
|
|
28
|
+
<div>
|
|
29
|
+
<Badge variant="secondary" className="mb-3 bg-primary-foreground/15 text-primary-foreground hover:bg-primary-foreground/20">
|
|
30
|
+
Contact us
|
|
31
|
+
</Badge>
|
|
32
|
+
<h2 className="text-2xl font-semibold">Let's talk</h2>
|
|
33
|
+
<p className="mt-2 text-sm text-primary-foreground/70">
|
|
34
|
+
Have a question or want to work together? Fill in the form and we'll get back to you within one business day.
|
|
35
|
+
</p>
|
|
36
|
+
</div>
|
|
37
|
+
|
|
38
|
+
<Separator className="bg-primary-foreground/20" />
|
|
39
|
+
|
|
40
|
+
<div className="space-y-4">
|
|
41
|
+
{contactInfo.map(({ Icon, label, value }) => (
|
|
42
|
+
<div key={label} className="flex items-center gap-3">
|
|
43
|
+
<div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-primary-foreground/10">
|
|
44
|
+
<Icon className="size-4" />
|
|
45
|
+
</div>
|
|
46
|
+
<div>
|
|
47
|
+
<div className="text-xs text-primary-foreground/60">{label}</div>
|
|
48
|
+
<div className="text-sm font-medium">{value}</div>
|
|
49
|
+
</div>
|
|
50
|
+
</div>
|
|
51
|
+
))}
|
|
52
|
+
</div>
|
|
53
|
+
</div>
|
|
54
|
+
</div>
|
|
55
|
+
|
|
56
|
+
{/* Form */}
|
|
57
|
+
<div className="p-8">
|
|
58
|
+
{submitted ? (
|
|
59
|
+
<div className="flex h-full min-h-[320px] flex-col items-center justify-center gap-4 text-center">
|
|
60
|
+
<div className="flex h-12 w-12 items-center justify-center rounded-2xl bg-primary/10 text-primary">
|
|
61
|
+
<MessageSquare className="size-6" />
|
|
62
|
+
</div>
|
|
63
|
+
<div>
|
|
64
|
+
<h3 className="font-semibold">Message sent</h3>
|
|
65
|
+
<p className="mt-1 text-sm text-muted-foreground">
|
|
66
|
+
Thanks for reaching out. We'll get back to you within one business day.
|
|
67
|
+
</p>
|
|
68
|
+
</div>
|
|
69
|
+
<Button variant="outline" onClick={() => setSubmitted(false)}>Send another</Button>
|
|
70
|
+
</div>
|
|
71
|
+
) : (
|
|
72
|
+
<form onSubmit={handleSubmit} className="space-y-5">
|
|
73
|
+
<div className="grid gap-4 sm:grid-cols-2">
|
|
74
|
+
<div className="space-y-1.5">
|
|
75
|
+
<Label htmlFor="contact-first">First name</Label>
|
|
76
|
+
<Input id="contact-first" placeholder="Olivia" required />
|
|
77
|
+
</div>
|
|
78
|
+
<div className="space-y-1.5">
|
|
79
|
+
<Label htmlFor="contact-last">Last name</Label>
|
|
80
|
+
<Input id="contact-last" placeholder="Reed" required />
|
|
81
|
+
</div>
|
|
82
|
+
</div>
|
|
83
|
+
<div className="space-y-1.5">
|
|
84
|
+
<Label htmlFor="contact-email">Email</Label>
|
|
85
|
+
<Input id="contact-email" type="email" placeholder="olivia@company.com" required />
|
|
86
|
+
</div>
|
|
87
|
+
<div className="space-y-1.5">
|
|
88
|
+
<Label htmlFor="contact-subject">Subject</Label>
|
|
89
|
+
<Input id="contact-subject" placeholder="How can we help?" required />
|
|
90
|
+
</div>
|
|
91
|
+
<div className="space-y-1.5">
|
|
92
|
+
<Label htmlFor="contact-message">Message</Label>
|
|
93
|
+
<Textarea
|
|
94
|
+
id="contact-message"
|
|
95
|
+
placeholder="Tell us what you're working on..."
|
|
96
|
+
className="min-h-28 resize-none"
|
|
97
|
+
required
|
|
98
|
+
/>
|
|
99
|
+
</div>
|
|
100
|
+
<Button type="submit" className="w-full">
|
|
101
|
+
Send message
|
|
102
|
+
<Send className="ml-2 size-4" />
|
|
103
|
+
</Button>
|
|
104
|
+
</form>
|
|
105
|
+
)}
|
|
106
|
+
</div>
|
|
107
|
+
</div>
|
|
108
|
+
</section>
|
|
109
|
+
);
|
|
110
|
+
}
|