@olwiba/ui 0.1.1 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@olwiba/ui",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -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
+ }
@@ -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';
@@ -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
- showThemeToggle?: boolean;
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
- showThemeToggle = false,
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, truly centered */}
53
- <div className="hidden items-center gap-1 md:flex">
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 CTAsright column, right-aligned */}
66
- <div className="hidden items-center justify-end gap-3 md:flex">
67
- {cta?.secondary &&
68
- renderLink({
69
- href: cta.secondary.href,
70
- children: <Button variant="ghost" size="sm">{cta.secondary.label}</Button>,
71
- })}
72
- {cta?.primary &&
73
- renderLink({
74
- href: cta.primary.href,
75
- children: <Button size="sm">{cta.primary.label}</Button>,
76
- })}
77
- {showThemeToggle && <ThemeSwitchMinimal />}
78
- </div>
66
+ {/* Desktop rightcontrols + 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
- {showThemeToggle && <ThemeSwitchMinimal />}
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>