@olwiba/ui 0.2.26 → 0.2.27
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/docs-manifest.json +1 -1
- package/package.json +3 -2
- package/site/lib/sandboxes.ts +1647 -0
|
@@ -0,0 +1,1647 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import {
|
|
3
|
+
registerSandboxes,
|
|
4
|
+
type SandboxDefinition,
|
|
5
|
+
} from '@olwiba/docs';
|
|
6
|
+
|
|
7
|
+
const uiSandboxes: Record<string, SandboxDefinition> = {
|
|
8
|
+
'app-shell': {
|
|
9
|
+
id: 'app-shell',
|
|
10
|
+
defaultViewport: 'desktop',
|
|
11
|
+
preview: React.lazy(() => import('~/demos/app-shell')),
|
|
12
|
+
files: [
|
|
13
|
+
{
|
|
14
|
+
path: 'app/layout.tsx',
|
|
15
|
+
language: 'tsx',
|
|
16
|
+
code: `import { AppShell } from "@olwiba/ui";
|
|
17
|
+
import { LayoutDashboard, Users, FileText, Settings, Plus } from "lucide-react";
|
|
18
|
+
|
|
19
|
+
export default function AppLayout({ children }) {
|
|
20
|
+
return (
|
|
21
|
+
<AppShell
|
|
22
|
+
brand={{ name: "Acme App", href: "/" }}
|
|
23
|
+
navItems={[
|
|
24
|
+
{ icon: LayoutDashboard, label: "Dashboard", href: "/dashboard", isActive: true },
|
|
25
|
+
{ icon: Users, label: "Team", href: "/team" },
|
|
26
|
+
{ icon: FileText, label: "Documents", href: "/docs" },
|
|
27
|
+
{ icon: Settings, label: "Settings", href: "/settings" },
|
|
28
|
+
]}
|
|
29
|
+
action={{ icon: Plus, label: "New project", href: "/new" }}
|
|
30
|
+
user={{
|
|
31
|
+
name: "Alex Johnson",
|
|
32
|
+
email: "alex@acme.com",
|
|
33
|
+
plan: "pro",
|
|
34
|
+
onSignOut: () => signOut(),
|
|
35
|
+
onBilling: () => router.push("/billing"),
|
|
36
|
+
}}
|
|
37
|
+
pageTitle="Dashboard"
|
|
38
|
+
>
|
|
39
|
+
{children}
|
|
40
|
+
</AppShell>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
`,
|
|
44
|
+
},
|
|
45
|
+
],
|
|
46
|
+
},
|
|
47
|
+
'auth-section': {
|
|
48
|
+
id: 'auth-section',
|
|
49
|
+
defaultViewport: 'desktop',
|
|
50
|
+
preview: React.lazy(() => import('~/demos/auth-section')),
|
|
51
|
+
files: [
|
|
52
|
+
{
|
|
53
|
+
path: 'app/sign-in/page.tsx',
|
|
54
|
+
language: 'tsx',
|
|
55
|
+
code: `import { AuthSection } from "@olwiba/ui";
|
|
56
|
+
|
|
57
|
+
// Centered layout (default)
|
|
58
|
+
export default function SignInPage() {
|
|
59
|
+
return (
|
|
60
|
+
<AuthSection
|
|
61
|
+
onSubmit={(e) => { e.preventDefault(); signIn() }}
|
|
62
|
+
onSso={() => signInWithSso()}
|
|
63
|
+
signUpHref="/register"
|
|
64
|
+
forgotPasswordHref="/forgot-password"
|
|
65
|
+
/>
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
`,
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
path: 'app/sign-in/split-layout.tsx',
|
|
72
|
+
language: 'tsx',
|
|
73
|
+
code: `import { AuthSection } from "@olwiba/ui";
|
|
74
|
+
|
|
75
|
+
// Split layout with decorative panel
|
|
76
|
+
export default function SignInPage() {
|
|
77
|
+
return (
|
|
78
|
+
<AuthSection
|
|
79
|
+
layout="split"
|
|
80
|
+
onSubmit={(e) => { e.preventDefault(); signIn() }}
|
|
81
|
+
onSso={() => signInWithSso()}
|
|
82
|
+
signUpHref="/register"
|
|
83
|
+
forgotPasswordHref="/forgot-password"
|
|
84
|
+
/>
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
`,
|
|
88
|
+
},
|
|
89
|
+
],
|
|
90
|
+
},
|
|
91
|
+
'auth-split-block': {
|
|
92
|
+
id: 'auth-split-block',
|
|
93
|
+
defaultViewport: 'desktop',
|
|
94
|
+
preview: React.lazy(() => import('~/demos/auth-split-block')),
|
|
95
|
+
files: [
|
|
96
|
+
{
|
|
97
|
+
path: 'app/page.tsx',
|
|
98
|
+
language: 'tsx',
|
|
99
|
+
code: `import { AuthSplitBlock } from "@olwiba/ui";
|
|
100
|
+
|
|
101
|
+
export default function Page() {
|
|
102
|
+
return <AuthSplitBlock />;
|
|
103
|
+
}
|
|
104
|
+
`,
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
path: 'src/blocks/AuthSplitBlock.tsx',
|
|
108
|
+
language: 'tsx',
|
|
109
|
+
code: `import { Badge, Button, Card, CardContent, CardDescription, CardHeader, CardTitle, Input, Label } from "@olwiba/cn";
|
|
110
|
+
|
|
111
|
+
export function AuthSplitBlock() {
|
|
112
|
+
return (
|
|
113
|
+
<section className="grid min-h-[560px] overflow-hidden rounded-2xl border bg-card lg:grid-cols-2">
|
|
114
|
+
<div className="relative hidden p-8 lg:flex lg:flex-col lg:justify-between">
|
|
115
|
+
<div className="absolute inset-0 bg-gradient-to-br from-primary/20 via-transparent to-muted" />
|
|
116
|
+
<div className="relative z-10">
|
|
117
|
+
<Badge variant="secondary" className="mb-4">Welcome</Badge>
|
|
118
|
+
<h2 className="text-3xl font-semibold tracking-tight">Build better interfaces faster</h2>
|
|
119
|
+
</div>
|
|
120
|
+
</div>
|
|
121
|
+
|
|
122
|
+
<div className="flex items-center justify-center p-6 lg:p-10">
|
|
123
|
+
<Card className="w-full max-w-md">
|
|
124
|
+
<CardHeader>
|
|
125
|
+
<CardTitle>Sign in</CardTitle>
|
|
126
|
+
<CardDescription>Enter your email and password to continue.</CardDescription>
|
|
127
|
+
</CardHeader>
|
|
128
|
+
<CardContent className="space-y-4">
|
|
129
|
+
<div className="space-y-2">
|
|
130
|
+
<Label htmlFor="email">Email</Label>
|
|
131
|
+
<Input id="email" type="email" placeholder="name@company.com" />
|
|
132
|
+
</div>
|
|
133
|
+
<Button className="w-full">Continue</Button>
|
|
134
|
+
</CardContent>
|
|
135
|
+
</Card>
|
|
136
|
+
</div>
|
|
137
|
+
</section>
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
`,
|
|
141
|
+
},
|
|
142
|
+
],
|
|
143
|
+
},
|
|
144
|
+
'dashboard-overview-block': {
|
|
145
|
+
id: 'dashboard-overview-block',
|
|
146
|
+
defaultViewport: 'desktop',
|
|
147
|
+
preview: React.lazy(() => import('~/demos/dashboard-overview-block')),
|
|
148
|
+
files: [
|
|
149
|
+
{
|
|
150
|
+
path: 'app/page.tsx',
|
|
151
|
+
language: 'tsx',
|
|
152
|
+
code: `import { DashboardOverviewBlock } from "@olwiba/ui";
|
|
153
|
+
|
|
154
|
+
export default function Page() {
|
|
155
|
+
return <DashboardOverviewBlock />;
|
|
156
|
+
}
|
|
157
|
+
`,
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
path: 'src/blocks/DashboardOverviewBlock.tsx',
|
|
161
|
+
language: 'tsx',
|
|
162
|
+
code: `import { ArrowUpRight } from "lucide-react";
|
|
163
|
+
import { Badge, Button, Card, CardContent, CardDescription, CardHeader, CardTitle, Progress, Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@olwiba/cn";
|
|
164
|
+
|
|
165
|
+
export function DashboardOverviewBlock() {
|
|
166
|
+
return (
|
|
167
|
+
<section className="space-y-6 rounded-2xl border bg-card p-4 sm:p-6">
|
|
168
|
+
<div className="flex flex-wrap items-start justify-between gap-3">
|
|
169
|
+
<div>
|
|
170
|
+
<h2 className="text-2xl font-semibold tracking-tight">Overview</h2>
|
|
171
|
+
<p className="text-sm text-muted-foreground">Snapshot of key product and revenue metrics.</p>
|
|
172
|
+
</div>
|
|
173
|
+
<Button variant="outline" size="sm">Export report <ArrowUpRight className="ml-2 size-4" /></Button>
|
|
174
|
+
</div>
|
|
175
|
+
|
|
176
|
+
<Card>
|
|
177
|
+
<CardHeader>
|
|
178
|
+
<CardTitle>Recent invoices</CardTitle>
|
|
179
|
+
<CardDescription>Latest billing activity from your workspace.</CardDescription>
|
|
180
|
+
</CardHeader>
|
|
181
|
+
<CardContent>
|
|
182
|
+
<Table>
|
|
183
|
+
<TableHeader>
|
|
184
|
+
<TableRow>
|
|
185
|
+
<TableHead>Invoice</TableHead>
|
|
186
|
+
<TableHead>Status</TableHead>
|
|
187
|
+
</TableRow>
|
|
188
|
+
</TableHeader>
|
|
189
|
+
<TableBody>
|
|
190
|
+
<TableRow>
|
|
191
|
+
<TableCell>#INV-9031</TableCell>
|
|
192
|
+
<TableCell><Badge>Paid</Badge></TableCell>
|
|
193
|
+
</TableRow>
|
|
194
|
+
</TableBody>
|
|
195
|
+
</Table>
|
|
196
|
+
<div className="mt-4"><Progress value={72} /></div>
|
|
197
|
+
</CardContent>
|
|
198
|
+
</Card>
|
|
199
|
+
</section>
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
`,
|
|
203
|
+
},
|
|
204
|
+
],
|
|
205
|
+
},
|
|
206
|
+
'dashboard-shell-block': {
|
|
207
|
+
id: 'dashboard-shell-block',
|
|
208
|
+
defaultViewport: 'desktop',
|
|
209
|
+
preview: React.lazy(() => import('~/demos/dashboard-shell-block')),
|
|
210
|
+
files: [
|
|
211
|
+
{
|
|
212
|
+
path: 'app/page.tsx',
|
|
213
|
+
language: 'tsx',
|
|
214
|
+
code: `import { DashboardShellBlock } from "@olwiba/ui";
|
|
215
|
+
|
|
216
|
+
export default function Page() {
|
|
217
|
+
return <DashboardShellBlock />;
|
|
218
|
+
}
|
|
219
|
+
`,
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
path: 'src/blocks/DashboardShellBlock.tsx',
|
|
223
|
+
language: 'tsx',
|
|
224
|
+
code: `import { DashboardShellBlock } from "@olwiba/ui";
|
|
225
|
+
|
|
226
|
+
export default function Example() {
|
|
227
|
+
return <DashboardShellBlock />;
|
|
228
|
+
}
|
|
229
|
+
`,
|
|
230
|
+
},
|
|
231
|
+
],
|
|
232
|
+
},
|
|
233
|
+
'marketing-hero-block': {
|
|
234
|
+
id: 'marketing-hero-block',
|
|
235
|
+
defaultViewport: 'desktop',
|
|
236
|
+
preview: React.lazy(() => import('~/demos/marketing-hero-block')),
|
|
237
|
+
files: [
|
|
238
|
+
{
|
|
239
|
+
path: 'app/page.tsx',
|
|
240
|
+
language: 'tsx',
|
|
241
|
+
code: `import { MarketingHeroBlock } from "@olwiba/ui";
|
|
242
|
+
|
|
243
|
+
export default function Page() {
|
|
244
|
+
return <MarketingHeroBlock />;
|
|
245
|
+
}
|
|
246
|
+
`,
|
|
247
|
+
},
|
|
248
|
+
{
|
|
249
|
+
path: 'src/blocks/MarketingHeroBlock.tsx',
|
|
250
|
+
language: 'tsx',
|
|
251
|
+
code: `import { ArrowRight } from "lucide-react";
|
|
252
|
+
import { Badge, Button, Card, CardContent, CardDescription, CardHeader, CardTitle } from "@olwiba/cn";
|
|
253
|
+
|
|
254
|
+
export function MarketingHeroBlock() {
|
|
255
|
+
return (
|
|
256
|
+
<section className="overflow-hidden rounded-2xl border bg-card">
|
|
257
|
+
<div className="relative px-6 py-14 sm:px-10 sm:py-20">
|
|
258
|
+
<div className="relative mx-auto max-w-4xl text-center">
|
|
259
|
+
<Badge className="mb-4" variant="secondary">Base UI Blocks</Badge>
|
|
260
|
+
<h1 className="text-balance text-4xl font-semibold tracking-tight sm:text-5xl">
|
|
261
|
+
Ship polished screens with olwibaCN-powered building blocks
|
|
262
|
+
</h1>
|
|
263
|
+
<div className="mt-8 flex flex-wrap items-center justify-center gap-3">
|
|
264
|
+
<Button size="lg">Get started <ArrowRight className="ml-2 size-4" /></Button>
|
|
265
|
+
<Button size="lg" variant="outline">View examples</Button>
|
|
266
|
+
</div>
|
|
267
|
+
</div>
|
|
268
|
+
</div>
|
|
269
|
+
<div className="grid gap-4 p-6 sm:grid-cols-3 sm:p-8">
|
|
270
|
+
<Card>
|
|
271
|
+
<CardHeader>
|
|
272
|
+
<CardTitle className="text-base">Composable by default</CardTitle>
|
|
273
|
+
<CardDescription>Built from olwibaCN primitives.</CardDescription>
|
|
274
|
+
</CardHeader>
|
|
275
|
+
<CardContent />
|
|
276
|
+
</Card>
|
|
277
|
+
</div>
|
|
278
|
+
</section>
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
`,
|
|
282
|
+
},
|
|
283
|
+
],
|
|
284
|
+
},
|
|
285
|
+
'application-sidebar-block': {
|
|
286
|
+
id: 'application-sidebar-block',
|
|
287
|
+
defaultViewport: 'desktop',
|
|
288
|
+
preview: React.lazy(() => import('~/demos/application-sidebar-block')),
|
|
289
|
+
files: [
|
|
290
|
+
{
|
|
291
|
+
path: 'app/page.tsx',
|
|
292
|
+
language: 'tsx',
|
|
293
|
+
code: `import { ApplicationSidebarBlock } from "@olwiba/ui";
|
|
294
|
+
|
|
295
|
+
export default function Page() {
|
|
296
|
+
return <ApplicationSidebarBlock />;
|
|
297
|
+
}
|
|
298
|
+
`,
|
|
299
|
+
},
|
|
300
|
+
{
|
|
301
|
+
path: 'src/blocks/ApplicationSidebarBlock.tsx',
|
|
302
|
+
language: 'tsx',
|
|
303
|
+
code: `import { ApplicationSidebarBlock } from "@olwiba/ui";
|
|
304
|
+
|
|
305
|
+
export default function Example() {
|
|
306
|
+
return <ApplicationSidebarBlock />;
|
|
307
|
+
}
|
|
308
|
+
`,
|
|
309
|
+
},
|
|
310
|
+
],
|
|
311
|
+
},
|
|
312
|
+
'document-sidebar-block': {
|
|
313
|
+
id: 'document-sidebar-block',
|
|
314
|
+
defaultViewport: 'desktop',
|
|
315
|
+
preview: React.lazy(() => import('~/demos/document-sidebar-block')),
|
|
316
|
+
files: [
|
|
317
|
+
{
|
|
318
|
+
path: 'app/page.tsx',
|
|
319
|
+
language: 'tsx',
|
|
320
|
+
code: `import { DocumentSidebarBlock } from "@olwiba/ui";
|
|
321
|
+
|
|
322
|
+
export default function Page() {
|
|
323
|
+
return <DocumentSidebarBlock />;
|
|
324
|
+
}
|
|
325
|
+
`,
|
|
326
|
+
},
|
|
327
|
+
{
|
|
328
|
+
path: 'src/blocks/DocumentSidebarBlock.tsx',
|
|
329
|
+
language: 'tsx',
|
|
330
|
+
code: `import { DocumentSidebarBlock } from "@olwiba/ui";
|
|
331
|
+
|
|
332
|
+
export default function Example() {
|
|
333
|
+
return <DocumentSidebarBlock />;
|
|
334
|
+
}
|
|
335
|
+
`,
|
|
336
|
+
},
|
|
337
|
+
],
|
|
338
|
+
},
|
|
339
|
+
'login-block': {
|
|
340
|
+
id: 'login-block',
|
|
341
|
+
defaultViewport: 'desktop',
|
|
342
|
+
preview: React.lazy(() => import('~/demos/login-block')),
|
|
343
|
+
files: [
|
|
344
|
+
{
|
|
345
|
+
path: 'app/page.tsx',
|
|
346
|
+
language: 'tsx',
|
|
347
|
+
code: `import { LoginBlock } from "@olwiba/ui";
|
|
348
|
+
|
|
349
|
+
export default function Page() {
|
|
350
|
+
return <LoginBlock />;
|
|
351
|
+
}
|
|
352
|
+
`,
|
|
353
|
+
},
|
|
354
|
+
{
|
|
355
|
+
path: 'src/blocks/LoginBlock.tsx',
|
|
356
|
+
language: 'tsx',
|
|
357
|
+
code: `import { LoginBlock } from "@olwiba/ui";
|
|
358
|
+
|
|
359
|
+
export default function Example() {
|
|
360
|
+
return <LoginBlock />;
|
|
361
|
+
}
|
|
362
|
+
`,
|
|
363
|
+
},
|
|
364
|
+
],
|
|
365
|
+
},
|
|
366
|
+
'marketing-page': {
|
|
367
|
+
id: 'marketing-page',
|
|
368
|
+
defaultViewport: 'desktop',
|
|
369
|
+
preview: React.lazy(() => import('~/demos/marketing-page')),
|
|
370
|
+
files: [
|
|
371
|
+
{
|
|
372
|
+
path: 'app/page.tsx',
|
|
373
|
+
language: 'tsx',
|
|
374
|
+
code: `import {
|
|
375
|
+
MarketingNavBlock,
|
|
376
|
+
MarketingHeroBlock,
|
|
377
|
+
MarketingFeaturesBlock,
|
|
378
|
+
MarketingStatsBlock,
|
|
379
|
+
MarketingTestimonialsBlock,
|
|
380
|
+
MarketingPricingBlock,
|
|
381
|
+
MarketingCtaBlock,
|
|
382
|
+
MarketingFooterBlock,
|
|
383
|
+
} from "@olwiba/ui";
|
|
384
|
+
|
|
385
|
+
export default function MarketingPage() {
|
|
386
|
+
return (
|
|
387
|
+
<div className="flex flex-col gap-4">
|
|
388
|
+
<MarketingNavBlock />
|
|
389
|
+
<MarketingHeroBlock />
|
|
390
|
+
<MarketingFeaturesBlock />
|
|
391
|
+
<MarketingStatsBlock />
|
|
392
|
+
<MarketingTestimonialsBlock />
|
|
393
|
+
<MarketingPricingBlock />
|
|
394
|
+
<MarketingCtaBlock />
|
|
395
|
+
<MarketingFooterBlock />
|
|
396
|
+
</div>
|
|
397
|
+
);
|
|
398
|
+
}
|
|
399
|
+
`,
|
|
400
|
+
},
|
|
401
|
+
],
|
|
402
|
+
},
|
|
403
|
+
'billing-page': {
|
|
404
|
+
id: 'billing-page',
|
|
405
|
+
defaultViewport: 'desktop',
|
|
406
|
+
preview: React.lazy(() => import('~/demos/billing-page')),
|
|
407
|
+
files: [
|
|
408
|
+
{
|
|
409
|
+
path: 'app/billing/page.tsx',
|
|
410
|
+
language: 'tsx',
|
|
411
|
+
code: `import { StatCard } from "@olwiba/ui";
|
|
412
|
+
import { Badge, Button, Separator, Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@olwiba/cn";
|
|
413
|
+
|
|
414
|
+
// See the full source in the preview for the complete billing page composition.
|
|
415
|
+
export default function BillingPage() {
|
|
416
|
+
return (
|
|
417
|
+
<div className="flex min-h-screen flex-col">
|
|
418
|
+
{/* Nav + sidebar + billing content */}
|
|
419
|
+
</div>
|
|
420
|
+
);
|
|
421
|
+
}
|
|
422
|
+
`,
|
|
423
|
+
},
|
|
424
|
+
],
|
|
425
|
+
},
|
|
426
|
+
'sign-in-page': {
|
|
427
|
+
id: 'sign-in-page',
|
|
428
|
+
defaultViewport: 'desktop',
|
|
429
|
+
preview: React.lazy(() => import('~/demos/sign-in-page')),
|
|
430
|
+
files: [
|
|
431
|
+
{
|
|
432
|
+
path: 'app/sign-in/page.tsx',
|
|
433
|
+
language: 'tsx',
|
|
434
|
+
code: `import { MarketingNavBlock, AuthSplitBlock } from "@olwiba/ui";
|
|
435
|
+
|
|
436
|
+
export default function SignInPage() {
|
|
437
|
+
return (
|
|
438
|
+
<div className="flex flex-col gap-4">
|
|
439
|
+
<MarketingNavBlock />
|
|
440
|
+
<AuthSplitBlock />
|
|
441
|
+
</div>
|
|
442
|
+
);
|
|
443
|
+
}
|
|
444
|
+
`,
|
|
445
|
+
},
|
|
446
|
+
],
|
|
447
|
+
},
|
|
448
|
+
'app-ui-page': {
|
|
449
|
+
id: 'app-ui-page',
|
|
450
|
+
defaultViewport: 'desktop',
|
|
451
|
+
preview: React.lazy(() => import('~/demos/app-ui-page')),
|
|
452
|
+
files: [
|
|
453
|
+
{
|
|
454
|
+
path: 'app/dashboard/page.tsx',
|
|
455
|
+
language: 'tsx',
|
|
456
|
+
code: `import { DashboardShellBlock } from "@olwiba/ui";
|
|
457
|
+
|
|
458
|
+
export default function DashboardPage() {
|
|
459
|
+
return <DashboardShellBlock />;
|
|
460
|
+
}
|
|
461
|
+
`,
|
|
462
|
+
},
|
|
463
|
+
],
|
|
464
|
+
},
|
|
465
|
+
|
|
466
|
+
// ─── Marketing ───────────────────────────────────────────────────────────────
|
|
467
|
+
|
|
468
|
+
'hero-section': {
|
|
469
|
+
id: 'hero-section',
|
|
470
|
+
defaultViewport: 'desktop',
|
|
471
|
+
preview: React.lazy(() => import('~/demos/hero-section')),
|
|
472
|
+
files: [
|
|
473
|
+
{
|
|
474
|
+
path: 'app/page.tsx',
|
|
475
|
+
language: 'tsx',
|
|
476
|
+
code: `import { HeroSection } from "@olwiba/ui";
|
|
477
|
+
|
|
478
|
+
export default function Page() {
|
|
479
|
+
return <HeroSection />;
|
|
480
|
+
}
|
|
481
|
+
`,
|
|
482
|
+
},
|
|
483
|
+
],
|
|
484
|
+
},
|
|
485
|
+
'navbar': {
|
|
486
|
+
id: 'navbar',
|
|
487
|
+
defaultViewport: 'desktop',
|
|
488
|
+
preview: React.lazy(() => import('~/demos/navbar')),
|
|
489
|
+
files: [
|
|
490
|
+
{
|
|
491
|
+
path: 'app/layout.tsx',
|
|
492
|
+
language: 'tsx',
|
|
493
|
+
code: `import { Navbar } from "@olwiba/ui";
|
|
494
|
+
|
|
495
|
+
export default function Layout({ children }) {
|
|
496
|
+
return (
|
|
497
|
+
<>
|
|
498
|
+
<Navbar />
|
|
499
|
+
{children}
|
|
500
|
+
</>
|
|
501
|
+
);
|
|
502
|
+
}
|
|
503
|
+
`,
|
|
504
|
+
},
|
|
505
|
+
],
|
|
506
|
+
},
|
|
507
|
+
'features-section': {
|
|
508
|
+
id: 'features-section',
|
|
509
|
+
defaultViewport: 'desktop',
|
|
510
|
+
preview: React.lazy(() => import('~/demos/features-section')),
|
|
511
|
+
files: [
|
|
512
|
+
{
|
|
513
|
+
path: 'app/page.tsx',
|
|
514
|
+
language: 'tsx',
|
|
515
|
+
code: `import { FeaturesSection } from "@olwiba/ui";
|
|
516
|
+
|
|
517
|
+
export default function Page() {
|
|
518
|
+
return <FeaturesSection />;
|
|
519
|
+
}
|
|
520
|
+
`,
|
|
521
|
+
},
|
|
522
|
+
],
|
|
523
|
+
},
|
|
524
|
+
'stats-section': {
|
|
525
|
+
id: 'stats-section',
|
|
526
|
+
defaultViewport: 'desktop',
|
|
527
|
+
preview: React.lazy(() => import('~/demos/stats-section')),
|
|
528
|
+
files: [
|
|
529
|
+
{
|
|
530
|
+
path: 'app/page.tsx',
|
|
531
|
+
language: 'tsx',
|
|
532
|
+
code: `import { StatsSection } from "@olwiba/ui";
|
|
533
|
+
|
|
534
|
+
export default function Page() {
|
|
535
|
+
return <StatsSection />;
|
|
536
|
+
}
|
|
537
|
+
`,
|
|
538
|
+
},
|
|
539
|
+
],
|
|
540
|
+
},
|
|
541
|
+
'faq-section': {
|
|
542
|
+
id: 'faq-section',
|
|
543
|
+
defaultViewport: 'desktop',
|
|
544
|
+
preview: React.lazy(() => import('~/demos/faq-section')),
|
|
545
|
+
files: [
|
|
546
|
+
{
|
|
547
|
+
path: 'app/page.tsx',
|
|
548
|
+
language: 'tsx',
|
|
549
|
+
code: `import { FaqSection } from "@olwiba/ui";
|
|
550
|
+
|
|
551
|
+
export default function Page() {
|
|
552
|
+
return <FaqSection />;
|
|
553
|
+
}
|
|
554
|
+
`,
|
|
555
|
+
},
|
|
556
|
+
],
|
|
557
|
+
},
|
|
558
|
+
'pricing-section': {
|
|
559
|
+
id: 'pricing-section',
|
|
560
|
+
defaultViewport: 'desktop',
|
|
561
|
+
preview: React.lazy(() => import('~/demos/pricing-section')),
|
|
562
|
+
files: [
|
|
563
|
+
{
|
|
564
|
+
path: 'app/page.tsx',
|
|
565
|
+
language: 'tsx',
|
|
566
|
+
code: `import { PricingSection } from "@olwiba/ui";
|
|
567
|
+
|
|
568
|
+
export default function Page() {
|
|
569
|
+
return <PricingSection />;
|
|
570
|
+
}
|
|
571
|
+
`,
|
|
572
|
+
},
|
|
573
|
+
],
|
|
574
|
+
},
|
|
575
|
+
'testimonials-section': {
|
|
576
|
+
id: 'testimonials-section',
|
|
577
|
+
defaultViewport: 'desktop',
|
|
578
|
+
preview: React.lazy(() => import('~/demos/testimonials-section')),
|
|
579
|
+
files: [
|
|
580
|
+
{
|
|
581
|
+
path: 'app/page.tsx',
|
|
582
|
+
language: 'tsx',
|
|
583
|
+
code: `import { TestimonialsSection } from "@olwiba/ui";
|
|
584
|
+
|
|
585
|
+
export default function Page() {
|
|
586
|
+
return <TestimonialsSection />;
|
|
587
|
+
}
|
|
588
|
+
`,
|
|
589
|
+
},
|
|
590
|
+
],
|
|
591
|
+
},
|
|
592
|
+
'team-section': {
|
|
593
|
+
id: 'team-section',
|
|
594
|
+
defaultViewport: 'desktop',
|
|
595
|
+
preview: React.lazy(() => import('~/demos/team-section')),
|
|
596
|
+
files: [
|
|
597
|
+
{
|
|
598
|
+
path: 'app/page.tsx',
|
|
599
|
+
language: 'tsx',
|
|
600
|
+
code: `import { TeamSection } from "@olwiba/ui";
|
|
601
|
+
|
|
602
|
+
export default function Page() {
|
|
603
|
+
return <TeamSection />;
|
|
604
|
+
}
|
|
605
|
+
`,
|
|
606
|
+
},
|
|
607
|
+
],
|
|
608
|
+
},
|
|
609
|
+
'cta-section': {
|
|
610
|
+
id: 'cta-section',
|
|
611
|
+
defaultViewport: 'desktop',
|
|
612
|
+
preview: React.lazy(() => import('~/demos/cta-section')),
|
|
613
|
+
files: [
|
|
614
|
+
{
|
|
615
|
+
path: 'app/page.tsx',
|
|
616
|
+
language: 'tsx',
|
|
617
|
+
code: `import { CtaSection } from "@olwiba/ui";
|
|
618
|
+
|
|
619
|
+
export default function Page() {
|
|
620
|
+
return <CtaSection />;
|
|
621
|
+
}
|
|
622
|
+
`,
|
|
623
|
+
},
|
|
624
|
+
],
|
|
625
|
+
},
|
|
626
|
+
'newsletter-section': {
|
|
627
|
+
id: 'newsletter-section',
|
|
628
|
+
defaultViewport: 'desktop',
|
|
629
|
+
preview: React.lazy(() => import('~/demos/newsletter-section')),
|
|
630
|
+
files: [
|
|
631
|
+
{
|
|
632
|
+
path: 'app/page.tsx',
|
|
633
|
+
language: 'tsx',
|
|
634
|
+
code: `import { NewsletterSection } from "@olwiba/ui";
|
|
635
|
+
|
|
636
|
+
export default function Page() {
|
|
637
|
+
return <NewsletterSection />;
|
|
638
|
+
}
|
|
639
|
+
`,
|
|
640
|
+
},
|
|
641
|
+
],
|
|
642
|
+
},
|
|
643
|
+
'footer': {
|
|
644
|
+
id: 'footer',
|
|
645
|
+
defaultViewport: 'desktop',
|
|
646
|
+
preview: React.lazy(() => import('~/demos/footer')),
|
|
647
|
+
files: [
|
|
648
|
+
{
|
|
649
|
+
path: 'app/layout.tsx',
|
|
650
|
+
language: 'tsx',
|
|
651
|
+
code: `import { Footer } from "@olwiba/ui";
|
|
652
|
+
|
|
653
|
+
const navLinks = [
|
|
654
|
+
{ label: "Features", href: "#" },
|
|
655
|
+
{ label: "Pricing", href: "#" },
|
|
656
|
+
{ label: "About", href: "#" },
|
|
657
|
+
{ label: "Blog", href: "#" },
|
|
658
|
+
{ label: "Privacy", href: "#" },
|
|
659
|
+
{ label: "Terms", href: "#" },
|
|
660
|
+
];
|
|
661
|
+
|
|
662
|
+
export default function Layout({ children }) {
|
|
663
|
+
return (
|
|
664
|
+
<>
|
|
665
|
+
{children}
|
|
666
|
+
<Footer
|
|
667
|
+
brand={{ name: "Nexus Inc" }}
|
|
668
|
+
navLinks={navLinks}
|
|
669
|
+
/>
|
|
670
|
+
</>
|
|
671
|
+
);
|
|
672
|
+
}
|
|
673
|
+
`,
|
|
674
|
+
},
|
|
675
|
+
],
|
|
676
|
+
},
|
|
677
|
+
'steps-section': {
|
|
678
|
+
id: 'steps-section',
|
|
679
|
+
defaultViewport: 'desktop',
|
|
680
|
+
preview: React.lazy(() => import('~/demos/steps-section')),
|
|
681
|
+
files: [
|
|
682
|
+
{
|
|
683
|
+
path: 'app/page.tsx',
|
|
684
|
+
language: 'tsx',
|
|
685
|
+
code: `import { StepsSection } from "@olwiba/ui";
|
|
686
|
+
|
|
687
|
+
export default function Page() {
|
|
688
|
+
return (
|
|
689
|
+
<StepsSection
|
|
690
|
+
variant="timeline"
|
|
691
|
+
title="Up and running in minutes"
|
|
692
|
+
groups={[
|
|
693
|
+
{
|
|
694
|
+
steps: [
|
|
695
|
+
{ title: "Create your account", description: "Sign up with your email." },
|
|
696
|
+
{ title: "Add your first monitor", description: "Paste a search URL." },
|
|
697
|
+
],
|
|
698
|
+
after: <CtaCard />,
|
|
699
|
+
},
|
|
700
|
+
{
|
|
701
|
+
steps: [{ title: "Found what you needed?", description: "Cancel anytime." }],
|
|
702
|
+
},
|
|
703
|
+
]}
|
|
704
|
+
/>
|
|
705
|
+
);
|
|
706
|
+
}
|
|
707
|
+
`,
|
|
708
|
+
},
|
|
709
|
+
],
|
|
710
|
+
},
|
|
711
|
+
'contact-section': {
|
|
712
|
+
id: 'contact-section',
|
|
713
|
+
defaultViewport: 'desktop',
|
|
714
|
+
preview: React.lazy(() => import('~/demos/contact-section')),
|
|
715
|
+
files: [
|
|
716
|
+
{
|
|
717
|
+
path: 'app/contact/page.tsx',
|
|
718
|
+
language: 'tsx',
|
|
719
|
+
code: `import { ContactSection } from "@olwiba/ui";
|
|
720
|
+
|
|
721
|
+
export default function ContactPage() {
|
|
722
|
+
return <ContactSection />;
|
|
723
|
+
}
|
|
724
|
+
`,
|
|
725
|
+
},
|
|
726
|
+
],
|
|
727
|
+
},
|
|
728
|
+
|
|
729
|
+
// ─── Components ──────────────────────────────────────────────────────────────
|
|
730
|
+
|
|
731
|
+
'dock': {
|
|
732
|
+
id: 'dock',
|
|
733
|
+
defaultViewport: 'desktop',
|
|
734
|
+
preview: React.lazy(() => import('~/demos/dock')),
|
|
735
|
+
files: [{ path: 'components/Dock.tsx', language: 'tsx', code: `import { Dock } from "@olwiba/ui";
|
|
736
|
+
import { Bell, Home, Settings } from "lucide-react";
|
|
737
|
+
|
|
738
|
+
const items = [
|
|
739
|
+
{ icon: Home, label: "Home", onClick: () => {} },
|
|
740
|
+
{ icon: Bell, label: "Notifications", onClick: () => {} },
|
|
741
|
+
{ icon: Settings, label: "Settings", onClick: () => {} },
|
|
742
|
+
];
|
|
743
|
+
|
|
744
|
+
export default function Example() {
|
|
745
|
+
return <Dock items={items} />;
|
|
746
|
+
}
|
|
747
|
+
` }],
|
|
748
|
+
},
|
|
749
|
+
|
|
750
|
+
'spotlight': {
|
|
751
|
+
id: 'spotlight',
|
|
752
|
+
defaultViewport: 'desktop',
|
|
753
|
+
preview: React.lazy(() => import('~/demos/spotlight')),
|
|
754
|
+
files: [{ path: 'components/SpotlightDemo.tsx', language: 'tsx', code: `import { Spotlight } from "@olwiba/ui";
|
|
755
|
+
import { LayoutDashboard, Settings } from "lucide-react";
|
|
756
|
+
|
|
757
|
+
const groups = [
|
|
758
|
+
{
|
|
759
|
+
heading: "Pages",
|
|
760
|
+
items: [
|
|
761
|
+
{ id: "dashboard", label: "Dashboard", icon: <LayoutDashboard className="size-4" />, onSelect: () => {} },
|
|
762
|
+
],
|
|
763
|
+
},
|
|
764
|
+
{
|
|
765
|
+
heading: "Settings",
|
|
766
|
+
items: [
|
|
767
|
+
{ id: "settings", label: "Settings", icon: <Settings className="size-4" />, onSelect: () => {} },
|
|
768
|
+
],
|
|
769
|
+
},
|
|
770
|
+
];
|
|
771
|
+
|
|
772
|
+
export default function Example() {
|
|
773
|
+
return <Spotlight groups={groups} />;
|
|
774
|
+
}
|
|
775
|
+
` }],
|
|
776
|
+
},
|
|
777
|
+
|
|
778
|
+
'confirm-dialog': {
|
|
779
|
+
id: 'confirm-dialog',
|
|
780
|
+
defaultViewport: 'desktop',
|
|
781
|
+
preview: React.lazy(() => import('~/demos/confirm-dialog')),
|
|
782
|
+
files: [{ path: 'components/ConfirmDemo.tsx', language: 'tsx', code: `import { ConfirmDialog, useConfirm } from "@olwiba/ui";
|
|
783
|
+
import { Button } from "@olwiba/cn";
|
|
784
|
+
|
|
785
|
+
export default function Example() {
|
|
786
|
+
const { confirm, isOpen, options, handleConfirm, handleCancel } = useConfirm();
|
|
787
|
+
|
|
788
|
+
async function handleDelete() {
|
|
789
|
+
const ok = await confirm({
|
|
790
|
+
title: "Delete project?",
|
|
791
|
+
description: "This cannot be undone.",
|
|
792
|
+
confirmLabel: "Delete",
|
|
793
|
+
});
|
|
794
|
+
if (ok) console.log("Deleted");
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
return (
|
|
798
|
+
<>
|
|
799
|
+
<Button variant="destructive" onClick={handleDelete}>Delete project</Button>
|
|
800
|
+
<ConfirmDialog
|
|
801
|
+
isOpen={isOpen}
|
|
802
|
+
options={options}
|
|
803
|
+
handleConfirm={handleConfirm}
|
|
804
|
+
handleCancel={handleCancel}
|
|
805
|
+
destructive
|
|
806
|
+
/>
|
|
807
|
+
</>
|
|
808
|
+
);
|
|
809
|
+
}
|
|
810
|
+
` }],
|
|
811
|
+
},
|
|
812
|
+
|
|
813
|
+
'context-menu': {
|
|
814
|
+
id: 'context-menu',
|
|
815
|
+
defaultViewport: 'desktop',
|
|
816
|
+
preview: React.lazy(() => import('~/demos/context-menu')),
|
|
817
|
+
files: [{ path: 'components/ContextMenuDemo.tsx', language: 'tsx', code: `import { ContextMenu } from "@olwiba/ui";
|
|
818
|
+
|
|
819
|
+
const items = [
|
|
820
|
+
{ type: "item" as const, label: "Open", shortcut: "↵" },
|
|
821
|
+
{ type: "separator" as const },
|
|
822
|
+
{ type: "item" as const, label: "Delete", shortcut: "⌫" },
|
|
823
|
+
];
|
|
824
|
+
|
|
825
|
+
export default function Example() {
|
|
826
|
+
return (
|
|
827
|
+
<ContextMenu items={items}>
|
|
828
|
+
<div className="rounded-xl border border-dashed p-8 text-sm text-muted-foreground">
|
|
829
|
+
Right-click here
|
|
830
|
+
</div>
|
|
831
|
+
</ContextMenu>
|
|
832
|
+
);
|
|
833
|
+
}
|
|
834
|
+
` }],
|
|
835
|
+
},
|
|
836
|
+
|
|
837
|
+
'glass-card': {
|
|
838
|
+
id: 'glass-card',
|
|
839
|
+
defaultViewport: 'desktop',
|
|
840
|
+
preview: React.lazy(() => import('~/demos/glass-card')),
|
|
841
|
+
files: [{ path: 'components/GlassCardDemo.tsx', language: 'tsx', code: `import { GlassCard } from "@olwiba/ui";
|
|
842
|
+
|
|
843
|
+
export default function Example() {
|
|
844
|
+
return (
|
|
845
|
+
<div className="bg-gradient-to-br from-violet-500 to-cyan-400 p-12 rounded-2xl">
|
|
846
|
+
<GlassCard blur="md" className="p-6">
|
|
847
|
+
<h3 className="font-semibold">Glass card</h3>
|
|
848
|
+
<p className="text-sm text-muted-foreground mt-1">Frosted glass effect.</p>
|
|
849
|
+
</GlassCard>
|
|
850
|
+
</div>
|
|
851
|
+
);
|
|
852
|
+
}
|
|
853
|
+
` }],
|
|
854
|
+
},
|
|
855
|
+
|
|
856
|
+
'feature-card': {
|
|
857
|
+
id: 'feature-card',
|
|
858
|
+
defaultViewport: 'desktop',
|
|
859
|
+
preview: React.lazy(() => import('~/demos/feature-card')),
|
|
860
|
+
files: [{ path: 'components/FeatureCardDemo.tsx', language: 'tsx', code: `import { FeatureCard } from "@olwiba/ui";
|
|
861
|
+
import { Zap } from "lucide-react";
|
|
862
|
+
|
|
863
|
+
export default function Example() {
|
|
864
|
+
return (
|
|
865
|
+
<FeatureCard
|
|
866
|
+
icon={Zap}
|
|
867
|
+
title="Fast by default"
|
|
868
|
+
description="Built on optimised primitives."
|
|
869
|
+
href="/docs"
|
|
870
|
+
/>
|
|
871
|
+
);
|
|
872
|
+
}
|
|
873
|
+
` }],
|
|
874
|
+
},
|
|
875
|
+
|
|
876
|
+
'stat-card': {
|
|
877
|
+
id: 'stat-card',
|
|
878
|
+
defaultViewport: 'desktop',
|
|
879
|
+
preview: React.lazy(() => import('~/demos/stat-card')),
|
|
880
|
+
files: [{ path: 'components/StatCardDemo.tsx', language: 'tsx', code: `import { StatCard } from "@olwiba/ui";
|
|
881
|
+
|
|
882
|
+
export default function Example() {
|
|
883
|
+
return (
|
|
884
|
+
<div className="grid grid-cols-3 gap-4">
|
|
885
|
+
<StatCard label="Revenue" value="£24,200" delta="+12%" trend="up" description="vs last month" />
|
|
886
|
+
<StatCard label="Users" value="3,841" delta="+4%" trend="up" />
|
|
887
|
+
<StatCard label="Churn" value="1.8%" delta="+0.3%" trend="down" />
|
|
888
|
+
</div>
|
|
889
|
+
);
|
|
890
|
+
}
|
|
891
|
+
` }],
|
|
892
|
+
},
|
|
893
|
+
|
|
894
|
+
'testimonial-card': {
|
|
895
|
+
id: 'testimonial-card',
|
|
896
|
+
defaultViewport: 'desktop',
|
|
897
|
+
preview: React.lazy(() => import('~/demos/testimonial-card')),
|
|
898
|
+
files: [{ path: 'components/TestimonialCardDemo.tsx', language: 'tsx', code: `import { TestimonialCard } from "@olwiba/ui";
|
|
899
|
+
|
|
900
|
+
export default function Example() {
|
|
901
|
+
return (
|
|
902
|
+
<TestimonialCard
|
|
903
|
+
quote="The component library cut our setup time in half."
|
|
904
|
+
name="Sarah Chen"
|
|
905
|
+
role="Product Engineer"
|
|
906
|
+
company="Vercel"
|
|
907
|
+
initials="SC"
|
|
908
|
+
rating={5}
|
|
909
|
+
/>
|
|
910
|
+
);
|
|
911
|
+
}
|
|
912
|
+
` }],
|
|
913
|
+
},
|
|
914
|
+
|
|
915
|
+
'pricing-card': {
|
|
916
|
+
id: 'pricing-card',
|
|
917
|
+
defaultViewport: 'desktop',
|
|
918
|
+
preview: React.lazy(() => import('~/demos/pricing-card')),
|
|
919
|
+
files: [{ path: 'components/PricingCardDemo.tsx', language: 'tsx', code: `import { PricingCard } from "@olwiba/ui";
|
|
920
|
+
|
|
921
|
+
const features = [
|
|
922
|
+
{ label: "Unlimited projects", included: true },
|
|
923
|
+
{ label: "Priority support", included: true },
|
|
924
|
+
{ label: "Custom domains", included: true },
|
|
925
|
+
{ label: "SLA guarantee", included: false },
|
|
926
|
+
];
|
|
927
|
+
|
|
928
|
+
export default function Example() {
|
|
929
|
+
return (
|
|
930
|
+
<PricingCard
|
|
931
|
+
name="Pro"
|
|
932
|
+
price="£15"
|
|
933
|
+
description="For teams shipping production apps."
|
|
934
|
+
features={features}
|
|
935
|
+
cta="Upgrade to Pro"
|
|
936
|
+
highlighted
|
|
937
|
+
badge="Most popular"
|
|
938
|
+
onSelect={() => {}}
|
|
939
|
+
/>
|
|
940
|
+
);
|
|
941
|
+
}
|
|
942
|
+
` }],
|
|
943
|
+
},
|
|
944
|
+
|
|
945
|
+
'image-card': {
|
|
946
|
+
id: 'image-card',
|
|
947
|
+
defaultViewport: 'desktop',
|
|
948
|
+
preview: React.lazy(() => import('~/demos/image-card')),
|
|
949
|
+
files: [{ path: 'components/ImageCardDemo.tsx', language: 'tsx', code: `import { ImageCard } from "@olwiba/ui";
|
|
950
|
+
|
|
951
|
+
export default function Example() {
|
|
952
|
+
return (
|
|
953
|
+
<ImageCard
|
|
954
|
+
src="https://picsum.photos/seed/landscape/640/360"
|
|
955
|
+
alt="Landscape"
|
|
956
|
+
overlay="subtle"
|
|
957
|
+
>
|
|
958
|
+
<h3 className="font-semibold text-lg text-white">Mountain escape</h3>
|
|
959
|
+
<p className="text-sm text-white/80">Swiss Alps</p>
|
|
960
|
+
</ImageCard>
|
|
961
|
+
);
|
|
962
|
+
}
|
|
963
|
+
` }],
|
|
964
|
+
},
|
|
965
|
+
|
|
966
|
+
// ─── Motion ──────────────────────────────────────────────────────────────────
|
|
967
|
+
|
|
968
|
+
'animated-swap': {
|
|
969
|
+
id: 'animated-swap',
|
|
970
|
+
defaultViewport: 'desktop',
|
|
971
|
+
preview: React.lazy(() => import('~/demos/animated-swap')),
|
|
972
|
+
files: [{ path: 'components/AnimatedSwapDemo.tsx', language: 'tsx', code: `import { useState } from "react";
|
|
973
|
+
import { AnimatedSwap } from "@olwiba/ui";
|
|
974
|
+
|
|
975
|
+
const PRICES = ["$9", "$25", "$120"];
|
|
976
|
+
|
|
977
|
+
export default function Example() {
|
|
978
|
+
const [step, setStep] = useState(0);
|
|
979
|
+
const price = PRICES[step % PRICES.length];
|
|
980
|
+
|
|
981
|
+
return (
|
|
982
|
+
<div className="flex items-end gap-1" onClick={() => setStep((s) => s + 1)}>
|
|
983
|
+
<AnimatedSwap swapKey={price} effect="roll" className="text-5xl font-bold tracking-tight">
|
|
984
|
+
{price}
|
|
985
|
+
</AnimatedSwap>
|
|
986
|
+
<span className="mb-1.5 text-sm text-muted-foreground">/month</span>
|
|
987
|
+
</div>
|
|
988
|
+
);
|
|
989
|
+
}
|
|
990
|
+
` }],
|
|
991
|
+
},
|
|
992
|
+
|
|
993
|
+
'count-up': {
|
|
994
|
+
id: 'count-up',
|
|
995
|
+
defaultViewport: 'desktop',
|
|
996
|
+
preview: React.lazy(() => import('~/demos/count-up')),
|
|
997
|
+
files: [{ path: 'components/CountUpDemo.tsx', language: 'tsx', code: `import { CountUp } from "@olwiba/ui";
|
|
998
|
+
|
|
999
|
+
export default function Example() {
|
|
1000
|
+
return (
|
|
1001
|
+
<div className="flex gap-8 text-center">
|
|
1002
|
+
<div>
|
|
1003
|
+
<CountUp to={12400} prefix="£" duration={1800} className="text-3xl font-bold" />
|
|
1004
|
+
<p className="text-sm text-muted-foreground">Revenue</p>
|
|
1005
|
+
</div>
|
|
1006
|
+
<div>
|
|
1007
|
+
<CountUp to={98.6} decimals={1} suffix="%" duration={1800} className="text-3xl font-bold" />
|
|
1008
|
+
<p className="text-sm text-muted-foreground">Uptime</p>
|
|
1009
|
+
</div>
|
|
1010
|
+
</div>
|
|
1011
|
+
);
|
|
1012
|
+
}
|
|
1013
|
+
` }],
|
|
1014
|
+
},
|
|
1015
|
+
|
|
1016
|
+
'fade-in': {
|
|
1017
|
+
id: 'fade-in',
|
|
1018
|
+
defaultViewport: 'desktop',
|
|
1019
|
+
preview: React.lazy(() => import('~/demos/fade-in')),
|
|
1020
|
+
files: [{ path: 'components/FadeInDemo.tsx', language: 'tsx', code: `import { FadeIn } from "@olwiba/ui";
|
|
1021
|
+
|
|
1022
|
+
export default function Example() {
|
|
1023
|
+
return (
|
|
1024
|
+
<FadeIn direction="up" delay={100}>
|
|
1025
|
+
<div className="rounded-xl border bg-card p-6">
|
|
1026
|
+
<h3 className="font-semibold">Fades in from below</h3>
|
|
1027
|
+
<p className="text-sm text-muted-foreground mt-1">Triggered on intersection.</p>
|
|
1028
|
+
</div>
|
|
1029
|
+
</FadeIn>
|
|
1030
|
+
);
|
|
1031
|
+
}
|
|
1032
|
+
` }],
|
|
1033
|
+
},
|
|
1034
|
+
|
|
1035
|
+
'stagger-children': {
|
|
1036
|
+
id: 'stagger-children',
|
|
1037
|
+
defaultViewport: 'desktop',
|
|
1038
|
+
preview: React.lazy(() => import('~/demos/stagger-children')),
|
|
1039
|
+
files: [{ path: 'components/StaggerDemo.tsx', language: 'tsx', code: `import { StaggerChildren } from "@olwiba/ui";
|
|
1040
|
+
|
|
1041
|
+
export default function Example() {
|
|
1042
|
+
return (
|
|
1043
|
+
<StaggerChildren className="flex flex-col gap-2" stagger={80}>
|
|
1044
|
+
{["Dashboard", "Analytics", "Projects", "Team"].map((item) => (
|
|
1045
|
+
<div key={item} className="rounded-xl border bg-card px-4 py-3 text-sm">
|
|
1046
|
+
{item}
|
|
1047
|
+
</div>
|
|
1048
|
+
))}
|
|
1049
|
+
</StaggerChildren>
|
|
1050
|
+
);
|
|
1051
|
+
}
|
|
1052
|
+
` }],
|
|
1053
|
+
},
|
|
1054
|
+
|
|
1055
|
+
'page-transition': {
|
|
1056
|
+
id: 'page-transition',
|
|
1057
|
+
defaultViewport: 'desktop',
|
|
1058
|
+
preview: React.lazy(() => import('~/demos/page-transition')),
|
|
1059
|
+
files: [{ path: 'routes/page.tsx', language: 'tsx', code: `import { PageTransition } from "@olwiba/ui";
|
|
1060
|
+
|
|
1061
|
+
export default function Page() {
|
|
1062
|
+
return (
|
|
1063
|
+
<PageTransition variant="slide-up">
|
|
1064
|
+
<main className="p-6">
|
|
1065
|
+
<h1 className="text-2xl font-bold">Page content</h1>
|
|
1066
|
+
<p className="text-muted-foreground mt-2">Animates in on mount.</p>
|
|
1067
|
+
</main>
|
|
1068
|
+
</PageTransition>
|
|
1069
|
+
);
|
|
1070
|
+
}
|
|
1071
|
+
` }],
|
|
1072
|
+
},
|
|
1073
|
+
|
|
1074
|
+
// ─── Hooks ───────────────────────────────────────────────────────────────────
|
|
1075
|
+
|
|
1076
|
+
'use-copy-to-clipboard': {
|
|
1077
|
+
id: 'use-copy-to-clipboard',
|
|
1078
|
+
defaultViewport: 'desktop',
|
|
1079
|
+
preview: React.lazy(() => import('~/demos/use-copy-to-clipboard')),
|
|
1080
|
+
files: [{ path: 'components/CopyButton.tsx', language: 'tsx', code: `import { useCopyToClipboard } from "@olwiba/ui";
|
|
1081
|
+
import { Button } from "@olwiba/cn";
|
|
1082
|
+
import { Check, Copy } from "lucide-react";
|
|
1083
|
+
|
|
1084
|
+
export function CopyButton({ text }: { text: string }) {
|
|
1085
|
+
const [copied, copy] = useCopyToClipboard();
|
|
1086
|
+
|
|
1087
|
+
return (
|
|
1088
|
+
<Button size="sm" variant="outline" onClick={() => copy(text)}>
|
|
1089
|
+
{copied ? <Check className="size-4 text-emerald-500" /> : <Copy className="size-4" />}
|
|
1090
|
+
{copied ? "Copied" : "Copy"}
|
|
1091
|
+
</Button>
|
|
1092
|
+
);
|
|
1093
|
+
}
|
|
1094
|
+
` }],
|
|
1095
|
+
},
|
|
1096
|
+
|
|
1097
|
+
'use-debounce': {
|
|
1098
|
+
id: 'use-debounce',
|
|
1099
|
+
defaultViewport: 'desktop',
|
|
1100
|
+
preview: React.lazy(() => import('~/demos/use-debounce')),
|
|
1101
|
+
files: [{ path: 'components/SearchInput.tsx', language: 'tsx', code: `import { useDebounce } from "@olwiba/ui";
|
|
1102
|
+
import { Input } from "@olwiba/cn";
|
|
1103
|
+
import React from "react";
|
|
1104
|
+
|
|
1105
|
+
export function SearchInput() {
|
|
1106
|
+
const [query, setQuery] = React.useState("");
|
|
1107
|
+
const debounced = useDebounce(query, 400);
|
|
1108
|
+
|
|
1109
|
+
// debounced updates 400ms after the user stops typing
|
|
1110
|
+
console.log("Search for:", debounced);
|
|
1111
|
+
|
|
1112
|
+
return <Input placeholder="Search..." value={query} onChange={(e) => setQuery(e.target.value)} />;
|
|
1113
|
+
}
|
|
1114
|
+
` }],
|
|
1115
|
+
},
|
|
1116
|
+
|
|
1117
|
+
'use-intersection-observer': {
|
|
1118
|
+
id: 'use-intersection-observer',
|
|
1119
|
+
defaultViewport: 'desktop',
|
|
1120
|
+
preview: React.lazy(() => import('~/demos/use-intersection-observer')),
|
|
1121
|
+
files: [{ path: 'components/LazySection.tsx', language: 'tsx', code: `import { useIntersectionObserver } from "@olwiba/ui";
|
|
1122
|
+
|
|
1123
|
+
export function LazySection() {
|
|
1124
|
+
const [ref, isVisible] = useIntersectionObserver({ threshold: 0.5 });
|
|
1125
|
+
|
|
1126
|
+
return (
|
|
1127
|
+
<div ref={ref as React.RefObject<HTMLDivElement>} className={isVisible ? "opacity-100" : "opacity-0"}>
|
|
1128
|
+
{isVisible ? "Now visible!" : "Waiting to enter viewport..."}
|
|
1129
|
+
</div>
|
|
1130
|
+
);
|
|
1131
|
+
}
|
|
1132
|
+
` }],
|
|
1133
|
+
},
|
|
1134
|
+
|
|
1135
|
+
'use-local-storage': {
|
|
1136
|
+
id: 'use-local-storage',
|
|
1137
|
+
defaultViewport: 'desktop',
|
|
1138
|
+
preview: React.lazy(() => import('~/demos/use-local-storage')),
|
|
1139
|
+
files: [{ path: 'components/ThemePicker.tsx', language: 'tsx', code: `import { useLocalStorage } from "@olwiba/ui";
|
|
1140
|
+
|
|
1141
|
+
export function ThemePicker() {
|
|
1142
|
+
const [theme, setTheme] = useLocalStorage<"light" | "dark">("theme", "light");
|
|
1143
|
+
|
|
1144
|
+
return (
|
|
1145
|
+
<button onClick={() => setTheme(theme === "light" ? "dark" : "light")}>
|
|
1146
|
+
Current: {theme}
|
|
1147
|
+
</button>
|
|
1148
|
+
);
|
|
1149
|
+
}
|
|
1150
|
+
` }],
|
|
1151
|
+
},
|
|
1152
|
+
|
|
1153
|
+
'use-media-query': {
|
|
1154
|
+
id: 'use-media-query',
|
|
1155
|
+
defaultViewport: 'desktop',
|
|
1156
|
+
preview: React.lazy(() => import('~/demos/use-media-query')),
|
|
1157
|
+
files: [{ path: 'components/ResponsiveLayout.tsx', language: 'tsx', code: `import { useMediaQuery } from "@olwiba/ui";
|
|
1158
|
+
|
|
1159
|
+
export function ResponsiveLayout() {
|
|
1160
|
+
const isDesktop = useMediaQuery("(min-width: 1024px)");
|
|
1161
|
+
const prefersDark = useMediaQuery("(prefers-color-scheme: dark)");
|
|
1162
|
+
|
|
1163
|
+
return (
|
|
1164
|
+
<div>
|
|
1165
|
+
<p>{isDesktop ? "Desktop layout" : "Mobile layout"}</p>
|
|
1166
|
+
<p>{prefersDark ? "Dark mode" : "Light mode"}</p>
|
|
1167
|
+
</div>
|
|
1168
|
+
);
|
|
1169
|
+
}
|
|
1170
|
+
` }],
|
|
1171
|
+
},
|
|
1172
|
+
|
|
1173
|
+
'use-pagination': {
|
|
1174
|
+
id: 'use-pagination',
|
|
1175
|
+
defaultViewport: 'desktop',
|
|
1176
|
+
preview: React.lazy(() => import('~/demos/use-pagination')),
|
|
1177
|
+
files: [{ path: 'components/PaginatedList.tsx', language: 'tsx', code: `import { usePagination } from "@olwiba/ui";
|
|
1178
|
+
import { Button } from "@olwiba/cn";
|
|
1179
|
+
|
|
1180
|
+
const ITEMS = Array.from({ length: 20 }, (_, i) => \`Item \${i + 1}\`);
|
|
1181
|
+
|
|
1182
|
+
export function PaginatedList() {
|
|
1183
|
+
const { page, totalPages, offset, pageSize, hasPrev, hasNext, prev, next } = usePagination(ITEMS.length, 5);
|
|
1184
|
+
const visible = ITEMS.slice(offset, offset + pageSize);
|
|
1185
|
+
|
|
1186
|
+
return (
|
|
1187
|
+
<div className="space-y-2">
|
|
1188
|
+
{visible.map((item) => <div key={item} className="rounded border p-2 text-sm">{item}</div>)}
|
|
1189
|
+
<div className="flex gap-2">
|
|
1190
|
+
<Button size="sm" variant="outline" disabled={!hasPrev} onClick={prev}>Prev</Button>
|
|
1191
|
+
<span className="text-sm self-center">{page} / {totalPages}</span>
|
|
1192
|
+
<Button size="sm" variant="outline" disabled={!hasNext} onClick={next}>Next</Button>
|
|
1193
|
+
</div>
|
|
1194
|
+
</div>
|
|
1195
|
+
);
|
|
1196
|
+
}
|
|
1197
|
+
` }],
|
|
1198
|
+
},
|
|
1199
|
+
|
|
1200
|
+
'data-table': {
|
|
1201
|
+
id: 'data-table',
|
|
1202
|
+
title: 'DataTable',
|
|
1203
|
+
defaultViewport: 'desktop',
|
|
1204
|
+
preview: React.lazy(() => import('~/demos/data-table')),
|
|
1205
|
+
files: [{ path: 'components/ProjectsTable.tsx', language: 'tsx', code: `import type { ColumnDef } from "@tanstack/react-table";
|
|
1206
|
+
import { DataTable } from "@olwiba/ui";
|
|
1207
|
+
import { Badge, Button } from "@olwiba/cn";
|
|
1208
|
+
|
|
1209
|
+
interface Project {
|
|
1210
|
+
id: string;
|
|
1211
|
+
name: string;
|
|
1212
|
+
status: "live" | "building" | "paused";
|
|
1213
|
+
updatedAt: string;
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
const columns: ColumnDef<Project>[] = [
|
|
1217
|
+
{ accessorKey: "name", header: "Project" },
|
|
1218
|
+
{
|
|
1219
|
+
accessorKey: "status",
|
|
1220
|
+
header: "Status",
|
|
1221
|
+
cell: ({ row }) => <Badge className="capitalize">{row.original.status}</Badge>,
|
|
1222
|
+
},
|
|
1223
|
+
{ accessorKey: "updatedAt", header: "Updated" },
|
|
1224
|
+
];
|
|
1225
|
+
|
|
1226
|
+
export function ProjectsTable({ projects }: { projects: Project[] }) {
|
|
1227
|
+
return (
|
|
1228
|
+
<DataTable
|
|
1229
|
+
columns={columns}
|
|
1230
|
+
data={projects}
|
|
1231
|
+
searchKey="name"
|
|
1232
|
+
toolbar={<Button size="sm">New project</Button>}
|
|
1233
|
+
/>
|
|
1234
|
+
);
|
|
1235
|
+
}
|
|
1236
|
+
` }],
|
|
1237
|
+
},
|
|
1238
|
+
|
|
1239
|
+
'file-upload': {
|
|
1240
|
+
id: 'file-upload',
|
|
1241
|
+
title: 'FileUpload',
|
|
1242
|
+
defaultViewport: 'desktop',
|
|
1243
|
+
preview: React.lazy(() => import('~/demos/file-upload')),
|
|
1244
|
+
files: [{ path: 'components/AvatarUpload.tsx', language: 'tsx', code: `import { FileUpload } from "@olwiba/ui";
|
|
1245
|
+
|
|
1246
|
+
export function AvatarUpload() {
|
|
1247
|
+
return (
|
|
1248
|
+
<FileUpload
|
|
1249
|
+
multiple
|
|
1250
|
+
accept="image/png,image/jpeg"
|
|
1251
|
+
maxSizeMb={5}
|
|
1252
|
+
maxFiles={4}
|
|
1253
|
+
hint="PNG or JPG, up to 5MB each"
|
|
1254
|
+
onFilesAdded={(files) => uploadFiles(files)}
|
|
1255
|
+
/>
|
|
1256
|
+
);
|
|
1257
|
+
}
|
|
1258
|
+
` }],
|
|
1259
|
+
},
|
|
1260
|
+
|
|
1261
|
+
'command-menu': {
|
|
1262
|
+
id: 'command-menu',
|
|
1263
|
+
title: 'CommandMenu',
|
|
1264
|
+
defaultViewport: 'desktop',
|
|
1265
|
+
preview: React.lazy(() => import('~/demos/command-menu')),
|
|
1266
|
+
files: [{ path: 'components/GlobalSearch.tsx', language: 'tsx', code: `import { CommandMenu } from "@olwiba/ui";
|
|
1267
|
+
import { LayoutDashboard, CreditCard, Settings } from "lucide-react";
|
|
1268
|
+
|
|
1269
|
+
export function GlobalSearch() {
|
|
1270
|
+
return (
|
|
1271
|
+
<CommandMenu
|
|
1272
|
+
groups={[
|
|
1273
|
+
{
|
|
1274
|
+
heading: "Navigate",
|
|
1275
|
+
items: [
|
|
1276
|
+
{ id: "dashboard", label: "Dashboard", icon: LayoutDashboard, onSelect: () => router.push("/dashboard") },
|
|
1277
|
+
{ id: "billing", label: "Billing", icon: CreditCard, onSelect: () => router.push("/billing") },
|
|
1278
|
+
{ id: "settings", label: "Settings", icon: Settings, onSelect: () => router.push("/settings") },
|
|
1279
|
+
],
|
|
1280
|
+
},
|
|
1281
|
+
]}
|
|
1282
|
+
/>
|
|
1283
|
+
);
|
|
1284
|
+
}
|
|
1285
|
+
` }],
|
|
1286
|
+
},
|
|
1287
|
+
|
|
1288
|
+
notify: {
|
|
1289
|
+
id: 'notify',
|
|
1290
|
+
title: 'notify',
|
|
1291
|
+
defaultViewport: 'desktop',
|
|
1292
|
+
preview: React.lazy(() => import('~/demos/notify')),
|
|
1293
|
+
files: [{ path: 'components/DeployButton.tsx', language: 'tsx', code: `import { notify } from "@olwiba/ui";
|
|
1294
|
+
import { Toaster, Button } from "@olwiba/cn";
|
|
1295
|
+
|
|
1296
|
+
export function DeployButton() {
|
|
1297
|
+
return (
|
|
1298
|
+
<>
|
|
1299
|
+
<Button onClick={() => notify({
|
|
1300
|
+
variant: "success",
|
|
1301
|
+
title: "Deploy complete",
|
|
1302
|
+
description: "Your project is live.",
|
|
1303
|
+
})}>
|
|
1304
|
+
Deploy
|
|
1305
|
+
</Button>
|
|
1306
|
+
<Toaster />
|
|
1307
|
+
</>
|
|
1308
|
+
);
|
|
1309
|
+
}
|
|
1310
|
+
` }],
|
|
1311
|
+
},
|
|
1312
|
+
|
|
1313
|
+
'settings-section': {
|
|
1314
|
+
id: 'settings-section',
|
|
1315
|
+
title: 'SettingsSection',
|
|
1316
|
+
defaultViewport: 'desktop',
|
|
1317
|
+
preview: React.lazy(() => import('~/demos/settings-section')),
|
|
1318
|
+
files: [{ path: 'app/settings/page.tsx', language: 'tsx', code: `import { SettingsSection } from "@olwiba/ui";
|
|
1319
|
+
import { Button } from "@olwiba/cn";
|
|
1320
|
+
|
|
1321
|
+
export default function SettingsPage() {
|
|
1322
|
+
return (
|
|
1323
|
+
<div className="divide-y divide-border">
|
|
1324
|
+
<SettingsSection title="Profile" description="This appears on your public profile.">
|
|
1325
|
+
<MyProfileForm />
|
|
1326
|
+
</SettingsSection>
|
|
1327
|
+
<SettingsSection title="Delete workspace" description="This cannot be undone." danger>
|
|
1328
|
+
<Button variant="destructive">Delete workspace</Button>
|
|
1329
|
+
</SettingsSection>
|
|
1330
|
+
</div>
|
|
1331
|
+
);
|
|
1332
|
+
}
|
|
1333
|
+
` }],
|
|
1334
|
+
},
|
|
1335
|
+
|
|
1336
|
+
'team-members-panel': {
|
|
1337
|
+
id: 'team-members-panel',
|
|
1338
|
+
title: 'TeamMembersPanel',
|
|
1339
|
+
defaultViewport: 'desktop',
|
|
1340
|
+
preview: React.lazy(() => import('~/demos/team-members-panel')),
|
|
1341
|
+
files: [{ path: 'app/team/page.tsx', language: 'tsx', code: `import { TeamMembersPanel } from "@olwiba/ui";
|
|
1342
|
+
|
|
1343
|
+
export default function TeamPage({ members }: { members: TeamMemberRecord[] }) {
|
|
1344
|
+
return (
|
|
1345
|
+
<TeamMembersPanel
|
|
1346
|
+
members={members}
|
|
1347
|
+
onInvite={(email, role) => inviteMember(email, role)}
|
|
1348
|
+
onRoleChange={(id, role) => updateRole(id, role)}
|
|
1349
|
+
onRemove={(id) => removeMember(id)}
|
|
1350
|
+
/>
|
|
1351
|
+
);
|
|
1352
|
+
}
|
|
1353
|
+
` }],
|
|
1354
|
+
},
|
|
1355
|
+
|
|
1356
|
+
'update-banner': {
|
|
1357
|
+
id: 'update-banner',
|
|
1358
|
+
title: 'UpdateBanner',
|
|
1359
|
+
defaultViewport: 'desktop',
|
|
1360
|
+
preview: React.lazy(() => import('~/demos/update-banner')),
|
|
1361
|
+
files: [
|
|
1362
|
+
{
|
|
1363
|
+
path: 'app/layout.tsx',
|
|
1364
|
+
language: 'tsx',
|
|
1365
|
+
code: `import { UpdateBanner } from "@olwiba/ui";
|
|
1366
|
+
|
|
1367
|
+
declare const __BUILD_VERSION__: string;
|
|
1368
|
+
|
|
1369
|
+
export function RootLayout({ children }) {
|
|
1370
|
+
return (
|
|
1371
|
+
<>
|
|
1372
|
+
{children}
|
|
1373
|
+
<UpdateBanner
|
|
1374
|
+
currentVersion={__BUILD_VERSION__}
|
|
1375
|
+
fetchVersion={async () => {
|
|
1376
|
+
const res = await fetch("/api/version", { cache: "no-store" });
|
|
1377
|
+
return res.json();
|
|
1378
|
+
}}
|
|
1379
|
+
/>
|
|
1380
|
+
</>
|
|
1381
|
+
);
|
|
1382
|
+
}
|
|
1383
|
+
`,
|
|
1384
|
+
},
|
|
1385
|
+
],
|
|
1386
|
+
},
|
|
1387
|
+
'billing-panel': {
|
|
1388
|
+
id: 'billing-panel',
|
|
1389
|
+
title: 'BillingPanel',
|
|
1390
|
+
defaultViewport: 'desktop',
|
|
1391
|
+
preview: React.lazy(() => import('~/demos/billing-panel')),
|
|
1392
|
+
files: [{ path: 'app/billing/page.tsx', language: 'tsx', code: `import { BillingPanel } from "@olwiba/ui";
|
|
1393
|
+
|
|
1394
|
+
export default function BillingPage() {
|
|
1395
|
+
return (
|
|
1396
|
+
<BillingPanel
|
|
1397
|
+
planName="Pro"
|
|
1398
|
+
planPrice="$29/mo"
|
|
1399
|
+
renewalDate="March 12, 2026"
|
|
1400
|
+
usage={[{ label: "Seats", used: 6, limit: 10 }]}
|
|
1401
|
+
paymentMethod={{ brand: "Visa", last4: "4242", expiry: "08/27" }}
|
|
1402
|
+
invoices={invoices}
|
|
1403
|
+
onManagePlan={() => openPlanDialog()}
|
|
1404
|
+
/>
|
|
1405
|
+
);
|
|
1406
|
+
}
|
|
1407
|
+
` }],
|
|
1408
|
+
},
|
|
1409
|
+
|
|
1410
|
+
'onboarding-wizard': {
|
|
1411
|
+
id: 'onboarding-wizard',
|
|
1412
|
+
title: 'OnboardingWizard',
|
|
1413
|
+
defaultViewport: 'desktop',
|
|
1414
|
+
preview: React.lazy(() => import('~/demos/onboarding-wizard')),
|
|
1415
|
+
files: [{ path: 'app/onboarding/page.tsx', language: 'tsx', code: `import { OnboardingWizard } from "@olwiba/ui";
|
|
1416
|
+
|
|
1417
|
+
export default function OnboardingPage() {
|
|
1418
|
+
return (
|
|
1419
|
+
<OnboardingWizard
|
|
1420
|
+
onComplete={() => router.push("/dashboard")}
|
|
1421
|
+
steps={[
|
|
1422
|
+
{ id: "welcome", title: "Welcome", content: <WelcomeStep /> },
|
|
1423
|
+
{
|
|
1424
|
+
id: "workspace",
|
|
1425
|
+
title: "Name your workspace",
|
|
1426
|
+
content: <WorkspaceNameStep />,
|
|
1427
|
+
onNext: () => (name.trim() ? true : "Give your workspace a name to continue."),
|
|
1428
|
+
},
|
|
1429
|
+
]}
|
|
1430
|
+
/>
|
|
1431
|
+
);
|
|
1432
|
+
}
|
|
1433
|
+
` }],
|
|
1434
|
+
},
|
|
1435
|
+
|
|
1436
|
+
'action-email': {
|
|
1437
|
+
id: 'action-email',
|
|
1438
|
+
title: 'Action email',
|
|
1439
|
+
defaultViewport: 'desktop',
|
|
1440
|
+
preview: React.lazy(() => import('~/demos/action-email')),
|
|
1441
|
+
files: [{ path: 'emails/sign-in.tsx', language: 'tsx', code: `import { ActionEmail } from "@olwiba/ui/email";
|
|
1442
|
+
import { render } from "@react-email/render";
|
|
1443
|
+
|
|
1444
|
+
export async function signInEmail(url: string) {
|
|
1445
|
+
return render(
|
|
1446
|
+
<ActionEmail
|
|
1447
|
+
appName="Nexus Inc"
|
|
1448
|
+
preview="Sign in to Nexus Inc"
|
|
1449
|
+
heading="Sign in with one click"
|
|
1450
|
+
description="This link expires in 15 minutes and can only be used once."
|
|
1451
|
+
actionLabel="Sign in"
|
|
1452
|
+
actionUrl={url}
|
|
1453
|
+
/>,
|
|
1454
|
+
);
|
|
1455
|
+
}
|
|
1456
|
+
` }],
|
|
1457
|
+
},
|
|
1458
|
+
|
|
1459
|
+
'notice-email': {
|
|
1460
|
+
id: 'notice-email',
|
|
1461
|
+
title: 'Notice email',
|
|
1462
|
+
defaultViewport: 'desktop',
|
|
1463
|
+
preview: React.lazy(() => import('~/demos/notice-email')),
|
|
1464
|
+
files: [{ path: 'emails/export-ready.tsx', language: 'tsx', code: `import { NoticeEmail } from "@olwiba/ui/email";
|
|
1465
|
+
import { render } from "@react-email/render";
|
|
1466
|
+
|
|
1467
|
+
export function exportReadyEmail() {
|
|
1468
|
+
return render(
|
|
1469
|
+
<NoticeEmail
|
|
1470
|
+
appName="Nexus Inc"
|
|
1471
|
+
preview="Your export is ready"
|
|
1472
|
+
heading="Your export is ready"
|
|
1473
|
+
body="The workspace export you requested has finished."
|
|
1474
|
+
/>,
|
|
1475
|
+
);
|
|
1476
|
+
}
|
|
1477
|
+
` }],
|
|
1478
|
+
},
|
|
1479
|
+
|
|
1480
|
+
'email-layout': {
|
|
1481
|
+
id: 'email-layout',
|
|
1482
|
+
title: 'Email layout',
|
|
1483
|
+
defaultViewport: 'desktop',
|
|
1484
|
+
preview: React.lazy(() => import('~/demos/email-layout')),
|
|
1485
|
+
files: [{ path: 'emails/custom.tsx', language: 'tsx', code: `import { EmailLayout } from "@olwiba/ui/email";
|
|
1486
|
+
import { EmailText } from "@olwiba/cn/email";
|
|
1487
|
+
|
|
1488
|
+
export function CustomEmail() {
|
|
1489
|
+
return (
|
|
1490
|
+
<EmailLayout appName="Nexus Inc" preview="A message from Nexus Inc">
|
|
1491
|
+
<EmailText>Anything here sits inside the branded shell.</EmailText>
|
|
1492
|
+
</EmailLayout>
|
|
1493
|
+
);
|
|
1494
|
+
}
|
|
1495
|
+
` }],
|
|
1496
|
+
},
|
|
1497
|
+
|
|
1498
|
+
'email-link-fallback': {
|
|
1499
|
+
id: 'email-link-fallback',
|
|
1500
|
+
title: 'Email link fallback',
|
|
1501
|
+
defaultViewport: 'desktop',
|
|
1502
|
+
preview: React.lazy(() => import('~/demos/email-link-fallback')),
|
|
1503
|
+
files: [{ path: 'emails/with-fallback.tsx', language: 'tsx', code: `import { EmailLayout, EmailLinkFallback } from "@olwiba/ui/email";
|
|
1504
|
+
|
|
1505
|
+
export function WithFallback({ url }: { url: string }) {
|
|
1506
|
+
return (
|
|
1507
|
+
<EmailLayout appName="Nexus Inc" preview="Sign in to Nexus Inc">
|
|
1508
|
+
{/* your own heading and button */}
|
|
1509
|
+
<EmailLinkFallback actionUrl={url} />
|
|
1510
|
+
</EmailLayout>
|
|
1511
|
+
);
|
|
1512
|
+
}
|
|
1513
|
+
` }],
|
|
1514
|
+
},
|
|
1515
|
+
|
|
1516
|
+
'post-list': {
|
|
1517
|
+
id: 'post-list',
|
|
1518
|
+
title: 'Post list',
|
|
1519
|
+
defaultViewport: 'desktop',
|
|
1520
|
+
preview: React.lazy(() => import('~/demos/post-list')),
|
|
1521
|
+
files: [{ path: 'components/blog-index.tsx', language: 'tsx', code: `import { PostList } from "@olwiba/ui";
|
|
1522
|
+
|
|
1523
|
+
const posts = [
|
|
1524
|
+
{
|
|
1525
|
+
title: "A calmer path from idea to release",
|
|
1526
|
+
description: "How a small set of product conventions helps teams ship without redesigning every page.",
|
|
1527
|
+
date: "2026-08-24",
|
|
1528
|
+
slug: "calmer-path-to-release",
|
|
1529
|
+
tags: ["Product"],
|
|
1530
|
+
},
|
|
1531
|
+
];
|
|
1532
|
+
|
|
1533
|
+
export function BlogIndex() {
|
|
1534
|
+
return <PostList posts={posts} />;
|
|
1535
|
+
}
|
|
1536
|
+
` }],
|
|
1537
|
+
},
|
|
1538
|
+
|
|
1539
|
+
carousel: {
|
|
1540
|
+
id: 'carousel',
|
|
1541
|
+
title: 'Carousel',
|
|
1542
|
+
defaultViewport: 'desktop',
|
|
1543
|
+
preview: React.lazy(() => import('~/demos/carousel')),
|
|
1544
|
+
files: [{ path: 'components/feature-carousel.tsx', language: 'tsx', code: `import { Carousel, FeatureCard } from "@olwiba/ui";
|
|
1545
|
+
|
|
1546
|
+
export function FeatureCarousel({ features }) {
|
|
1547
|
+
return (
|
|
1548
|
+
<Carousel ariaLabel="Platform features">
|
|
1549
|
+
{features.map((f) => (
|
|
1550
|
+
<FeatureCard key={f.title} icon={f.icon} title={f.title} description={f.description} />
|
|
1551
|
+
))}
|
|
1552
|
+
</Carousel>
|
|
1553
|
+
);
|
|
1554
|
+
}
|
|
1555
|
+
` }],
|
|
1556
|
+
},
|
|
1557
|
+
|
|
1558
|
+
'notifications-popover': {
|
|
1559
|
+
id: 'notifications-popover',
|
|
1560
|
+
title: 'NotificationsPopover',
|
|
1561
|
+
defaultViewport: 'desktop',
|
|
1562
|
+
preview: React.lazy(() => import('~/demos/notifications-popover')),
|
|
1563
|
+
files: [{ path: 'components/header-notifications.tsx', language: 'tsx', code: `import { NotificationsPopover } from "@olwiba/ui";
|
|
1564
|
+
|
|
1565
|
+
export function HeaderNotifications({ notifications }) {
|
|
1566
|
+
return (
|
|
1567
|
+
<NotificationsPopover
|
|
1568
|
+
notifications={notifications}
|
|
1569
|
+
onMarkAllRead={() => markAllRead()}
|
|
1570
|
+
onNotificationClick={(n) => markRead(n.id)}
|
|
1571
|
+
/>
|
|
1572
|
+
);
|
|
1573
|
+
}
|
|
1574
|
+
` }],
|
|
1575
|
+
},
|
|
1576
|
+
|
|
1577
|
+
'activity-feed': {
|
|
1578
|
+
id: 'activity-feed',
|
|
1579
|
+
title: 'ActivityFeed',
|
|
1580
|
+
defaultViewport: 'desktop',
|
|
1581
|
+
preview: React.lazy(() => import('~/demos/activity-feed')),
|
|
1582
|
+
files: [{ path: 'app/dashboard/activity.tsx', language: 'tsx', code: `import { ActivityFeed } from "@olwiba/ui";
|
|
1583
|
+
import { GitCommit } from "lucide-react";
|
|
1584
|
+
|
|
1585
|
+
export function RecentActivity({ events }) {
|
|
1586
|
+
return (
|
|
1587
|
+
<ActivityFeed
|
|
1588
|
+
items={events.map((e) => ({
|
|
1589
|
+
id: e.id,
|
|
1590
|
+
title: <><b>{e.actor}</b> {e.action}</>,
|
|
1591
|
+
description: e.detail,
|
|
1592
|
+
timestamp: e.when,
|
|
1593
|
+
avatar: e.avatarUrl,
|
|
1594
|
+
icon: <GitCommit />,
|
|
1595
|
+
}))}
|
|
1596
|
+
/>
|
|
1597
|
+
);
|
|
1598
|
+
}
|
|
1599
|
+
` }],
|
|
1600
|
+
},
|
|
1601
|
+
chart: {
|
|
1602
|
+
id: 'chart',
|
|
1603
|
+
title: 'Chart',
|
|
1604
|
+
defaultViewport: 'desktop',
|
|
1605
|
+
preview: React.lazy(() => import('~/demos/chart')),
|
|
1606
|
+
files: [{ path: 'app/dashboard/revenue-chart.tsx', language: 'tsx', code: `import { Chart } from "@olwiba/ui";
|
|
1607
|
+
|
|
1608
|
+
export function RevenueChart({ revenue }) {
|
|
1609
|
+
return (
|
|
1610
|
+
<Chart
|
|
1611
|
+
type="line"
|
|
1612
|
+
data={revenue}
|
|
1613
|
+
xKey="month"
|
|
1614
|
+
series={[
|
|
1615
|
+
{ key: "mrr", label: "MRR" },
|
|
1616
|
+
{ key: "costs", label: "Costs" },
|
|
1617
|
+
]}
|
|
1618
|
+
valueFormatter={(v) => \`$\${v.toLocaleString()}\`}
|
|
1619
|
+
/>
|
|
1620
|
+
);
|
|
1621
|
+
}
|
|
1622
|
+
` }],
|
|
1623
|
+
},
|
|
1624
|
+
|
|
1625
|
+
sortable: {
|
|
1626
|
+
id: 'sortable',
|
|
1627
|
+
title: 'Sortable',
|
|
1628
|
+
defaultViewport: 'desktop',
|
|
1629
|
+
preview: React.lazy(() => import('~/demos/sortable')),
|
|
1630
|
+
files: [{ path: 'components/task-list.tsx', language: 'tsx', code: `import { Sortable } from "@olwiba/ui";
|
|
1631
|
+
|
|
1632
|
+
export function TaskList({ tasks }) {
|
|
1633
|
+
const [order, setOrder] = React.useState(tasks.map((t) => t.id));
|
|
1634
|
+
|
|
1635
|
+
return (
|
|
1636
|
+
<Sortable items={order} onReorder={setOrder}>
|
|
1637
|
+
{order.map((id) => (
|
|
1638
|
+
<TaskRow key={id} task={tasks.find((t) => t.id === id)} />
|
|
1639
|
+
))}
|
|
1640
|
+
</Sortable>
|
|
1641
|
+
);
|
|
1642
|
+
}
|
|
1643
|
+
` }],
|
|
1644
|
+
},
|
|
1645
|
+
};
|
|
1646
|
+
|
|
1647
|
+
registerSandboxes(uiSandboxes);
|