create-swift-rust 0.4.0 → 0.7.1

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.
Files changed (65) hide show
  1. package/dist/index.js +74 -938
  2. package/dist/index.js.map +1 -1
  3. package/package.json +6 -4
  4. package/templates/default/README.md +29 -0
  5. package/templates/default/_env.example +5 -0
  6. package/templates/default/_env.local +5 -0
  7. package/templates/default/_gitignore +14 -0
  8. package/templates/default/biome.json +12 -0
  9. package/templates/default/globals.d.ts +19 -0
  10. package/templates/default/package.json +30 -0
  11. package/templates/default/postcss.config.mjs +7 -0
  12. package/templates/default/public/blog/introducing-swift-rust.svg +12 -0
  13. package/templates/default/public/og-image.svg +25 -0
  14. package/templates/default/public/samples/axelborg-towers-modern-architecture.jpg +0 -0
  15. package/templates/default/public/samples/brown-high-skyscrapers.jpg +0 -0
  16. package/templates/default/public/samples/close-up-portrait-gorgeous-young-woman.jpg +0 -0
  17. package/templates/default/public/samples/full-shot-woman-taking-selfie.jpg +0 -0
  18. package/templates/default/public/samples/happy-black-parents-with-kids-making-video-call-smart-phone-home.jpg +0 -0
  19. package/templates/default/public/samples/metaverse-concept-collage-design.jpg +0 -0
  20. package/templates/default/src/app/about/layout.tsx +5 -0
  21. package/templates/default/src/app/about/page.tsx +74 -0
  22. package/templates/default/src/app/about/team/page.tsx +88 -0
  23. package/templates/default/src/app/api/contact/route.ts +15 -0
  24. package/templates/default/src/app/api/health/route.ts +7 -0
  25. package/templates/default/src/app/api/posts/[id]/route.ts +33 -0
  26. package/templates/default/src/app/api/posts/route.ts +29 -0
  27. package/templates/default/src/app/api/search/route.ts +15 -0
  28. package/templates/default/src/app/api/stats/route.ts +16 -0
  29. package/templates/default/src/app/blog/[slug]/page.tsx +116 -0
  30. package/templates/default/src/app/blog/page.tsx +115 -0
  31. package/templates/default/src/app/blog/tag/[tag]/page.tsx +82 -0
  32. package/templates/default/src/app/contact/page.tsx +131 -0
  33. package/templates/default/src/app/dashboard/analytics/page.tsx +103 -0
  34. package/templates/default/src/app/dashboard/layout.tsx +11 -0
  35. package/templates/default/src/app/dashboard/loading.tsx +18 -0
  36. package/templates/default/src/app/dashboard/page.tsx +181 -0
  37. package/templates/default/src/app/dashboard/posts/[id]/not-found.tsx +23 -0
  38. package/templates/default/src/app/dashboard/posts/[id]/page.tsx +87 -0
  39. package/templates/default/src/app/dashboard/posts/new/page.tsx +103 -0
  40. package/templates/default/src/app/dashboard/posts/page.tsx +81 -0
  41. package/templates/default/src/app/dashboard/settings/page.tsx +119 -0
  42. package/templates/default/src/app/error.tsx +42 -0
  43. package/templates/default/src/app/favicon.ico +0 -0
  44. package/templates/default/src/app/favicon.svg +8 -0
  45. package/templates/default/src/app/fonts/page.tsx +610 -0
  46. package/templates/default/src/app/globals.css +238 -0
  47. package/templates/default/src/app/images/page.tsx +56 -0
  48. package/templates/default/src/app/layout.tsx +44 -0
  49. package/templates/default/src/app/loading.tsx +13 -0
  50. package/templates/default/src/app/not-found.tsx +25 -0
  51. package/templates/default/src/app/page.tsx +410 -0
  52. package/templates/default/src/app/videos/page.tsx +182 -0
  53. package/templates/default/src/components/font-studio.tsx +378 -0
  54. package/templates/default/src/components/footer.tsx +75 -0
  55. package/templates/default/src/components/logo.tsx +26 -0
  56. package/templates/default/src/components/nav.tsx +102 -0
  57. package/templates/default/src/components/sidebar.tsx +109 -0
  58. package/templates/default/src/lib/blur.ts +5 -0
  59. package/templates/default/src/lib/format.ts +31 -0
  60. package/templates/default/src/lib/posts.ts +132 -0
  61. package/templates/default/src/lib/site.config.ts +39 -0
  62. package/templates/default/src/lib/stats.ts +25 -0
  63. package/templates/default/swift-rust.config.json +8 -0
  64. package/templates/default/tsconfig.json +27 -0
  65. package/templates/default/vercel.json +23 -0
@@ -0,0 +1,410 @@
1
+ import { posts } from "@/lib/posts";
2
+ import { Link } from "swift-rust";
3
+ import { siteConfig } from "@/lib/site.config";
4
+
5
+ const RENDERERS = [
6
+ {
7
+ name: "SSR + WASM",
8
+ description: "Server-rendered HTML, hydrated with WebAssembly islands.",
9
+ badge: "Default",
10
+ },
11
+ {
12
+ name: "SSR only",
13
+ description: "Server-rendered HTML. No client JavaScript at all.",
14
+ },
15
+ {
16
+ name: "SSR + HTMX",
17
+ description: "Progressive enhancement with HTMX-style interactions.",
18
+ },
19
+ {
20
+ name: "Full WASM SPA",
21
+ description: "Single-page app compiled entirely to WebAssembly.",
22
+ },
23
+ ];
24
+
25
+ const FEATURES = [
26
+ {
27
+ title: "Streaming SSR",
28
+ description: "Async components, Suspense boundaries, and edge-ready streaming out of the box.",
29
+ icon: IconBolt,
30
+ },
31
+ {
32
+ title: "JSX / TSX first",
33
+ description:
34
+ "Write your UI in TypeScript with first-class types. No DSL, no config gymnastics.",
35
+ icon: IconCode,
36
+ },
37
+ {
38
+ title: "App router",
39
+ description:
40
+ "File-system routing, nested layouts, dynamic params, and special files you already know.",
41
+ icon: IconLayers,
42
+ },
43
+ {
44
+ title: "Built-in components",
45
+ description: "Image, Font, and PDF components with automatic optimization, like you expect.",
46
+ icon: IconBox,
47
+ },
48
+ {
49
+ title: "Four render modes",
50
+ description: "Pick the right rendering strategy per project, not per page.",
51
+ icon: IconGrid,
52
+ },
53
+ {
54
+ title: "Single binary",
55
+ description:
56
+ "Compile your app to a single Rust binary. No Node, no edge runtime, no surprises.",
57
+ icon: IconBinary,
58
+ },
59
+ ];
60
+
61
+ export default function HomePage() {
62
+ const recentPosts = posts.slice(0, 3);
63
+
64
+ return (
65
+ <>
66
+ <section className="relative overflow-hidden">
67
+ <div className="container-page pt-20 pb-24 sm:pt-28 sm:pb-32">
68
+ <div className="mx-auto max-w-3xl text-center">
69
+ <Link
70
+ href="/blog/introducing-swift-rust"
71
+ className="badge badge-accent mb-8 inline-flex hover:opacity-90"
72
+ >
73
+ <span className="pulse-dot h-1.5 w-1.5 rounded-full bg-current" />
74
+ v{siteConfig.version} — first public preview
75
+ <svg
76
+ viewBox="0 0 24 24"
77
+ className="h-3 w-3"
78
+ fill="none"
79
+ stroke="currentColor"
80
+ strokeWidth="2"
81
+ >
82
+ <path d="M5 12h14M13 6l6 6-6 6" strokeLinecap="round" strokeLinejoin="round" />
83
+ </svg>
84
+ </Link>
85
+ <h1 className="text-balance text-5xl font-semibold tracking-tight text-[var(--color-fg)] sm:text-6xl md:text-7xl">
86
+ The React framework
87
+ <br />
88
+ <span className="text-[var(--color-accent)]">Powered</span>{" "}
89
+ <span className="text-[var(--color-fg-muted)]">with Rust +</span>{" "}
90
+ <span className="text-[var(--color-accent)]">Bun</span>.
91
+ </h1>
92
+ <p className="mx-auto mt-7 max-w-xl text-pretty text-lg leading-relaxed text-[var(--color-fg-muted)]">
93
+ TSX, streaming SSR, file-system routing, and a single binary at deploy.{" "}
94
+ <span className="font-semibold text-[var(--color-fg)]">10x faster than Next.js.</span>{" "}
95
+ Familiar to anyone who has used Next.js. Familiar to nobody else.
96
+ </p>
97
+ <div className="mt-10 flex flex-wrap items-center justify-center gap-3">
98
+ <Link href={siteConfig.docsUrl} className="btn btn-accent btn-lg">
99
+ Get started
100
+ <svg
101
+ viewBox="0 0 24 24"
102
+ className="h-4 w-4"
103
+ fill="none"
104
+ stroke="currentColor"
105
+ strokeWidth="2"
106
+ >
107
+ <path d="M5 12h14M13 6l6 6-6 6" strokeLinecap="round" strokeLinejoin="round" />
108
+ </svg>
109
+ </Link>
110
+ <Link href={siteConfig.githubUrl} className="btn btn-outline btn-lg">
111
+ <svg viewBox="0 0 24 24" className="h-4 w-4" fill="currentColor">
112
+ <path d="M12 .5C5.65.5.5 5.65.5 12c0 5.08 3.29 9.39 7.86 10.91.58.11.79-.25.79-.56 0-.27-.01-1.16-.02-2.11-3.2.7-3.88-1.36-3.88-1.36-.52-1.33-1.28-1.69-1.28-1.69-1.05-.72.08-.71.08-.71 1.16.08 1.77 1.19 1.77 1.19 1.03 1.77 2.71 1.26 3.37.96.1-.75.4-1.26.73-1.55-2.55-.29-5.24-1.28-5.24-5.69 0-1.26.45-2.29 1.18-3.1-.12-.29-.51-1.46.11-3.04 0 0 .97-.31 3.18 1.18a11 11 0 0 1 5.79 0c2.21-1.49 3.18-1.18 3.18-1.18.62 1.58.23 2.75.11 3.04.74.81 1.18 1.84 1.18 3.1 0 4.42-2.69 5.4-5.25 5.68.41.36.78 1.05.78 2.12 0 1.53-.01 2.77-.01 3.14 0 .31.21.68.8.56C20.21 21.38 23.5 17.08 23.5 12 23.5 5.65 18.35.5 12 .5Z" />
113
+ </svg>
114
+ Star on GitHub
115
+ </Link>
116
+ </div>
117
+ </div>
118
+
119
+ <CodePreview />
120
+ </div>
121
+ </section>
122
+
123
+ <section className="border-t border-[var(--color-border)] bg-[var(--color-surface)] py-24">
124
+ <div className="container-page">
125
+ <div className="mx-auto max-w-2xl text-center">
126
+ <h2 className="text-3xl font-semibold tracking-tight sm:text-4xl">
127
+ Four ways to render.
128
+ </h2>
129
+ <p className="mt-4 text-pretty text-[var(--color-fg-muted)]">
130
+ Pick a mode when you scaffold. Change it later in one line of config.
131
+ </p>
132
+ </div>
133
+ <div className="mt-16 grid gap-px overflow-hidden rounded-2xl border border-[var(--color-border)] bg-[var(--color-border)] sm:grid-cols-2 lg:grid-cols-4">
134
+ {RENDERERS.map((r) => (
135
+ <div key={r.name} className="flex flex-col gap-2 bg-[var(--color-surface)] p-6">
136
+ <div className="flex items-center justify-between">
137
+ <h3 className="text-[0.95rem] font-semibold text-[var(--color-fg)]">{r.name}</h3>
138
+ {r.badge ? <span className="badge badge-accent">{r.badge}</span> : null}
139
+ </div>
140
+ <p className="text-[0.875rem] leading-relaxed text-[var(--color-fg-muted)]">
141
+ {r.description}
142
+ </p>
143
+ </div>
144
+ ))}
145
+ </div>
146
+ </div>
147
+ </section>
148
+
149
+ <section className="py-24">
150
+ <div className="container-page">
151
+ <div className="mx-auto max-w-2xl text-center">
152
+ <h2 className="text-3xl font-semibold tracking-tight sm:text-4xl">
153
+ Everything you&apos;d expect.
154
+ </h2>
155
+ <p className="mt-4 text-pretty text-[var(--color-fg-muted)]">
156
+ The primitives every framework should have shipped with years ago.
157
+ </p>
158
+ </div>
159
+ <div className="mt-16 grid gap-px overflow-hidden rounded-2xl border border-[var(--color-border)] bg-[var(--color-border)] sm:grid-cols-2 lg:grid-cols-3">
160
+ {FEATURES.map((f) => {
161
+ const Icon = f.icon;
162
+ return (
163
+ <div
164
+ key={f.title}
165
+ className="flex flex-col gap-3 bg-[var(--color-surface)] p-6 transition-colors hover:bg-[var(--color-surface-2)]"
166
+ >
167
+ <Icon className="h-5 w-5 text-[var(--color-accent)]" />
168
+ <h3 className="text-[0.95rem] font-semibold text-[var(--color-fg)]">{f.title}</h3>
169
+ <p className="text-[0.875rem] leading-relaxed text-[var(--color-fg-muted)]">
170
+ {f.description}
171
+ </p>
172
+ </div>
173
+ );
174
+ })}
175
+ </div>
176
+ </div>
177
+ </section>
178
+
179
+ <section className="border-t border-[var(--color-border)] bg-[var(--color-surface)] py-24">
180
+ <div className="container-page">
181
+ <div className="grid items-start gap-16 lg:grid-cols-[1fr_1.4fr]">
182
+ <div className="lg:sticky lg:top-24">
183
+ <h2 className="text-3xl font-semibold tracking-tight sm:text-4xl">From the blog.</h2>
184
+ <p className="mt-4 text-[var(--color-fg-muted)]">
185
+ Field notes, release announcements, and the occasional rant about bundlers.
186
+ </p>
187
+ <Link
188
+ href="/blog"
189
+ className="mt-6 inline-flex items-center gap-1.5 text-[0.875rem] font-medium text-[var(--color-accent)] hover:text-[var(--color-accent-hover)]"
190
+ >
191
+ Read all posts
192
+ <svg
193
+ viewBox="0 0 24 24"
194
+ className="h-3.5 w-3.5"
195
+ fill="none"
196
+ stroke="currentColor"
197
+ strokeWidth="2"
198
+ >
199
+ <path d="M5 12h14M13 6l6 6-6 6" strokeLinecap="round" strokeLinejoin="round" />
200
+ </svg>
201
+ </Link>
202
+ </div>
203
+ <ul className="divide-y divide-[var(--color-border)] border-y border-[var(--color-border)]">
204
+ {recentPosts.map((post) => (
205
+ <li key={post.slug}>
206
+ <Link
207
+ href={`/blog/${post.slug}`}
208
+ className="group flex items-start justify-between gap-6 py-6 transition-colors"
209
+ >
210
+ <div className="flex-1">
211
+ <div className="flex items-center gap-2 text-[0.75rem] text-[var(--color-fg-subtle)]">
212
+ <time dateTime={post.date}>{post.date}</time>
213
+ <span>·</span>
214
+ <span>{post.readingTime}</span>
215
+ </div>
216
+ <h3 className="mt-2 text-[1.0625rem] font-semibold text-[var(--color-fg)] group-hover:text-[var(--color-accent)]">
217
+ {post.title}
218
+ </h3>
219
+ <p className="mt-1.5 text-[0.875rem] leading-relaxed text-[var(--color-fg-muted)]">
220
+ {post.excerpt}
221
+ </p>
222
+ </div>
223
+ <svg
224
+ viewBox="0 0 24 24"
225
+ className="mt-1 h-4 w-4 shrink-0 text-[var(--color-fg-subtle)] transition-all group-hover:translate-x-0.5 group-hover:text-[var(--color-fg)]"
226
+ fill="none"
227
+ stroke="currentColor"
228
+ strokeWidth="2"
229
+ >
230
+ <path
231
+ d="M5 12h14M13 6l6 6-6 6"
232
+ strokeLinecap="round"
233
+ strokeLinejoin="round"
234
+ />
235
+ </svg>
236
+ </Link>
237
+ </li>
238
+ ))}
239
+ </ul>
240
+ </div>
241
+ </div>
242
+ </section>
243
+
244
+ <section className="py-24">
245
+ <div className="container-page">
246
+ <div className="rounded-3xl border border-[var(--color-border)] bg-[var(--color-fg)] p-12 text-center sm:p-16">
247
+ <h2 className="text-balance text-3xl font-semibold tracking-tight text-[var(--color-bg)] sm:text-4xl">
248
+ Stop waiting on the build.
249
+ </h2>
250
+ <p className="mx-auto mt-4 max-w-lg text-pretty text-[var(--color-fg-subtle)]">
251
+ Install with one command. Scaffold a project, write TSX, ship a binary.
252
+ </p>
253
+ <div className="mt-8 flex flex-wrap items-center justify-center gap-3">
254
+ <code className="rounded-md border border-white/10 bg-white/5 px-3 py-2 font-mono text-[0.875rem] text-white">
255
+ bun create swift-rust@latest
256
+ </code>
257
+ <Link href="/contact" className="btn btn-accent">
258
+ Talk to us
259
+ </Link>
260
+ </div>
261
+ </div>
262
+ </div>
263
+ </section>
264
+ </>
265
+ );
266
+ }
267
+
268
+ function CodePreview() {
269
+ return (
270
+ <div className="mx-auto mt-20 max-w-4xl overflow-hidden rounded-2xl border border-[var(--color-border)] bg-[var(--color-surface)] shadow-[0_24px_64px_-12px_rgb(0_0_0_0.12)]">
271
+ <div className="flex items-center gap-1.5 border-b border-[var(--color-border)] px-4 py-3">
272
+ <span className="h-2.5 w-2.5 rounded-full bg-[#ff5f57]" />
273
+ <span className="h-2.5 w-2.5 rounded-full bg-[#febc2e]" />
274
+ <span className="h-2.5 w-2.5 rounded-full bg-[#28c840]" />
275
+ <span className="ml-3 font-mono text-[0.75rem] text-[var(--color-fg-subtle)]">
276
+ app/page.tsx
277
+ </span>
278
+ </div>
279
+ <pre className="overflow-x-auto p-6 font-mono text-[0.8125rem] leading-relaxed">
280
+ <code>
281
+ <span className="block text-[var(--color-fg-subtle)]">
282
+ {"// A page is just a component."}
283
+ </span>
284
+ {"\n"}
285
+ <span className="text-[#7c3aed]">export default function</span>{" "}
286
+ <span className="text-[#0ea5e9]">Page</span>
287
+ <span className="text-[var(--color-fg-muted)]">() {"{"}</span>
288
+ {"\n "}
289
+ <span className="text-[var(--color-fg-subtle)]">return</span>{" "}
290
+ <span className="text-[var(--color-fg-muted)]">(</span>
291
+ {"\n <"}
292
+ <span className="text-[#dc2626]">main</span>{" "}
293
+ <span className="text-[#0ea5e9]">className</span>
294
+ <span className="text-[var(--color-fg-muted)]">=</span>
295
+ <span className="text-[#16a34a]">"container"</span>
296
+ <span className="text-[var(--color-fg-muted)]">{">"}</span>
297
+ {"\n <"}
298
+ <span className="text-[#dc2626]">h1</span>
299
+ <span className="text-[var(--color-fg-muted)]">{">"}</span>
300
+ <span className="text-[var(--color-fg)]">Hello from Rust</span>
301
+ <span className="text-[var(--color-fg-muted)]">{"</"}</span>
302
+ <span className="text-[#dc2626]">h1</span>
303
+ <span className="text-[var(--color-fg-muted)]">{">"}</span>
304
+ {"\n <"}
305
+ <span className="text-[#dc2626]">p</span>
306
+ <span className="text-[var(--color-fg-muted)]">{">"}</span>
307
+ <span className="text-[var(--color-fg-muted)]">{"Streamed in 14ms."}</span>
308
+ <span className="text-[var(--color-fg-muted)]">{"</"}</span>
309
+ <span className="text-[#dc2626]">p</span>
310
+ <span className="text-[var(--color-fg-muted)]">{">"}</span>
311
+ {"\n "}
312
+ <span className="text-[var(--color-fg-muted)]">{"</"}</span>
313
+ <span className="text-[#dc2626]">main</span>
314
+ <span className="text-[var(--color-fg-muted)]">{">"}</span>
315
+ {"\n "}
316
+ <span className="text-[var(--color-fg-muted)]">{")"}</span>
317
+ {"\n"}
318
+ <span className="text-[var(--color-fg-muted)]">{"}"}</span>
319
+ </code>
320
+ </pre>
321
+ </div>
322
+ );
323
+ }
324
+
325
+ function IconBolt({ className = "" }: { className?: string }) {
326
+ return (
327
+ <svg
328
+ viewBox="0 0 24 24"
329
+ fill="none"
330
+ stroke="currentColor"
331
+ strokeWidth="1.75"
332
+ className={className}
333
+ >
334
+ <path d="M13 2 4 14h7l-1 8 9-12h-7l1-8Z" strokeLinejoin="round" />
335
+ </svg>
336
+ );
337
+ }
338
+ function IconCode({ className = "" }: { className?: string }) {
339
+ return (
340
+ <svg
341
+ viewBox="0 0 24 24"
342
+ fill="none"
343
+ stroke="currentColor"
344
+ strokeWidth="1.75"
345
+ className={className}
346
+ >
347
+ <path d="m8 6-6 6 6 6M16 6l6 6-6 6" strokeLinecap="round" strokeLinejoin="round" />
348
+ </svg>
349
+ );
350
+ }
351
+ function IconLayers({ className = "" }: { className?: string }) {
352
+ return (
353
+ <svg
354
+ viewBox="0 0 24 24"
355
+ fill="none"
356
+ stroke="currentColor"
357
+ strokeWidth="1.75"
358
+ className={className}
359
+ >
360
+ <path d="M12 3 2 8l10 5 10-5-10-5ZM2 14l10 5 10-5M2 18l10 5 10-5" strokeLinejoin="round" />
361
+ </svg>
362
+ );
363
+ }
364
+ function IconBox({ className = "" }: { className?: string }) {
365
+ return (
366
+ <svg
367
+ viewBox="0 0 24 24"
368
+ fill="none"
369
+ stroke="currentColor"
370
+ strokeWidth="1.75"
371
+ className={className}
372
+ >
373
+ <path d="M3 7v10l9 4 9-4V7l-9-4-9 4Z" strokeLinejoin="round" />
374
+ <path d="M3 7l9 4 9-4M12 11v10" />
375
+ </svg>
376
+ );
377
+ }
378
+ function IconGrid({ className = "" }: { className?: string }) {
379
+ return (
380
+ <svg
381
+ viewBox="0 0 24 24"
382
+ fill="none"
383
+ stroke="currentColor"
384
+ strokeWidth="1.75"
385
+ className={className}
386
+ >
387
+ <rect x="3" y="3" width="7" height="7" rx="1" />
388
+ <rect x="14" y="3" width="7" height="7" rx="1" />
389
+ <rect x="3" y="14" width="7" height="7" rx="1" />
390
+ <rect x="14" y="14" width="7" height="7" rx="1" />
391
+ </svg>
392
+ );
393
+ }
394
+ function IconBinary({ className = "" }: { className?: string }) {
395
+ return (
396
+ <svg
397
+ viewBox="0 0 24 24"
398
+ fill="none"
399
+ stroke="currentColor"
400
+ strokeWidth="1.75"
401
+ className={className}
402
+ >
403
+ <path
404
+ d="M6 4 2 9l4 5M18 4l4 5-4 5M12 4v16M9 4h6M9 20h6"
405
+ strokeLinecap="round"
406
+ strokeLinejoin="round"
407
+ />
408
+ </svg>
409
+ );
410
+ }
@@ -0,0 +1,182 @@
1
+ "use client";
2
+ import { type ChangeEvent, useState } from "react";
3
+ import type { Metadata } from "swift-rust";
4
+ import { Video } from "swift-rust";
5
+
6
+ export const metadata: Metadata = { title: "Videos" };
7
+
8
+ const YOUTUBE_URL = "https://youtu.be/rTJzsHwpZko?si=dyq6s5btq7MnASdS";
9
+ const YOUTUBE_ID = "rTJzsHwpZko";
10
+
11
+ const DETECTION_URLS = [
12
+ YOUTUBE_URL,
13
+ "https://www.youtube.com/watch?v=rTJzsHwpZko",
14
+ "https://www.youtube.com/embed/rTJzsHwpZko",
15
+ "https://www.youtube.com/shorts/rTJzsHwpZko",
16
+ "https://example.com/clip.mp4",
17
+ "https://vimeo.com/76979871",
18
+ "not-a-url",
19
+ ];
20
+
21
+ const SAMPLE_CODE = `import { Video, isYouTubeUrl, getYouTubeId } from "swift-rust";
22
+
23
+ const url = "https://youtu.be/rTJzsHwpZko?si=dyq6s5btq7MnASdS";
24
+
25
+ // Responsive: a fixed-ratio wrapper, video fills 100%.
26
+ <div className="aspect-video w-full">
27
+ <Video src={url} aspectRatio="16 / 9" className="h-full w-full rounded-xl" />
28
+ </div>
29
+
30
+ isYouTubeUrl(url); // → true
31
+ getYouTubeId(url); // → "rTJzsHwpZko"`;
32
+
33
+ export default function VideosPage() {
34
+ const [input, setInput] = useState(YOUTUBE_URL);
35
+ const [detected, setDetected] = useState(true);
36
+
37
+ return (
38
+ <div className="container-page py-16 sm:py-20">
39
+ <p className="text-[0.75rem] font-semibold uppercase tracking-wider text-[var(--color-fg-subtle)]">
40
+ Showcase
41
+ </p>
42
+ <h1 className="mt-2 text-4xl font-semibold tracking-tight sm:text-5xl">Video</h1>
43
+ <p className="mt-4 max-w-2xl text-[var(--color-fg-muted)]">
44
+ Drop a YouTube URL, get an embed. Auto-detected, privacy-preserving, zero JavaScript
45
+ required from your bundle.
46
+ </p>
47
+
48
+ <div className="mt-12 grid items-start gap-12 lg:grid-cols-[1.6fr_1fr]">
49
+ <div className="min-w-0 rounded-2xl border border-[var(--color-border)] bg-[var(--color-surface)] p-3">
50
+ <div className="aspect-video w-full overflow-hidden rounded-xl">
51
+ <Video
52
+ src={YOUTUBE_URL}
53
+ aspectRatio="16 / 9"
54
+ className="h-full w-full rounded-xl"
55
+ style={{ width: "100%", height: "100%" }}
56
+ />
57
+ </div>
58
+ </div>
59
+
60
+ <div className="min-w-0">
61
+ <h2 className="text-xl font-semibold tracking-tight">YouTube embed, auto-detected</h2>
62
+ <p className="mt-3 text-[0.95rem] leading-relaxed text-[var(--color-fg-muted)]">
63
+ The component inspects the URL, extracts the video ID, and switches to a
64
+ privacy-preserving <code>youtube-nocookie</code> embed automatically. No provider config
65
+ needed — paste, render, ship.
66
+ </p>
67
+
68
+ <div className="mt-6 overflow-hidden rounded-xl border border-[var(--color-border)] bg-[var(--color-surface)]">
69
+ <div className="border-b border-[var(--color-border)] bg-[var(--color-surface-2)] px-4 py-2 font-mono text-[0.7rem] text-[var(--color-fg-subtle)]">
70
+ example.tsx
71
+ </div>
72
+ <pre className="overflow-x-auto p-4 font-mono text-[0.75rem] leading-relaxed text-[var(--color-fg)]">
73
+ <code>{SAMPLE_CODE}</code>
74
+ </pre>
75
+ </div>
76
+ </div>
77
+ </div>
78
+
79
+ <section className="mt-24">
80
+ <h2 className="text-2xl font-semibold tracking-tight">Auto-detection in action</h2>
81
+ <p className="mt-2 max-w-2xl text-[var(--color-fg-muted)]">
82
+ The same <code>isYouTubeUrl</code> helper that powers the <code>Video</code> component is
83
+ exported from <code>swift-rust</code>. Try a few URLs against the live detector:
84
+ </p>
85
+ <div className="mt-6 overflow-hidden rounded-2xl border border-[var(--color-border)] bg-[var(--color-surface)] p-6">
86
+ <label className="block text-[0.7rem] font-semibold uppercase tracking-wider text-[var(--color-fg-subtle)]">
87
+ URL
88
+ </label>
89
+ <input
90
+ type="text"
91
+ value={input}
92
+ onChange={(e: ChangeEvent<HTMLInputElement>) => {
93
+ setInput(e.target.value);
94
+ try {
95
+ new URL(e.target.value);
96
+ setDetected(/youtu\.?be/.test(new URL(e.target.value).hostname));
97
+ } catch {
98
+ setDetected(false);
99
+ }
100
+ }}
101
+ className="mt-2 w-full rounded-md border border-[var(--color-border)] bg-[var(--color-bg)] px-3 py-2 font-mono text-[0.85rem] text-[var(--color-fg)] focus:border-[var(--color-accent)] focus:outline-none"
102
+ spellCheck={false}
103
+ />
104
+ <div className="mt-3 flex items-center gap-2 text-[0.85rem]">
105
+ <span
106
+ className={
107
+ detected
108
+ ? "rounded-full bg-[var(--color-accent)] px-2.5 py-0.5 text-[0.7rem] font-semibold uppercase tracking-wider text-white"
109
+ : "rounded-full border border-[var(--color-border)] bg-[var(--color-bg)] px-2.5 py-0.5 text-[0.7rem] font-semibold uppercase tracking-wider text-[var(--color-fg-muted)]"
110
+ }
111
+ >
112
+ {detected ? "YouTube" : "Not YouTube"}
113
+ </span>
114
+ <code className="font-mono text-[0.7rem] text-[var(--color-fg-subtle)]">
115
+ isYouTubeUrl("{input}") → {String(detected)}
116
+ </code>
117
+ </div>
118
+ </div>
119
+
120
+ <div className="mt-8 grid gap-3 sm:grid-cols-2">
121
+ {DETECTION_URLS.map((url) => {
122
+ let yt = false;
123
+ try {
124
+ const u = new URL(url);
125
+ yt = u.hostname === "youtu.be" || u.hostname.endsWith("youtube.com");
126
+ } catch {
127
+ yt = false;
128
+ }
129
+ return (
130
+ <div
131
+ key={url}
132
+ className="flex items-center gap-3 rounded-xl border border-[var(--color-border)] bg-[var(--color-surface)] px-4 py-3"
133
+ >
134
+ <code className="flex-1 truncate font-mono text-[0.8rem] text-[var(--color-fg)]">
135
+ {url}
136
+ </code>
137
+ {yt ? (
138
+ <span className="shrink-0 rounded-full bg-[var(--color-accent)] px-2.5 py-0.5 text-[0.7rem] font-semibold uppercase tracking-wider text-white">
139
+ YouTube
140
+ </span>
141
+ ) : (
142
+ <span className="shrink-0 rounded-full border border-[var(--color-border)] bg-[var(--color-bg)] px-2.5 py-0.5 text-[0.7rem] font-semibold uppercase tracking-wider text-[var(--color-fg-muted)]">
143
+ —
144
+ </span>
145
+ )}
146
+ </div>
147
+ );
148
+ })}
149
+ </div>
150
+ </section>
151
+
152
+ <section className="mt-24">
153
+ <h2 className="text-2xl font-semibold tracking-tight">Helper functions</h2>
154
+ <p className="mt-2 max-w-2xl text-[var(--color-fg-muted)]">
155
+ Need to know what kind of URL you have, or build an embed URL yourself? These utilities
156
+ are exported from <code>swift-rust</code>.
157
+ </p>
158
+ <div className="mt-8 overflow-hidden rounded-2xl border border-[var(--color-border)] bg-[var(--color-surface)]">
159
+ <div className="border-b border-[var(--color-border)] bg-[var(--color-surface-2)] px-4 py-2 font-mono text-[0.7rem] text-[var(--color-fg-subtle)]">
160
+ helpers.ts
161
+ </div>
162
+ <pre className="overflow-x-auto p-5 font-mono text-[0.78rem] leading-relaxed text-[var(--color-fg)]">
163
+ <code>{`import {
164
+ isYouTubeUrl,
165
+ getYouTubeId,
166
+ detectProvider,
167
+ getYouTubeEmbedUrl,
168
+ } from "swift-rust";
169
+
170
+ const url = "https://youtu.be/${YOUTUBE_ID}?si=dyq6s5btq7MnASdS";
171
+
172
+ isYouTubeUrl(url); // → true
173
+ getYouTubeId(url); // → "${YOUTUBE_ID}"
174
+ detectProvider(url); // → "youtube"
175
+ getYouTubeEmbedUrl("${YOUTUBE_ID}", { autoPlay: true, mute: true });
176
+ // → "https://www.youtube-nocookie.com/embed/${YOUTUBE_ID}?autoplay=1&mute=1"`}</code>
177
+ </pre>
178
+ </div>
179
+ </section>
180
+ </div>
181
+ );
182
+ }