@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/dist/index.d.ts +35 -5
- package/dist/index.js +226 -90
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
- package/src/app/AppShell.tsx +20 -13
- package/src/app/AuthSection.tsx +2 -1
- package/src/app/ErrorPage.tsx +107 -40
- package/src/blog/PostCard.tsx +102 -31
- package/src/blog/PostList.tsx +18 -2
- package/src/components/BrandColorSwitchMinimal.tsx +8 -2
- package/src/components/FlowBracket.tsx +21 -13
- package/src/index.ts +2 -1
- package/src/marketing/HeroSection.tsx +2 -1
- package/src/marketing/Navbar.tsx +11 -12
- package/src/marketing/section-spacing.ts +6 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@olwiba/ui",
|
|
3
|
-
"version": "0.1.
|
|
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.
|
|
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.
|
|
67
|
-
"@olwiba/docs": "0.1.
|
|
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",
|
package/src/app/AppShell.tsx
CHANGED
|
@@ -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
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
<
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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 />
|
package/src/app/AuthSection.tsx
CHANGED
|
@@ -113,7 +113,8 @@ function DefaultForm({
|
|
|
113
113
|
return (
|
|
114
114
|
<Card className="w-full">
|
|
115
115
|
{hasPrefill && (
|
|
116
|
-
|
|
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>}
|
package/src/app/ErrorPage.tsx
CHANGED
|
@@ -1,16 +1,26 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import type { ReactNode } from 'react';
|
|
4
|
-
import { ArrowLeft,
|
|
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
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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:
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
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
|
-
|
|
141
|
+
)}
|
|
75
142
|
</div>
|
|
76
143
|
</section>
|
|
77
144
|
);
|
package/src/blog/PostCard.tsx
CHANGED
|
@@ -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: '
|
|
28
|
+
month: 'short',
|
|
21
29
|
day: 'numeric',
|
|
22
30
|
});
|
|
23
31
|
}
|
|
24
32
|
|
|
25
|
-
export function PostCard({
|
|
26
|
-
|
|
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 = ({
|
|
29
|
-
|
|
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
|
|
33
|
-
|
|
34
|
-
|
|
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-
|
|
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
|
-
<
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
);
|
package/src/blog/PostList.tsx
CHANGED
|
@@ -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({
|
|
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
|
|
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=
|
|
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
|
-
|
|
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},${
|
|
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,${
|
|
86
|
-
: `M 1000,${anchorBottom} L ${R},${anchorBottom} L ${R},${
|
|
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:${
|
|
96
|
-
94%{top:${
|
|
97
|
-
100%{top:${
|
|
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:${
|
|
104
|
-
94%{top:${
|
|
105
|
-
100%{top:${
|
|
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:${
|
|
116
|
+
0%{top:${aTR}%;left:100%;opacity:0}
|
|
109
117
|
6%{opacity:1}
|
|
110
|
-
20%{top:${
|
|
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: `${
|
|
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}%` : `${
|
|
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=
|
|
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">
|