@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.
@@ -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);