@olwiba/ui 0.1.0 → 0.1.2
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 +10 -6
- package/dist/index.js +66 -35
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/components/ModeSwitchMinimal.tsx +40 -0
- package/src/components/RootErrorFallback.tsx +5 -7
- package/src/context/OlwibaUIContext.tsx +5 -2
- package/src/index.ts +1 -0
- package/src/marketing/Navbar.tsx +30 -20
package/package.json
CHANGED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import * as React from 'react';
|
|
4
|
+
import { LayoutGrid, Sparkles, Layers } from 'lucide-react';
|
|
5
|
+
import { Button } from '@olwiba/cn';
|
|
6
|
+
import { useOlwibaUI, type UIMode } from '../context/OlwibaUIContext';
|
|
7
|
+
|
|
8
|
+
const MODES: UIMode[] = ['default', 'playful', 'smooth'];
|
|
9
|
+
|
|
10
|
+
const MODE_ICONS: Record<UIMode, React.ReactNode> = {
|
|
11
|
+
default: <LayoutGrid className="size-4" />,
|
|
12
|
+
playful: <Sparkles className="size-4" />,
|
|
13
|
+
smooth: <Layers className="size-4" />,
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const MODE_LABELS: Record<UIMode, string> = {
|
|
17
|
+
default: 'Default mode',
|
|
18
|
+
playful: 'Playful mode',
|
|
19
|
+
smooth: 'Smooth mode',
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export function ModeSwitchMinimal() {
|
|
23
|
+
const { mode, setMode } = useOlwibaUI();
|
|
24
|
+
|
|
25
|
+
const cycle = () => {
|
|
26
|
+
const idx = MODES.indexOf(mode);
|
|
27
|
+
setMode(MODES[(idx + 1) % MODES.length]);
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<Button variant="ghost" size="icon" onClick={cycle} className="size-8" aria-label={MODE_LABELS[mode]}>
|
|
32
|
+
<span
|
|
33
|
+
key={mode}
|
|
34
|
+
className="inline-flex size-4 items-center justify-center animate-in fade-in zoom-in-75 duration-200 motion-reduce:animate-none"
|
|
35
|
+
>
|
|
36
|
+
{MODE_ICONS[mode]}
|
|
37
|
+
</span>
|
|
38
|
+
</Button>
|
|
39
|
+
);
|
|
40
|
+
}
|
|
@@ -6,10 +6,10 @@ import { Button } from '@olwiba/cn';
|
|
|
6
6
|
|
|
7
7
|
interface RootErrorFallbackProps {
|
|
8
8
|
error: Error;
|
|
9
|
-
|
|
9
|
+
reset: () => void;
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
export function RootErrorFallback({ error,
|
|
12
|
+
export function RootErrorFallback({ error, reset }: RootErrorFallbackProps) {
|
|
13
13
|
return (
|
|
14
14
|
<div className="flex min-h-svh flex-col items-center justify-center gap-6 p-8 text-center">
|
|
15
15
|
<div className="flex flex-col items-center gap-3">
|
|
@@ -19,11 +19,9 @@ export function RootErrorFallback({ error, resetErrorBoundary }: RootErrorFallba
|
|
|
19
19
|
{error.message || 'An unexpected error occurred.'}
|
|
20
20
|
</p>
|
|
21
21
|
</div>
|
|
22
|
-
{
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
</Button>
|
|
26
|
-
)}
|
|
22
|
+
<Button onClick={reset} variant="outline">
|
|
23
|
+
Try again
|
|
24
|
+
</Button>
|
|
27
25
|
</div>
|
|
28
26
|
);
|
|
29
27
|
}
|
|
@@ -8,11 +8,13 @@ export type UIMode = 'default' | 'playful' | 'smooth';
|
|
|
8
8
|
interface OlwibaUIContextValue {
|
|
9
9
|
isMobile: boolean;
|
|
10
10
|
mode: UIMode;
|
|
11
|
+
setMode: (mode: UIMode) => void;
|
|
11
12
|
}
|
|
12
13
|
|
|
13
14
|
const OlwibaUIContext = React.createContext<OlwibaUIContextValue>({
|
|
14
15
|
isMobile: false,
|
|
15
16
|
mode: 'default',
|
|
17
|
+
setMode: () => {},
|
|
16
18
|
});
|
|
17
19
|
|
|
18
20
|
export interface OlwibaUIProviderProps {
|
|
@@ -33,14 +35,15 @@ export interface OlwibaUIProviderProps {
|
|
|
33
35
|
mode?: UIMode;
|
|
34
36
|
}
|
|
35
37
|
|
|
36
|
-
export function OlwibaUIProvider({ children, isMobile: isMobileProp, mode = 'default' }: OlwibaUIProviderProps) {
|
|
38
|
+
export function OlwibaUIProvider({ children, isMobile: isMobileProp, mode: initialMode = 'default' }: OlwibaUIProviderProps) {
|
|
37
39
|
const detectedMobile = useIsMobile();
|
|
38
40
|
const isMobile = isMobileProp ?? detectedMobile;
|
|
41
|
+
const [mode, setMode] = React.useState<UIMode>(initialMode);
|
|
39
42
|
|
|
40
43
|
const variant = mode !== 'default' ? (mode as UIVariant) : undefined;
|
|
41
44
|
|
|
42
45
|
return (
|
|
43
|
-
<OlwibaUIContext.Provider value={{ isMobile, mode }}>
|
|
46
|
+
<OlwibaUIContext.Provider value={{ isMobile, mode, setMode }}>
|
|
44
47
|
<UIVariantProvider mode={variant}>
|
|
45
48
|
{children}
|
|
46
49
|
</UIVariantProvider>
|
package/src/index.ts
CHANGED
|
@@ -101,6 +101,7 @@ export { FullPageSpinner } from './components/Spinner';
|
|
|
101
101
|
export { PageHeader, type PageHeaderProps, type PageHeaderBreadcrumb, type PageHeaderBackButton } from './components/PageHeader';
|
|
102
102
|
export { Suspensed } from './components/Suspensed';
|
|
103
103
|
export { ThemeSwitchMinimal } from './components/ThemeSwitchMinimal';
|
|
104
|
+
export { ModeSwitchMinimal } from './components/ModeSwitchMinimal';
|
|
104
105
|
export { ThemeColorUpdater } from './components/ThemeColorUpdater';
|
|
105
106
|
export { VersionBanner } from './components/VersionBanner';
|
|
106
107
|
export { DevBanner, type DevBannerProps } from './components/DevBanner';
|
package/src/marketing/Navbar.tsx
CHANGED
|
@@ -4,7 +4,6 @@ import * as React from 'react';
|
|
|
4
4
|
import { Menu, X } from 'lucide-react';
|
|
5
5
|
import { Button, Separator, Sheet, SheetContent, SheetTrigger } from '@olwiba/cn';
|
|
6
6
|
import type { AppShellRenderLink } from '../app/AppShell';
|
|
7
|
-
import { ThemeSwitchMinimal } from '../components/ThemeSwitchMinimal';
|
|
8
7
|
|
|
9
8
|
export interface NavbarProps {
|
|
10
9
|
brand: { name: string; logo?: React.ReactNode; href?: string };
|
|
@@ -13,7 +12,8 @@ export interface NavbarProps {
|
|
|
13
12
|
primary?: { label: string; href: string };
|
|
14
13
|
secondary?: { label: string; href: string };
|
|
15
14
|
};
|
|
16
|
-
|
|
15
|
+
/** Icon-button controls rendered right-of-nav, left-of-CTA. Pass e.g. ModeSwitchMinimal, ThemeSwitchMinimal. */
|
|
16
|
+
controls?: React.ReactNode[];
|
|
17
17
|
renderLink?: AppShellRenderLink;
|
|
18
18
|
}
|
|
19
19
|
|
|
@@ -25,11 +25,12 @@ export function Navbar({
|
|
|
25
25
|
brand,
|
|
26
26
|
navLinks,
|
|
27
27
|
cta,
|
|
28
|
-
|
|
28
|
+
controls,
|
|
29
29
|
renderLink = defaultRenderLink,
|
|
30
30
|
}: NavbarProps) {
|
|
31
31
|
const [open, setOpen] = React.useState(false);
|
|
32
32
|
const brandHref = brand.href ?? '/';
|
|
33
|
+
const hasRightContent = controls?.length || cta?.primary || cta?.secondary;
|
|
33
34
|
|
|
34
35
|
return (
|
|
35
36
|
<section className="overflow-hidden rounded-2xl border bg-card">
|
|
@@ -49,8 +50,8 @@ export function Navbar({
|
|
|
49
50
|
),
|
|
50
51
|
})}
|
|
51
52
|
|
|
52
|
-
{/* Desktop nav links — center column,
|
|
53
|
-
<div className=
|
|
53
|
+
{/* Desktop nav links — center column when right content exists, else shift right */}
|
|
54
|
+
<div className={`hidden items-center gap-1 md:flex ${!hasRightContent ? 'col-span-2 justify-end' : ''}`}>
|
|
54
55
|
{navLinks.map((link) => (
|
|
55
56
|
<span key={link.label}>
|
|
56
57
|
{renderLink({
|
|
@@ -62,20 +63,27 @@ export function Navbar({
|
|
|
62
63
|
))}
|
|
63
64
|
</div>
|
|
64
65
|
|
|
65
|
-
{/* Desktop
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
66
|
+
{/* Desktop right — controls + CTA */}
|
|
67
|
+
{hasRightContent && (
|
|
68
|
+
<div className="hidden items-center justify-end gap-2 md:flex">
|
|
69
|
+
{controls?.map((control, i) => (
|
|
70
|
+
<React.Fragment key={i}>{control}</React.Fragment>
|
|
71
|
+
))}
|
|
72
|
+
{(cta?.secondary || cta?.primary) && controls?.length ? (
|
|
73
|
+
<div className="mx-1 h-4 w-px bg-border" />
|
|
74
|
+
) : null}
|
|
75
|
+
{cta?.secondary &&
|
|
76
|
+
renderLink({
|
|
77
|
+
href: cta.secondary.href,
|
|
78
|
+
children: <Button variant="ghost" size="sm">{cta.secondary.label}</Button>,
|
|
79
|
+
})}
|
|
80
|
+
{cta?.primary &&
|
|
81
|
+
renderLink({
|
|
82
|
+
href: cta.primary.href,
|
|
83
|
+
children: <Button size="sm">{cta.primary.label}</Button>,
|
|
84
|
+
})}
|
|
85
|
+
</div>
|
|
86
|
+
)}
|
|
79
87
|
|
|
80
88
|
{/* Mobile drawer — right column on small screens */}
|
|
81
89
|
<Sheet open={open} onOpenChange={setOpen}>
|
|
@@ -103,7 +111,9 @@ export function Navbar({
|
|
|
103
111
|
})}
|
|
104
112
|
</span>
|
|
105
113
|
<div className="flex items-center gap-1">
|
|
106
|
-
{
|
|
114
|
+
{controls?.map((control, i) => (
|
|
115
|
+
<React.Fragment key={i}>{control}</React.Fragment>
|
|
116
|
+
))}
|
|
107
117
|
<Button variant="ghost" size="icon" onClick={() => setOpen(false)}>
|
|
108
118
|
<X className="size-4" />
|
|
109
119
|
<span className="sr-only">Close menu</span>
|