@olwiba/ui 0.0.34 → 0.0.36

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.0.34",
3
+ "version": "0.0.36",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -43,11 +43,18 @@
43
43
  "zod": "^4.3.6"
44
44
  },
45
45
  "peerDependencies": {
46
+ "@content-collections/mdx": ">=0.2.0",
46
47
  "@olwiba/cn": ">=0.1.2",
47
48
  "react": ">=18.0.0",
48
49
  "react-dom": ">=18.0.0"
49
50
  },
51
+ "peerDependenciesMeta": {
52
+ "@content-collections/mdx": {
53
+ "optional": true
54
+ }
55
+ },
50
56
  "devDependencies": {
57
+ "@content-collections/mdx": "^0.2.2",
51
58
  "@olwiba/cn": "^0.1.15",
52
59
  "@tailwindcss/vite": "^4.1.18",
53
60
  "@tanstack/react-router": "1.154.8",
@@ -89,6 +89,8 @@ export interface AppShellProps {
89
89
  pageTitle?: string;
90
90
  /** Slot rendered at the start of the top header bar, after the sidebar trigger. */
91
91
  headerStart?: ReactNode;
92
+ /** Slot rendered at the end of the top header bar. */
93
+ headerEnd?: ReactNode;
92
94
  /** Override link rendering for SPA navigation. Defaults to a native <a>. */
93
95
  renderLink?: AppShellRenderLink;
94
96
  children?: ReactNode;
@@ -205,18 +207,26 @@ function ShellSidebar({
205
207
  side: 'left' | 'right';
206
208
  sidebarPosition: 'viewport' | 'contained';
207
209
  }) {
210
+ const fallbackLogo = typeof brand.name === 'string' ? brand.name.slice(0, 1).toUpperCase() : null;
211
+
208
212
  return (
209
213
  <Sidebar side={side} collapsible={collapsible} sidebarPosition={sidebarPosition}>
210
214
  <SidebarHeader>
211
215
  <SidebarMenu>
212
216
  <SidebarMenuItem>
213
- <SidebarMenuButton asChild className="data-[slot=sidebar-menu-button]:!p-1.5">
217
+ <SidebarMenuButton asChild tooltip={typeof brand.name === 'string' ? brand.name : undefined} className="data-[slot=sidebar-menu-button]:!p-1.5">
214
218
  {renderLink({
215
219
  href: brand.href ?? '#',
216
220
  children: (
217
221
  <>
218
- {brand.logo}
219
- <span className="text-base font-semibold">{brand.name}</span>
222
+ {brand.logo ?? (
223
+ <span className="flex size-4 shrink-0 items-center justify-center text-sm font-semibold">
224
+ {fallbackLogo}
225
+ </span>
226
+ )}
227
+ <span className="min-w-0 truncate text-base font-semibold group-data-[collapsible=icon]:hidden">
228
+ {brand.name}
229
+ </span>
220
230
  </>
221
231
  ),
222
232
  })}
@@ -289,7 +299,15 @@ function ShellSidebar({
289
299
  );
290
300
  }
291
301
 
292
- function ShellHeader({ pageTitle, headerStart }: { pageTitle: string; headerStart?: ReactNode }) {
302
+ function ShellHeader({
303
+ pageTitle,
304
+ headerStart,
305
+ headerEnd,
306
+ }: {
307
+ pageTitle: string;
308
+ headerStart?: ReactNode;
309
+ headerEnd?: ReactNode;
310
+ }) {
293
311
  return (
294
312
  <header className="flex h-12 shrink-0 items-center gap-2 border-b transition-[width,height] ease-linear group-has-data-[collapsible=icon]/sidebar-wrapper:h-12">
295
313
  <div className="flex w-full items-center gap-1 px-4 lg:gap-2 lg:px-6">
@@ -301,6 +319,11 @@ function ShellHeader({ pageTitle, headerStart }: { pageTitle: string; headerStar
301
319
  )}
302
320
  <Separator orientation="vertical" className="mx-2 data-[orientation=vertical]:h-4" />
303
321
  <h1 className="text-base font-medium">{pageTitle}</h1>
322
+ {headerEnd && (
323
+ <div className="ml-auto flex items-center gap-1">
324
+ {headerEnd}
325
+ </div>
326
+ )}
304
327
  </div>
305
328
  </header>
306
329
  );
@@ -331,6 +354,7 @@ export function AppShell({
331
354
  user = defaultUser,
332
355
  pageTitle = 'Dashboard',
333
356
  headerStart,
357
+ headerEnd,
334
358
  renderLink = defaultRenderLink,
335
359
  collapsible = 'icon',
336
360
  sidebarPosition = 'viewport',
@@ -355,7 +379,7 @@ export function AppShell({
355
379
  sidebarPosition={sidebarPosition}
356
380
  />
357
381
  <SidebarInset className={isContained ? undefined : 'overflow-y-auto'}>
358
- <ShellHeader pageTitle={pageTitle} headerStart={headerStart} />
382
+ <ShellHeader pageTitle={pageTitle} headerStart={headerStart} headerEnd={headerEnd} />
359
383
  {children}
360
384
  </SidebarInset>
361
385
  </SidebarProvider>
@@ -0,0 +1,71 @@
1
+ import * as React from 'react';
2
+ import { useMDXComponent } from '@content-collections/mdx/react';
3
+ import { cn } from '../lib/utils';
4
+
5
+ const components = {
6
+ h1: ({ className, ...props }: React.HTMLAttributes<HTMLHeadingElement>) => (
7
+ <h1 className={cn('mt-8 scroll-m-20 text-3xl font-bold tracking-tight text-foreground first:mt-0', className)} {...props} />
8
+ ),
9
+ h2: ({ className, ...props }: React.HTMLAttributes<HTMLHeadingElement>) => (
10
+ <h2 className={cn('mt-10 scroll-m-20 text-2xl font-semibold tracking-tight text-foreground first:mt-0', className)} {...props} />
11
+ ),
12
+ h3: ({ className, ...props }: React.HTMLAttributes<HTMLHeadingElement>) => (
13
+ <h3 className={cn('mt-8 scroll-m-20 text-xl font-semibold tracking-tight text-foreground', className)} {...props} />
14
+ ),
15
+ h4: ({ className, ...props }: React.HTMLAttributes<HTMLHeadingElement>) => (
16
+ <h4 className={cn('mt-6 scroll-m-20 text-lg font-semibold tracking-tight text-foreground', className)} {...props} />
17
+ ),
18
+ p: ({ className, ...props }: React.HTMLAttributes<HTMLParagraphElement>) => (
19
+ <p className={cn('leading-7 text-muted-foreground [&:not(:first-child)]:mt-6', className)} {...props} />
20
+ ),
21
+ ul: ({ className, ...props }: React.HTMLAttributes<HTMLUListElement>) => (
22
+ <ul className={cn('my-6 ml-6 list-disc text-muted-foreground', className)} {...props} />
23
+ ),
24
+ ol: ({ className, ...props }: React.HTMLAttributes<HTMLOListElement>) => (
25
+ <ol className={cn('my-6 ml-6 list-decimal text-muted-foreground', className)} {...props} />
26
+ ),
27
+ li: ({ className, ...props }: React.HTMLAttributes<HTMLLIElement>) => (
28
+ <li className={cn('mt-2', className)} {...props} />
29
+ ),
30
+ blockquote: ({ className, ...props }: React.HTMLAttributes<HTMLQuoteElement>) => (
31
+ <blockquote className={cn('mt-6 rounded-r-lg border-l-4 border-primary bg-muted py-4 pl-6 pr-4 italic text-muted-foreground', className)} {...props} />
32
+ ),
33
+ hr: (props: React.HTMLAttributes<HTMLHRElement>) => (
34
+ <hr className="my-8 border-border" {...props} />
35
+ ),
36
+ img: ({ className, alt, ...props }: React.ImgHTMLAttributes<HTMLImageElement>) => (
37
+ <img className={cn('rounded-lg border shadow-sm', className)} alt={alt} {...props} />
38
+ ),
39
+ pre: ({ className, ...props }: React.HTMLAttributes<HTMLPreElement>) => (
40
+ <pre className={cn('mt-6 mb-4 overflow-x-auto rounded-lg bg-muted p-4 text-sm', className)} {...props} />
41
+ ),
42
+ code: ({ className, ...props }: React.HTMLAttributes<HTMLElement>) => (
43
+ <code className={cn('relative rounded bg-muted px-[0.3rem] py-[0.2rem] font-mono text-sm text-foreground', className)} {...props} />
44
+ ),
45
+ table: ({ className, ...props }: React.HTMLAttributes<HTMLTableElement>) => (
46
+ <div className="my-6 w-full overflow-y-auto rounded-lg border border-border">
47
+ <table className={cn('w-full', className)} {...props} />
48
+ </div>
49
+ ),
50
+ tr: ({ className, ...props }: React.HTMLAttributes<HTMLTableRowElement>) => (
51
+ <tr className={cn('m-0 border-t border-border p-0 even:bg-muted', className)} {...props} />
52
+ ),
53
+ th: ({ className, ...props }: React.HTMLAttributes<HTMLTableCellElement>) => (
54
+ <th className={cn('border border-border px-4 py-2 text-left font-bold [&[align=center]]:text-center [&[align=right]]:text-right', className)} {...props} />
55
+ ),
56
+ td: ({ className, ...props }: React.HTMLAttributes<HTMLTableCellElement>) => (
57
+ <td className={cn('border border-border px-4 py-2 text-left [&[align=center]]:text-center [&[align=right]]:text-right', className)} {...props} />
58
+ ),
59
+ a: ({ className, ...props }: React.AnchorHTMLAttributes<HTMLAnchorElement>) => (
60
+ <a className={cn('font-medium text-primary underline underline-offset-4 hover:text-primary/80', className)} {...props} />
61
+ ),
62
+ };
63
+
64
+ export interface MdxContentProps {
65
+ code: string;
66
+ }
67
+
68
+ export function MdxContent({ code }: MdxContentProps) {
69
+ const MDXComponent = useMDXComponent(code);
70
+ return <MDXComponent components={components} />;
71
+ }
@@ -0,0 +1,33 @@
1
+ import * as React from 'react';
2
+ import { cn } from '../lib/utils';
3
+
4
+ function AppScreenRoot({ children, className }: { children?: React.ReactNode; className?: string }) {
5
+ return (
6
+ <div className={cn('flex h-full flex-col', className)}>
7
+ {children}
8
+ </div>
9
+ );
10
+ }
11
+
12
+ function AppScreenHeader({ children, className }: { children?: React.ReactNode; className?: string }) {
13
+ return (
14
+ <div className={cn('flex items-center gap-2 border-b border-border px-4 py-3 shrink-0', className)}>
15
+ {children}
16
+ </div>
17
+ );
18
+ }
19
+
20
+ function AppScreenBody({ children, className }: { children?: React.ReactNode; className?: string }) {
21
+ return (
22
+ <div className={cn('flex-1 overflow-auto px-4 py-3', className)}>
23
+ {children}
24
+ </div>
25
+ );
26
+ }
27
+
28
+ export const AppScreen = Object.assign(AppScreenRoot, {
29
+ Header: AppScreenHeader,
30
+ Body: AppScreenBody,
31
+ });
32
+
33
+ export type { };
@@ -0,0 +1,65 @@
1
+ import * as React from 'react';
2
+ import { cn } from '../lib/utils';
3
+
4
+ const sizes = {
5
+ sm: { outer: 'w-44', height: 'h-80', notch: 'w-16 h-4', border: 'rounded-[2rem]', screen: 'rounded-[1.75rem]' },
6
+ md: { outer: 'w-56', height: 'h-[26rem]', notch: 'w-20 h-5', border: 'rounded-[2.5rem]', screen: 'rounded-[2.25rem]' },
7
+ lg: { outer: 'w-72', height: 'h-[34rem]', notch: 'w-24 h-6', border: 'rounded-[3rem]', screen: 'rounded-[2.75rem]' },
8
+ } as const;
9
+
10
+ export interface PhoneFrameProps {
11
+ children?: React.ReactNode;
12
+ size?: keyof typeof sizes;
13
+ className?: string;
14
+ float?: boolean;
15
+ }
16
+
17
+ export function PhoneFrame({ children, size = 'md', className, float = true }: PhoneFrameProps) {
18
+ const s = sizes[size];
19
+ return (
20
+ <>
21
+ {float && (
22
+ <style>{`
23
+ @keyframes olwiba-phone-float {
24
+ 0%, 100% { transform: translateY(0px); }
25
+ 50% { transform: translateY(-10px); }
26
+ }
27
+ .olwiba-phone-float { animation: olwiba-phone-float 5s ease-in-out infinite; }
28
+ `}</style>
29
+ )}
30
+ <div
31
+ className={cn(
32
+ 'relative mx-auto',
33
+ s.outer,
34
+ float && 'olwiba-phone-float',
35
+ className,
36
+ )}
37
+ >
38
+ {/* Device body */}
39
+ <div
40
+ className={cn(
41
+ 'relative flex w-full flex-col overflow-hidden border-[3px] border-foreground/15 bg-muted shadow-2xl',
42
+ s.border,
43
+ s.height,
44
+ )}
45
+ >
46
+ {/* Status bar / notch area */}
47
+ <div className="relative flex justify-center pt-3 pb-1 shrink-0">
48
+ <div className={cn('rounded-full bg-foreground/10', s.notch)} />
49
+ </div>
50
+
51
+ {/* Screen */}
52
+ <div className={cn('mx-1.5 mb-1.5 flex-1 overflow-hidden bg-background', s.screen)}>
53
+ {children}
54
+ </div>
55
+ </div>
56
+
57
+ {/* Side buttons */}
58
+ <div className="absolute right-[-5px] top-24 h-12 w-[3px] rounded-l-sm bg-foreground/15" />
59
+ <div className="absolute left-[-5px] top-20 h-8 w-[3px] rounded-r-sm bg-foreground/15" />
60
+ <div className="absolute left-[-5px] top-32 h-8 w-[3px] rounded-r-sm bg-foreground/15" />
61
+ <div className="absolute left-[-5px] top-44 h-8 w-[3px] rounded-r-sm bg-foreground/15" />
62
+ </div>
63
+ </>
64
+ );
65
+ }
package/src/index.ts CHANGED
@@ -81,6 +81,10 @@ export { Dock, type DockProps, type DockItem } from './components/Dock';
81
81
  export { ContextMenu, type ContextMenuProps, type ContextMenuDef } from './components/ContextMenu';
82
82
  export { ConfirmDialog, type ConfirmDialogProps } from './components/ConfirmDialog';
83
83
 
84
+ // ─── Components — device mockups ────────────────────────────────────────────
85
+ export { PhoneFrame, type PhoneFrameProps } from './components/PhoneFrame';
86
+ export { AppScreen } from './components/AppScreen';
87
+
84
88
  // ─── Components — cards ───────────────────────────────────────────────────────
85
89
  export { GlassCard, type GlassCardProps } from './components/GlassCard';
86
90
  export { FeatureCard, type FeatureCardProps } from './components/FeatureCard';
@@ -103,6 +107,7 @@ export { RootErrorFallback } from './components/RootErrorFallback';
103
107
  // ─── Blog ────────────────────────────────────────────────────────────────────
104
108
  export { PostCard, type PostCardProps } from './blog/PostCard';
105
109
  export { PostList, type PostListProps } from './blog/PostList';
110
+ export { MdxContent, type MdxContentProps } from './blog/MdxContent';
106
111
 
107
112
  // ─── Hooks ────────────────────────────────────────────────────────────────────
108
113
  export { useMounted } from './hooks/use-mounted';
@@ -4,6 +4,7 @@ import * as React from 'react';
4
4
  import { ArrowRight } from 'lucide-react';
5
5
  import { Avatar, AvatarFallback, AvatarImage, Badge, Button } from '@olwiba/cn';
6
6
  import { FadeIn } from '../motion/FadeIn';
7
+ import { PhoneFrame } from '../components/PhoneFrame';
7
8
  import type { AppShellRenderLink } from '../app/AppShell';
8
9
 
9
10
  export interface HeroSectionProps {
@@ -12,7 +13,9 @@ export interface HeroSectionProps {
12
13
  description: string;
13
14
  primaryCta: { label: string; href: string };
14
15
  secondaryCta?: { label: string; href: string };
15
- heroImage: React.ReactNode;
16
+ heroImage?: React.ReactNode;
17
+ media?: 'image' | 'phone' | 'none';
18
+ phoneSize?: 'sm' | 'md' | 'lg';
16
19
  avatarUrls?: string[];
17
20
  socialProofText?: string;
18
21
  renderLink?: AppShellRenderLink;
@@ -29,6 +32,8 @@ export function HeroSection({
29
32
  primaryCta,
30
33
  secondaryCta,
31
34
  heroImage,
35
+ media = 'image',
36
+ phoneSize = 'lg',
32
37
  avatarUrls,
33
38
  socialProofText,
34
39
  renderLink = defaultRenderLink,
@@ -74,12 +79,22 @@ export function HeroSection({
74
79
  </div>
75
80
  </FadeIn>
76
81
 
77
- {/* Right - hero image */}
78
- <FadeIn direction="right" delay={200}>
79
- <div className="overflow-hidden rounded-2xl">
80
- {heroImage}
81
- </div>
82
- </FadeIn>
82
+ {/* Right - media */}
83
+ {media !== 'none' && (
84
+ <FadeIn direction="right" delay={200}>
85
+ {media === 'phone' ? (
86
+ <div className="flex justify-center">
87
+ <PhoneFrame size={phoneSize}>
88
+ {heroImage}
89
+ </PhoneFrame>
90
+ </div>
91
+ ) : (
92
+ <div className="overflow-hidden rounded-2xl">
93
+ {heroImage}
94
+ </div>
95
+ )}
96
+ </FadeIn>
97
+ )}
83
98
  </div>
84
99
 
85
100
  {/* Avatar social proof row */}