@olwiba/ui 0.2.26 → 0.2.28

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