@olwiba/ui 0.1.11 → 0.1.13

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.11",
3
+ "version": "0.1.13",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -41,7 +41,7 @@
41
41
  "@dnd-kit/modifiers": "^9.0.0",
42
42
  "@dnd-kit/sortable": "^10.0.0",
43
43
  "@dnd-kit/utilities": "^3.2.2",
44
- "@olwiba/dx": "0.0.18",
44
+ "@olwiba/dx": "0.0.20",
45
45
  "@tanstack/react-table": "^8.21.3",
46
46
  "clsx": "^2.1.1",
47
47
  "lucide-react": "^0.562.0",
@@ -63,8 +63,8 @@
63
63
  },
64
64
  "devDependencies": {
65
65
  "@content-collections/mdx": "^0.2.2",
66
- "@olwiba/cn": "0.1.21",
67
- "@olwiba/docs": "0.1.29",
66
+ "@olwiba/cn": "0.1.25",
67
+ "@olwiba/docs": "0.1.32",
68
68
  "@tailwindcss/vite": "^4.1.18",
69
69
  "@tanstack/react-router": "1.154.8",
70
70
  "@tanstack/react-router-devtools": "1.154.8",
@@ -61,6 +61,7 @@ export interface AppShellUser {
61
61
  plan?: string;
62
62
  onSignOut?: () => void;
63
63
  onBilling?: () => void;
64
+ onNotifications?: () => void;
64
65
  }
65
66
 
66
67
  export interface AppShellBrand {
@@ -159,19 +160,25 @@ function NavUser({ user }: { user: AppShellUser }) {
159
160
  </div>
160
161
  </div>
161
162
  </DropdownMenuLabel>
162
- <DropdownMenuSeparator />
163
- <DropdownMenuGroup>
164
- {user.onBilling && (
165
- <DropdownMenuItem onClick={user.onBilling}>
166
- <CreditCardIcon />
167
- Billing
168
- </DropdownMenuItem>
169
- )}
170
- <DropdownMenuItem>
171
- <BellIcon />
172
- Notifications
173
- </DropdownMenuItem>
174
- </DropdownMenuGroup>
163
+ {(user.onBilling || user.onNotifications) && (
164
+ <>
165
+ <DropdownMenuSeparator />
166
+ <DropdownMenuGroup>
167
+ {user.onBilling && (
168
+ <DropdownMenuItem onClick={user.onBilling}>
169
+ <CreditCardIcon />
170
+ Billing
171
+ </DropdownMenuItem>
172
+ )}
173
+ {user.onNotifications && (
174
+ <DropdownMenuItem onClick={user.onNotifications}>
175
+ <BellIcon />
176
+ Notifications
177
+ </DropdownMenuItem>
178
+ )}
179
+ </DropdownMenuGroup>
180
+ </>
181
+ )}
175
182
  {user.onSignOut && (
176
183
  <>
177
184
  <DropdownMenuSeparator />
@@ -113,7 +113,8 @@ function DefaultForm({
113
113
  return (
114
114
  <Card className="w-full">
115
115
  {hasPrefill && (
116
- <style>{`@keyframes auth-prefill{0%{box-shadow:0 0 0 0 hsl(var(--primary)/0)}40%{box-shadow:0 0 0 3px hsl(var(--primary)/0.4)}100%{box-shadow:0 0 0 0 hsl(var(--primary)/0)}}`}</style>
116
+ // Theme tokens hold full color values (oklch), so opacity must come from color-mix, not hsl(var()/a)
117
+ <style>{`@keyframes auth-prefill{0%{box-shadow:0 0 0 0 transparent}40%{box-shadow:0 0 0 3px color-mix(in oklab,var(--primary) 45%,transparent),0 0 16px 2px color-mix(in oklab,var(--primary) 35%,transparent)}100%{box-shadow:0 0 0 0 transparent}}`}</style>
117
118
  )}
118
119
  <CardHeader>
119
120
  {brand && <div className="mb-2">{brand}</div>}
@@ -1,16 +1,26 @@
1
1
  'use client';
2
2
 
3
3
  import type { ReactNode } from 'react';
4
- import { ArrowLeft, Compass } from 'lucide-react';
4
+ import { ArrowLeft, ArrowUpRight } from 'lucide-react';
5
+ import { cn } from '@olwiba/cn';
5
6
  import { Button } from '../primitives/Button';
6
7
  import type { AppShellRenderLink } from './AppShell';
7
8
 
9
+ export interface ErrorPageLink {
10
+ label: string;
11
+ href: string;
12
+ description?: string;
13
+ icon?: ReactNode;
14
+ }
15
+
8
16
  export interface ErrorPageProps {
9
17
  statusCode?: string;
10
18
  title?: string;
11
19
  description?: string;
12
20
  action?: { label: string; href: string };
13
21
  backAction?: { label: string; href?: string; onClick?: () => void };
22
+ /** Optional "maybe you were looking for" links rendered below the actions. */
23
+ links?: ErrorPageLink[];
14
24
  renderLink?: AppShellRenderLink;
15
25
  }
16
26
 
@@ -24,54 +34,111 @@ export function ErrorPage({
24
34
  description = "The page you're looking for doesn't exist or has been moved. Check the URL or head back home.",
25
35
  action = { label: 'Take me home', href: '/' },
26
36
  backAction = { label: 'Go back' },
37
+ links,
27
38
  renderLink = defaultRenderLink,
28
39
  }: ErrorPageProps) {
40
+ const hasLinks = links && links.length > 0;
41
+
29
42
  return (
30
- <section className="overflow-hidden rounded-2xl border bg-card">
31
- <div className="relative flex min-h-[560px] flex-col items-center justify-center px-6 py-16 text-center">
32
- <div className="absolute inset-0 bg-[radial-gradient(circle_at_center,hsl(var(--muted)/0.8),transparent_70%)]" />
33
- <div className="relative space-y-6">
34
- <div className="flex justify-center">
35
- <div className="flex h-16 w-16 items-center justify-center rounded-2xl border bg-muted">
36
- <Compass className="size-8 text-muted-foreground" />
37
- </div>
38
- </div>
43
+ <section className="relative overflow-hidden rounded-2xl border bg-card">
44
+ {/* Dot grid, faded toward the edges */}
45
+ <div
46
+ aria-hidden
47
+ className="absolute inset-0 bg-[radial-gradient(hsl(var(--border))_1px,transparent_1px)] [background-size:24px_24px] [mask-image:radial-gradient(ellipse_at_center,black_30%,transparent_75%)]"
48
+ />
49
+ {/* Soft brand glow behind the numerals */}
50
+ <div
51
+ aria-hidden
52
+ className="absolute left-1/2 top-24 h-64 w-[36rem] max-w-full -translate-x-1/2 rounded-full bg-primary/10 blur-3xl"
53
+ />
39
54
 
40
- <div className="space-y-2">
41
- <div className="text-8xl font-bold tracking-tight text-muted-foreground/20">{statusCode}</div>
42
- <h1 className="-mt-4 text-2xl font-semibold tracking-tight">{title}</h1>
43
- <p className="mx-auto max-w-sm text-sm text-muted-foreground">
44
- {description}
45
- </p>
46
- </div>
55
+ <div
56
+ className={cn(
57
+ 'relative mx-auto flex max-w-3xl flex-col items-center px-6 pt-20 text-center sm:pt-24',
58
+ hasLinks ? 'pb-16' : 'pb-24 sm:pb-28',
59
+ )}
60
+ >
61
+ <span className="inline-flex items-center gap-2 rounded-full border bg-background/80 px-3 py-1 font-mono text-xs text-muted-foreground backdrop-blur">
62
+ <span className="relative flex size-1.5">
63
+ <span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-primary opacity-60" />
64
+ <span className="relative inline-flex size-1.5 rounded-full bg-primary" />
65
+ </span>
66
+ HTTP {statusCode}
67
+ </span>
68
+
69
+ <div
70
+ aria-hidden
71
+ className="pointer-events-none -mb-8 mt-2 select-none bg-gradient-to-b from-foreground/15 via-foreground/[0.06] to-transparent bg-clip-text text-[9rem] font-black leading-none tracking-tighter text-transparent sm:-mb-12 sm:text-[13rem]"
72
+ >
73
+ {statusCode}
74
+ </div>
47
75
 
48
- <div className="flex flex-wrap items-center justify-center gap-3">
49
- {backAction && (
50
- backAction.href ? (
76
+ <h1 className="text-balance text-2xl font-semibold tracking-tight sm:text-3xl">{title}</h1>
77
+ <p className="mx-auto mt-3 max-w-md text-pretty text-sm leading-6 text-muted-foreground">
78
+ {description}
79
+ </p>
80
+
81
+ <div className="mt-8 flex flex-wrap items-center justify-center gap-3">
82
+ {backAction &&
83
+ (backAction.href ? (
84
+ renderLink({
85
+ href: backAction.href,
86
+ children: (
87
+ <Button variant="outline">
88
+ <ArrowLeft className="mr-2 size-4" />
89
+ {backAction.label}
90
+ </Button>
91
+ ),
92
+ })
93
+ ) : (
94
+ <Button variant="outline" onClick={backAction.onClick}>
95
+ <ArrowLeft className="mr-2 size-4" />
96
+ {backAction.label}
97
+ </Button>
98
+ ))}
99
+ {action &&
100
+ renderLink({
101
+ href: action.href,
102
+ children: <Button>{action.label}</Button>,
103
+ })}
104
+ </div>
105
+
106
+ {hasLinks && (
107
+ <div className="mt-14 w-full">
108
+ <p className="text-xs font-medium uppercase tracking-wider text-muted-foreground">
109
+ Maybe you were looking for
110
+ </p>
111
+ <div className="mt-4 grid gap-3 text-left sm:grid-cols-2 lg:grid-cols-3">
112
+ {links.map((link) =>
51
113
  renderLink({
52
- href: backAction.href,
114
+ href: link.href,
115
+ className:
116
+ 'group flex items-start gap-3 rounded-xl border bg-background/60 p-4 backdrop-blur transition-colors hover:border-primary/40 hover:bg-accent',
53
117
  children: (
54
- <Button variant="outline">
55
- <ArrowLeft className="mr-2 size-4" />
56
- {backAction.label}
57
- </Button>
118
+ <>
119
+ {link.icon && (
120
+ <span className="mt-0.5 flex size-8 shrink-0 items-center justify-center rounded-lg border bg-muted text-muted-foreground">
121
+ {link.icon}
122
+ </span>
123
+ )}
124
+ <span className="min-w-0 grow">
125
+ <span className="flex items-center gap-1 text-sm font-medium">
126
+ {link.label}
127
+ <ArrowUpRight className="size-3.5 text-muted-foreground transition-transform group-hover:-translate-y-0.5 group-hover:translate-x-0.5 group-hover:text-foreground" />
128
+ </span>
129
+ {link.description && (
130
+ <span className="mt-0.5 block text-xs leading-5 text-muted-foreground">
131
+ {link.description}
132
+ </span>
133
+ )}
134
+ </span>
135
+ </>
58
136
  ),
59
- })
60
- ) : (
61
- <Button variant="outline" onClick={backAction.onClick}>
62
- <ArrowLeft className="mr-2 size-4" />
63
- {backAction.label}
64
- </Button>
65
- )
66
- )}
67
- {action && (
68
- renderLink({
69
- href: action.href,
70
- children: <Button>{action.label}</Button>,
71
- })
72
- )}
137
+ }),
138
+ )}
139
+ </div>
73
140
  </div>
74
- </div>
141
+ )}
75
142
  </div>
76
143
  </section>
77
144
  );
@@ -1,9 +1,14 @@
1
1
  'use client';
2
2
 
3
3
  import * as React from 'react';
4
- import { Badge, cn } from '@olwiba/cn';
5
4
  import type { AppShellRenderLink } from '../app/AppShell';
6
5
 
6
+ export interface PostAuthor {
7
+ name: string;
8
+ role?: string;
9
+ avatar?: string;
10
+ }
11
+
7
12
  export interface PostCardProps {
8
13
  title: string;
9
14
  description?: string;
@@ -11,55 +16,121 @@ export interface PostCardProps {
11
16
  slug: string;
12
17
  image?: string;
13
18
  tags?: string[];
19
+ author?: PostAuthor;
20
+ hrefPrefix?: string;
21
+ tagHrefPrefix?: string;
14
22
  renderLink?: AppShellRenderLink;
15
23
  }
16
24
 
17
25
  function formatDate(dateStr: string) {
18
26
  return new Date(dateStr).toLocaleDateString('en-US', {
19
27
  year: 'numeric',
20
- month: 'long',
28
+ month: 'short',
21
29
  day: 'numeric',
22
30
  });
23
31
  }
24
32
 
25
- export function PostCard({ title, description, date, slug, image, tags, renderLink }: PostCardProps) {
26
- const href = `/blog/${slug}`;
33
+ export function PostCard({
34
+ title,
35
+ description,
36
+ date,
37
+ slug,
38
+ image,
39
+ tags,
40
+ author,
41
+ hrefPrefix = '/blog',
42
+ tagHrefPrefix = '/blog/tag',
43
+ renderLink,
44
+ }: PostCardProps) {
45
+ const href = `${hrefPrefix}/${slug}`;
27
46
 
28
- const LinkWrapper = ({ children, className }: { children: React.ReactNode; className?: string }) =>
29
- renderLink ? renderLink({ href, children, className }) : <a href={href} className={className}>{children}</a>;
47
+ const LinkWrapper = ({
48
+ href: linkHref,
49
+ children,
50
+ className,
51
+ }: {
52
+ href: string;
53
+ children: React.ReactNode;
54
+ className?: string;
55
+ }) =>
56
+ renderLink ? (
57
+ renderLink({ href: linkHref, children, className })
58
+ ) : (
59
+ <a href={linkHref} className={className}>
60
+ {children}
61
+ </a>
62
+ );
30
63
 
31
64
  return (
32
- <article className="group flex flex-col gap-3">
33
- {image && (
34
- <LinkWrapper className="block overflow-hidden rounded-xl">
65
+ <article className="group relative flex flex-col items-start">
66
+ <div className="relative w-full overflow-hidden rounded-2xl bg-muted">
67
+ {image ? (
35
68
  <img
36
69
  src={image}
37
70
  alt={title}
38
- className="aspect-video w-full object-cover transition-transform duration-300 group-hover:scale-105"
71
+ className="aspect-video w-full object-cover transition-transform duration-500 group-hover:scale-[1.03]"
72
+ />
73
+ ) : (
74
+ <div
75
+ aria-hidden
76
+ className="aspect-video w-full bg-gradient-to-br from-primary/15 via-muted to-muted"
39
77
  />
40
- </LinkWrapper>
41
- )}
42
- <div className="flex flex-col gap-2">
43
- {tags && tags.length > 0 && (
44
- <div className="flex flex-wrap gap-1.5">
45
- {tags.map((tag) => (
46
- <Badge key={tag} variant="secondary" className="text-xs">
47
- {tag}
48
- </Badge>
49
- ))}
50
- </div>
51
78
  )}
52
- <h2 className="text-lg font-semibold leading-snug tracking-tight">
53
- <LinkWrapper className="hover:text-primary transition-colors">
54
- {title}
55
- </LinkWrapper>
56
- </h2>
57
- {description && (
58
- <p className="line-clamp-2 text-sm text-muted-foreground">{description}</p>
79
+ <div className="pointer-events-none absolute inset-0 rounded-2xl ring-1 ring-inset ring-foreground/10" />
80
+ </div>
81
+
82
+ <div className="flex w-full grow flex-col">
83
+ <div className="mt-6 flex flex-wrap items-center gap-x-3 gap-y-2 text-xs">
84
+ <time dateTime={date} className="text-muted-foreground">
85
+ {formatDate(date)}
86
+ </time>
87
+ {tags?.map((tag) => (
88
+ <LinkWrapper
89
+ key={tag}
90
+ href={`${tagHrefPrefix}/${encodeURIComponent(tag)}`}
91
+ className="relative z-10 rounded-full bg-muted px-3 py-1.5 font-medium text-muted-foreground transition-colors hover:bg-accent hover:text-accent-foreground"
92
+ >
93
+ {tag}
94
+ </LinkWrapper>
95
+ ))}
96
+ </div>
97
+
98
+ <div className="grow">
99
+ <h2 className="mt-3 text-lg font-semibold leading-snug tracking-tight transition-colors group-hover:text-primary">
100
+ <LinkWrapper href={href}>
101
+ <span className="absolute inset-0" aria-hidden />
102
+ {title}
103
+ </LinkWrapper>
104
+ </h2>
105
+ {description && (
106
+ <p className="mt-3 line-clamp-3 text-sm leading-6 text-muted-foreground">
107
+ {description}
108
+ </p>
109
+ )}
110
+ </div>
111
+
112
+ {author && (
113
+ <div className="mt-6 flex items-center gap-x-3">
114
+ {author.avatar ? (
115
+ <img
116
+ src={author.avatar}
117
+ alt={author.name}
118
+ className="size-9 rounded-full bg-muted object-cover"
119
+ />
120
+ ) : (
121
+ <span
122
+ aria-hidden
123
+ className="flex size-9 items-center justify-center rounded-full bg-muted text-sm font-semibold text-muted-foreground"
124
+ >
125
+ {author.name.charAt(0).toUpperCase()}
126
+ </span>
127
+ )}
128
+ <div className="text-sm leading-tight">
129
+ <p className="font-semibold">{author.name}</p>
130
+ {author.role && <p className="text-muted-foreground">{author.role}</p>}
131
+ </div>
132
+ </div>
59
133
  )}
60
- <time dateTime={date} className="text-xs text-muted-foreground">
61
- {formatDate(date)}
62
- </time>
63
134
  </div>
64
135
  </article>
65
136
  );
@@ -1,6 +1,7 @@
1
1
  'use client';
2
2
 
3
3
  import * as React from 'react';
4
+ import { cn } from '@olwiba/cn';
4
5
  import { PostCard, type PostCardProps } from './PostCard';
5
6
  import type { AppShellRenderLink } from '../app/AppShell';
6
7
 
@@ -8,9 +9,18 @@ export interface PostListProps {
8
9
  posts: PostCardProps[];
9
10
  renderLink?: AppShellRenderLink;
10
11
  emptyMessage?: string;
12
+ /** Max columns at the widest breakpoint. */
13
+ columns?: 2 | 3;
14
+ className?: string;
11
15
  }
12
16
 
13
- export function PostList({ posts, renderLink, emptyMessage = 'No posts published yet.' }: PostListProps) {
17
+ export function PostList({
18
+ posts,
19
+ renderLink,
20
+ emptyMessage = 'No posts published yet.',
21
+ columns = 3,
22
+ className,
23
+ }: PostListProps) {
14
24
  if (posts.length === 0) {
15
25
  return (
16
26
  <p className="text-sm text-muted-foreground italic">{emptyMessage}</p>
@@ -18,7 +28,13 @@ export function PostList({ posts, renderLink, emptyMessage = 'No posts published
18
28
  }
19
29
 
20
30
  return (
21
- <div className="grid gap-8 sm:grid-cols-2">
31
+ <div
32
+ className={cn(
33
+ 'grid grid-cols-1 gap-x-8 gap-y-14 sm:grid-cols-2',
34
+ columns === 3 && 'lg:grid-cols-3',
35
+ className,
36
+ )}
37
+ >
22
38
  {posts.map((post) => (
23
39
  <PostCard key={post.slug} {...post} renderLink={renderLink} />
24
40
  ))}
@@ -129,7 +129,9 @@ export function BrandColorSwitchMinimal() {
129
129
  <span className="relative inline-flex size-4 items-center justify-center">
130
130
  <Palette className="size-4" />
131
131
  <span
132
- className="absolute -bottom-0.5 -right-0.5 size-1.5 rounded-full ring-1 ring-background"
132
+ className={`absolute -bottom-0.5 -right-0.5 size-1.5 rounded-full ring-1 ring-background${
133
+ active === 'zinc' ? ' dark:ring-white/25' : ''
134
+ }`}
133
135
  style={{ background: activeHex }}
134
136
  />
135
137
  </span>
@@ -147,7 +149,11 @@ export function BrandColorSwitchMinimal() {
147
149
  className="group flex flex-col items-center gap-1"
148
150
  >
149
151
  <span
150
- className="flex size-7 items-center justify-center rounded-full ring-offset-background transition-all group-hover:scale-110"
152
+ // Zinc's swatch is near-black give it a border in dark mode so it
153
+ // doesn't vanish into the popover background (transparent in light)
154
+ className={`flex size-7 items-center justify-center rounded-full ring-offset-background transition-all group-hover:scale-110${
155
+ color.name === 'zinc' ? ' border border-transparent dark:border-white/25' : ''
156
+ }`}
151
157
  style={{
152
158
  background: PREVIEW_HEX[color.name],
153
159
  boxShadow:
@@ -8,6 +8,10 @@ export interface FlowBracketProps {
8
8
  * Defaults to 8 (near the top of the first child).
9
9
  */
10
10
  anchorTop?: number;
11
+ /** Per-side override for the top anchor of the left bracket. Falls back to anchorTop. */
12
+ anchorTopLeft?: number;
13
+ /** Per-side override for the top anchor of the right bracket. Falls back to anchorTop. */
14
+ anchorTopRight?: number;
11
15
  /**
12
16
  * Vertical anchor for the bottom connection as a percentage of the wrapper height (0–100).
13
17
  * Defaults to 92 (near the bottom of the last child).
@@ -57,6 +61,8 @@ export interface FlowBracketProps {
57
61
  */
58
62
  export function FlowBracket({
59
63
  anchorTop = 8,
64
+ anchorTopLeft,
65
+ anchorTopRight,
60
66
  anchorBottom = 92,
61
67
  extent = 22,
62
68
  arrow = true,
@@ -77,13 +83,15 @@ export function FlowBracket({
77
83
 
78
84
  const cl = colorLeft ?? color;
79
85
  const cr = colorRight ?? color;
86
+ const aTL = anchorTopLeft ?? anchorTop;
87
+ const aTR = anchorTopRight ?? anchorTop;
80
88
 
81
89
  // Left bracket: bottom → top (arrowhead at top, pointing right into genesis)
82
- const leftD = `M 0,${anchorBottom} L ${L},${anchorBottom} L ${L},${anchorTop} L 0,${anchorTop}`;
90
+ const leftD = `M 0,${anchorBottom} L ${L},${anchorBottom} L ${L},${aTL} L 0,${aTL}`;
83
91
  // Right bracket: reversed = top → bottom (arrowhead at bottom, pointing left into sync)
84
92
  const rightD = reverseRight
85
- ? `M 1000,${anchorTop} L ${R},${anchorTop} L ${R},${anchorBottom} L 1000,${anchorBottom}`
86
- : `M 1000,${anchorBottom} L ${R},${anchorBottom} L ${R},${anchorTop} L 1000,${anchorTop}`;
93
+ ? `M 1000,${aTR} L ${R},${aTR} L ${R},${anchorBottom} L 1000,${anchorBottom}`
94
+ : `M 1000,${anchorBottom} L ${R},${anchorBottom} L ${R},${aTR} L 1000,${aTR}`;
87
95
 
88
96
  // CSS dot: extent as % of container width
89
97
  const ep = `${(extent / 10).toFixed(1)}%`;
@@ -92,22 +100,22 @@ export function FlowBracket({
92
100
  0%{top:${anchorBottom}%;left:0;opacity:0}
93
101
  6%{opacity:1}
94
102
  20%{top:${anchorBottom}%;left:-${ep}}
95
- 80%{top:${anchorTop}%;left:-${ep};opacity:1}
96
- 94%{top:${anchorTop}%;left:0;opacity:0}
97
- 100%{top:${anchorTop}%;left:0;opacity:0}
103
+ 80%{top:${aTL}%;left:-${ep};opacity:1}
104
+ 94%{top:${aTL}%;left:0;opacity:0}
105
+ 100%{top:${aTL}%;left:0;opacity:0}
98
106
  }
99
107
  @keyframes fb-dr{
100
108
  0%{top:${anchorBottom}%;left:100%;opacity:0}
101
109
  6%{opacity:1}
102
110
  20%{top:${anchorBottom}%;left:calc(100% + ${ep})}
103
- 80%{top:${anchorTop}%;left:calc(100% + ${ep});opacity:1}
104
- 94%{top:${anchorTop}%;left:100%;opacity:0}
105
- 100%{top:${anchorTop}%;left:100%;opacity:0}
111
+ 80%{top:${aTR}%;left:calc(100% + ${ep});opacity:1}
112
+ 94%{top:${aTR}%;left:100%;opacity:0}
113
+ 100%{top:${aTR}%;left:100%;opacity:0}
106
114
  }
107
115
  @keyframes fb-drr{
108
- 0%{top:${anchorTop}%;left:100%;opacity:0}
116
+ 0%{top:${aTR}%;left:100%;opacity:0}
109
117
  6%{opacity:1}
110
- 20%{top:${anchorTop}%;left:calc(100% + ${ep})}
118
+ 20%{top:${aTR}%;left:calc(100% + ${ep})}
111
119
  80%{top:${anchorBottom}%;left:calc(100% + ${ep});opacity:1}
112
120
  94%{top:${anchorBottom}%;left:100%;opacity:0}
113
121
  100%{top:${anchorBottom}%;left:100%;opacity:0}
@@ -167,7 +175,7 @@ export function FlowBracket({
167
175
  <svg
168
176
  aria-hidden
169
177
  viewBox="0 0 10 8"
170
- style={{ ...arrowBase, top: `${anchorTop}%`, left: 0 }}
178
+ style={{ ...arrowBase, top: `${aTL}%`, left: 0 }}
171
179
  >
172
180
  <polyline points="1,1 8,4 1,7" style={{ stroke: cl }} {...chevronProps} />
173
181
  </svg>
@@ -180,7 +188,7 @@ export function FlowBracket({
180
188
  viewBox="0 0 10 8"
181
189
  style={{
182
190
  ...arrowBase,
183
- top: reverseRight ? `${anchorBottom}%` : `${anchorTop}%`,
191
+ top: reverseRight ? `${anchorBottom}%` : `${aTR}%`,
184
192
  left: '100%',
185
193
  }}
186
194
  >
package/src/index.ts CHANGED
@@ -52,6 +52,7 @@ export {
52
52
 
53
53
  // ─── Marketing — page sections ────────────────────────────────────────────────
54
54
  export { SectionTitle, type SectionTitleProps } from './marketing/SectionTitle';
55
+ export { marketingSectionSpacing, type MarketingSectionSpacing } from './marketing/section-spacing';
55
56
  export { HeroSection, type HeroSectionProps } from './marketing/HeroSection';
56
57
  export { FeaturesSection, type FeaturesSectionProps } from './marketing/FeaturesSection';
57
58
  export { GroupedFeaturesSection, type GroupedFeaturesSectionProps, type GroupedFeatureGroup } from './marketing/GroupedFeaturesSection';
@@ -124,7 +125,7 @@ export { RegisterHotkeys, type Hotkey } from './components/RegisterHotkeys';
124
125
  export { RootErrorFallback } from './components/RootErrorFallback';
125
126
 
126
127
  // ─── Blog ────────────────────────────────────────────────────────────────────
127
- export { PostCard, type PostCardProps } from './blog/PostCard';
128
+ export { PostCard, type PostCardProps, type PostAuthor } from './blog/PostCard';
128
129
  export { PostList, type PostListProps } from './blog/PostList';
129
130
  export { ChangelogList, type ChangelogListProps } from './blog/ChangelogList';
130
131
  export { ChangelogCard, type ChangelogCardProps, type ChangelogHighlight, type ChangelogReleaseType } from './blog/ChangelogCard';
@@ -6,6 +6,7 @@ import { Avatar, AvatarFallback, AvatarImage, Badge, Button } from '@olwiba/cn';
6
6
  import { FadeIn } from '../motion/FadeIn';
7
7
  import { PhoneFrame } from '../components/PhoneFrame';
8
8
  import type { AppShellRenderLink } from '../app/AppShell';
9
+ import { marketingSectionSpacing } from './section-spacing';
9
10
 
10
11
  export interface HeroSectionProps {
11
12
  heading: string;
@@ -40,7 +41,7 @@ export function HeroSection({
40
41
  }: HeroSectionProps) {
41
42
  return (
42
43
  <section className="overflow-hidden">
43
- <div className="px-6 py-14 sm:px-10 sm:py-20">
44
+ <div className={marketingSectionSpacing.hero}>
44
45
  <div className="mx-auto grid max-w-6xl items-center gap-10 lg:grid-cols-2 lg:gap-16">
45
46
  {/* Left - text content */}
46
47
  <FadeIn direction="left">