create-swift-rust 0.3.1 → 0.7.0

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 +87 -881
  2. package/dist/index.js.map +1 -1
  3. package/package.json +8 -6
  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 +101 -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 +29 -0
  65. package/templates/default/vercel.json +23 -0
@@ -0,0 +1,378 @@
1
+ "use client";
2
+
3
+ import { type ChangeEvent, useEffect, useMemo, useState } from "react";
4
+ import {
5
+ BebasNeue,
6
+ BricolageGrotesque,
7
+ Caveat,
8
+ Cinzel,
9
+ DmSans,
10
+ DmSerifDisplay,
11
+ Figtree,
12
+ FiraCode,
13
+ Geist,
14
+ GeistMono,
15
+ IbmPlexMono,
16
+ IbmPlexSans,
17
+ IbmPlexSerif,
18
+ Inter,
19
+ JetbrainsMono,
20
+ Lora,
21
+ Manrope,
22
+ Outfit,
23
+ PlayfairDisplay,
24
+ PlusJakartaSans,
25
+ Poppins,
26
+ Roboto,
27
+ Sora,
28
+ SourceCodePro,
29
+ SpaceGrotesk,
30
+ } from "swift-rust/font/google";
31
+
32
+ type Factory = (opts?: { variable?: boolean }) => {
33
+ className: string;
34
+ style: Record<string, string>;
35
+ variable?: string;
36
+ };
37
+
38
+ type StudioFont = {
39
+ name: string;
40
+ importName: string;
41
+ factory: Factory;
42
+ blurb: string;
43
+ };
44
+
45
+ const STUDIO_FONTS: StudioFont[] = [
46
+ { name: "Geist", importName: "Geist", factory: Geist as Factory, blurb: "Default UI font" },
47
+ { name: "Geist Mono", importName: "GeistMono", factory: GeistMono as Factory, blurb: "Default monospace" },
48
+ { name: "Inter", importName: "Inter", factory: Inter as Factory, blurb: "Designed for screens" },
49
+ { name: "Roboto", importName: "Roboto", factory: Roboto as Factory, blurb: "Modern, friendly" },
50
+ { name: "Poppins", importName: "Poppins", factory: Poppins as Factory, blurb: "Geometric, warm" },
51
+ { name: "Manrope", importName: "Manrope", factory: Manrope as Factory, blurb: "Clean sans for editorial" },
52
+ { name: "IBM Plex Sans", importName: "IbmPlexSans", factory: IbmPlexSans as Factory, blurb: "Corporate sans" },
53
+ { name: "Space Grotesk", importName: "SpaceGrotesk", factory: SpaceGrotesk as Factory, blurb: "Quirky display" },
54
+ { name: "Plus Jakarta Sans", importName: "PlusJakartaSans", factory: PlusJakartaSans as Factory, blurb: "Elegant, modern" },
55
+ { name: "Outfit", importName: "Outfit", factory: Outfit as Factory, blurb: "Geometric for branding" },
56
+ { name: "Figtree", importName: "Figtree", factory: Figtree as Factory, blurb: "Friendly, approachable" },
57
+ { name: "Sora", importName: "Sora", factory: Sora as Factory, blurb: "Clean, made for UI" },
58
+ { name: "DM Sans", importName: "DmSans", factory: DmSans as Factory, blurb: "Low-contrast geometric" },
59
+ { name: "Playfair Display", importName: "PlayfairDisplay", factory: PlayfairDisplay as Factory, blurb: "Editorial elegance" },
60
+ { name: "Lora", importName: "Lora", factory: Lora as Factory, blurb: "Balanced serif" },
61
+ { name: "DM Serif Display", importName: "DmSerifDisplay", factory: DmSerifDisplay as Factory, blurb: "Editorial display" },
62
+ { name: "IBM Plex Serif", importName: "IbmPlexSerif", factory: IbmPlexSerif as Factory, blurb: "Corporate serif" },
63
+ { name: "JetBrains Mono", importName: "JetbrainsMono", factory: JetbrainsMono as Factory, blurb: "Code editor" },
64
+ { name: "Fira Code", importName: "FiraCode", factory: FiraCode as Factory, blurb: "Monospace ligatures" },
65
+ { name: "Source Code Pro", importName: "SourceCodePro", factory: SourceCodePro as Factory, blurb: "Adobe monospace" },
66
+ { name: "IBM Plex Mono", importName: "IbmPlexMono", factory: IbmPlexMono as Factory, blurb: "Corporate mono" },
67
+ { name: "Bricolage Grotesque", importName: "BricolageGrotesque", factory: BricolageGrotesque as Factory, blurb: "Bold display sans" },
68
+ { name: "Bebas Neue", importName: "BebasNeue", factory: BebasNeue as Factory, blurb: "All-caps display" },
69
+ { name: "Cinzel", importName: "Cinzel", factory: Cinzel as Factory, blurb: "Roman capitals" },
70
+ { name: "Caveat", importName: "Caveat", factory: Caveat as Factory, blurb: "Casual handwriting" },
71
+ ];
72
+
73
+ const WEIGHTS = [
74
+ { value: 100, label: "Thin", abbr: "100" },
75
+ { value: 200, label: "ExtraLight", abbr: "200" },
76
+ { value: 300, label: "Light", abbr: "300" },
77
+ { value: 400, label: "Regular", abbr: "400" },
78
+ { value: 500, label: "Medium", abbr: "500" },
79
+ { value: 600, label: "SemiBold", abbr: "600" },
80
+ { value: 700, label: "Bold", abbr: "700" },
81
+ { value: 800, label: "ExtraBold", abbr: "800" },
82
+ { value: 900, label: "Black", abbr: "900" },
83
+ ] as const;
84
+
85
+ const SAMPLES = [
86
+ "Beautiful type",
87
+ "The quick brown fox jumps over the lazy dog",
88
+ "AaBbCc 1234567890",
89
+ "Ship fast, sleep well",
90
+ ] as const;
91
+
92
+ function toImportName(name: string): string {
93
+ return name.replace(/[\s-]+/g, "");
94
+ }
95
+
96
+ export function FontStudio() {
97
+ const [fontName, setFontName] = useState("Geist");
98
+ const [weight, setWeight] = useState(600);
99
+ const [italic, setItalic] = useState(false);
100
+ const [size, setSize] = useState(96);
101
+ const [tracking, setTracking] = useState(-3);
102
+ const [leading, setLeading] = useState(1.1);
103
+ const [sample, setSample] = useState("Beautiful type");
104
+ const [customFont, setCustomFont] = useState("");
105
+
106
+ useEffect(() => {
107
+ const trimmed = customFont.trim();
108
+ if (!trimmed) return;
109
+ const family = trimmed.replace(/\s+/g, "+");
110
+ const link = document.createElement("link");
111
+ link.rel = "stylesheet";
112
+ link.href = `https://fonts.googleapis.com/css2?family=${family}:wght@100;200;300;400;500;600;700;800;900&display=swap`;
113
+ link.dataset.swiftRustDynamicFont = trimmed;
114
+ document.head.appendChild(link);
115
+ return () => {
116
+ document.head.removeChild(link);
117
+ };
118
+ }, [customFont]);
119
+
120
+ const entry = useMemo(
121
+ () => STUDIO_FONTS.find((f) => f.name === fontName) ?? STUDIO_FONTS[0]!,
122
+ [fontName],
123
+ );
124
+ const loadedFont = entry.factory();
125
+
126
+ const activeFontFamily = customFont.trim()
127
+ ? `'${customFont}', system-ui, sans-serif`
128
+ : (loadedFont.style.fontFamily ?? `'${entry.name}', system-ui, sans-serif`);
129
+ const activeImportName = customFont.trim() ? toImportName(customFont) : entry.importName;
130
+
131
+ return (
132
+ <div className="rounded-2xl border border-[var(--color-border)] bg-[var(--color-surface)] overflow-hidden shadow-sm">
133
+ <div className="flex items-center justify-between border-b border-[var(--color-border)] bg-[var(--color-surface-2)] px-6 py-3">
134
+ <div className="flex items-center gap-2">
135
+ <span className="pulse-dot h-1.5 w-1.5 rounded-full bg-[var(--color-accent)]" />
136
+ <span className="text-xs font-medium uppercase tracking-wider text-[var(--color-fg-muted)]">
137
+ Typography studio
138
+ </span>
139
+ </div>
140
+ <span className="font-mono text-xs text-[var(--color-fg-subtle)]">
141
+ {entry.name} · {weight} · {italic ? "italic" : "roman"}
142
+ </span>
143
+ </div>
144
+
145
+ <div className="grid gap-0 lg:grid-cols-[20rem_1fr]">
146
+ <div className="border-b border-[var(--color-border)] p-6 lg:border-b-0 lg:border-r">
147
+ <div className="space-y-5">
148
+ <div>
149
+ <label
150
+ htmlFor="studio-font"
151
+ className="mb-1.5 block text-xs font-medium uppercase tracking-wider text-[var(--color-fg-subtle)]"
152
+ >
153
+ Font
154
+ </label>
155
+ <select
156
+ id="studio-font"
157
+ value={fontName}
158
+ onChange={(e: ChangeEvent<HTMLSelectElement>) => {
159
+ setFontName(e.target.value);
160
+ setCustomFont("");
161
+ }}
162
+ className="h-10 w-full rounded-md border border-[var(--color-border)] bg-[var(--color-surface)] px-3 text-sm text-[var(--color-fg)] focus:border-[var(--color-accent)] focus:outline-none focus:ring-2 focus:ring-[var(--color-accent-soft)]"
163
+ >
164
+ {STUDIO_FONTS.map((f) => (
165
+ <option key={f.name} value={f.name}>
166
+ {f.name}
167
+ </option>
168
+ ))}
169
+ </select>
170
+ </div>
171
+
172
+ <div>
173
+ <label
174
+ htmlFor="studio-weight"
175
+ className="mb-1.5 block text-xs font-medium uppercase tracking-wider text-[var(--color-fg-subtle)]"
176
+ >
177
+ Weight
178
+ </label>
179
+ <select
180
+ id="studio-weight"
181
+ value={weight}
182
+ onChange={(e: ChangeEvent<HTMLSelectElement>) =>
183
+ setWeight(Number(e.target.value))
184
+ }
185
+ className="h-10 w-full rounded-md border border-[var(--color-border)] bg-[var(--color-surface)] px-3 text-sm text-[var(--color-fg)] focus:border-[var(--color-accent)] focus:outline-none focus:ring-2 focus:ring-[var(--color-accent-soft)]"
186
+ >
187
+ {WEIGHTS.map((w) => (
188
+ <option key={w.value} value={w.value}>
189
+ {w.value} — {w.label}
190
+ </option>
191
+ ))}
192
+ </select>
193
+ </div>
194
+
195
+ <div>
196
+ <span className="mb-1.5 block text-xs font-medium uppercase tracking-wider text-[var(--color-fg-subtle)]">
197
+ Style
198
+ </span>
199
+ <div className="flex gap-2">
200
+ <button
201
+ type="button"
202
+ onClick={() => setItalic(false)}
203
+ className={`flex-1 h-10 rounded-md text-sm font-medium transition-colors ${
204
+ !italic
205
+ ? "bg-[var(--color-fg)] text-[var(--color-bg)]"
206
+ : "border border-[var(--color-border)] bg-[var(--color-surface)] text-[var(--color-fg-muted)] hover:text-[var(--color-fg)]"
207
+ }`}
208
+ >
209
+ Regular
210
+ </button>
211
+ <button
212
+ type="button"
213
+ onClick={() => setItalic(true)}
214
+ className={`flex-1 h-10 rounded-md text-sm font-medium transition-colors ${
215
+ italic
216
+ ? "bg-[var(--color-fg)] text-[var(--color-bg)]"
217
+ : "border border-[var(--color-border)] bg-[var(--color-surface)] text-[var(--color-fg-muted)] hover:text-[var(--color-fg)]"
218
+ }`}
219
+ >
220
+ Italic
221
+ </button>
222
+ </div>
223
+ </div>
224
+
225
+ <div>
226
+ <div className="mb-1.5 flex items-baseline justify-between">
227
+ <label
228
+ htmlFor="studio-size"
229
+ className="text-xs font-medium uppercase tracking-wider text-[var(--color-fg-subtle)]"
230
+ >
231
+ Size
232
+ </label>
233
+ <span className="font-mono text-xs text-[var(--color-fg-muted)]">{size}px</span>
234
+ </div>
235
+ <input
236
+ id="studio-size"
237
+ type="range"
238
+ min={16}
239
+ max={160}
240
+ value={size}
241
+ onChange={(e: ChangeEvent<HTMLInputElement>) =>
242
+ setSize(Number(e.target.value))
243
+ }
244
+ className="w-full accent-[var(--color-accent)]"
245
+ />
246
+ </div>
247
+
248
+ <div>
249
+ <div className="mb-1.5 flex items-baseline justify-between">
250
+ <label
251
+ htmlFor="studio-tracking"
252
+ className="text-xs font-medium uppercase tracking-wider text-[var(--color-fg-subtle)]"
253
+ >
254
+ Tracking
255
+ </label>
256
+ <span className="font-mono text-xs text-[var(--color-fg-muted)]">
257
+ {tracking >= 0 ? "+" : ""}
258
+ {tracking}
259
+ </span>
260
+ </div>
261
+ <input
262
+ id="studio-tracking"
263
+ type="range"
264
+ min={-10}
265
+ max={20}
266
+ value={tracking}
267
+ onChange={(e: ChangeEvent<HTMLInputElement>) =>
268
+ setTracking(Number(e.target.value))
269
+ }
270
+ className="w-full accent-[var(--color-accent)]"
271
+ />
272
+ </div>
273
+
274
+ <div>
275
+ <div className="mb-1.5 flex items-baseline justify-between">
276
+ <label
277
+ htmlFor="studio-leading"
278
+ className="text-xs font-medium uppercase tracking-wider text-[var(--color-fg-subtle)]"
279
+ >
280
+ Line height
281
+ </label>
282
+ <span className="font-mono text-xs text-[var(--color-fg-muted)]">
283
+ {leading.toFixed(2)}
284
+ </span>
285
+ </div>
286
+ <input
287
+ id="studio-leading"
288
+ type="range"
289
+ min={0.8}
290
+ max={2}
291
+ step={0.05}
292
+ value={leading}
293
+ onChange={(e: ChangeEvent<HTMLInputElement>) =>
294
+ setLeading(Number(e.target.value))
295
+ }
296
+ className="w-full accent-[var(--color-accent)]"
297
+ />
298
+ </div>
299
+
300
+ <div>
301
+ <label
302
+ htmlFor="studio-custom"
303
+ className="mb-1.5 block text-xs font-medium uppercase tracking-wider text-[var(--color-fg-subtle)]"
304
+ >
305
+ Any of 2,071 Google fonts
306
+ </label>
307
+ <input
308
+ id="studio-custom"
309
+ type="search"
310
+ value={customFont}
311
+ onChange={(e: ChangeEvent<HTMLInputElement>) => setCustomFont(e.target.value)}
312
+ placeholder="e.g. Fraunces, IBM Plex Sans KR, Anton"
313
+ className="h-10 w-full rounded-md border border-[var(--color-border)] bg-[var(--color-surface)] px-3 text-sm text-[var(--color-fg)] placeholder:text-[var(--color-fg-subtle)] focus:border-[var(--color-accent)] focus:outline-none focus:ring-2 focus:ring-[var(--color-accent-soft)]"
314
+ />
315
+ </div>
316
+ </div>
317
+ </div>
318
+
319
+ <div className="flex flex-col">
320
+ <div
321
+ className="flex-1 overflow-auto px-8 py-12 sm:px-12 sm:py-16"
322
+ style={{
323
+ fontFamily: activeFontFamily,
324
+ fontSize: `${size}px`,
325
+ fontWeight: weight,
326
+ fontStyle: italic ? "italic" : "normal",
327
+ letterSpacing: `${tracking / 100}em`,
328
+ lineHeight: leading,
329
+ color: "var(--color-fg)",
330
+ wordBreak: "break-word",
331
+ }}
332
+ >
333
+ {sample}
334
+ </div>
335
+
336
+ <div className="flex flex-wrap items-center gap-2 border-t border-[var(--color-border)] bg-[var(--color-surface-2)] px-6 py-3">
337
+ <span className="text-xs font-medium uppercase tracking-wider text-[var(--color-fg-subtle)]">
338
+ Sample
339
+ </span>
340
+ <div className="flex flex-wrap gap-1.5">
341
+ {SAMPLES.map((s) => (
342
+ <button
343
+ key={s}
344
+ type="button"
345
+ onClick={() => setSample(s)}
346
+ className={`rounded-full px-2.5 py-1 text-xs transition-colors ${
347
+ sample === s
348
+ ? "bg-[var(--color-fg)] text-[var(--color-bg)]"
349
+ : "border border-[var(--color-border)] bg-[var(--color-surface)] text-[var(--color-fg-muted)] hover:text-[var(--color-fg)]"
350
+ }`}
351
+ >
352
+ {s.length > 24 ? `${s.slice(0, 22)}…` : s}
353
+ </button>
354
+ ))}
355
+ </div>
356
+ <input
357
+ type="text"
358
+ value={sample}
359
+ onChange={(e: ChangeEvent<HTMLInputElement>) => setSample(e.target.value)}
360
+ className="ml-auto h-8 min-w-0 max-w-xs flex-1 rounded-md border border-[var(--color-border)] bg-[var(--color-surface)] px-3 text-xs text-[var(--color-fg)] focus:border-[var(--color-accent)] focus:outline-none focus:ring-2 focus:ring-[var(--color-accent-soft)]"
361
+ placeholder="Type to preview…"
362
+ />
363
+ </div>
364
+ </div>
365
+ </div>
366
+
367
+ <div className="border-t border-[var(--color-border)] bg-[var(--color-bg)] px-6 py-4 font-mono text-xs">
368
+ <span className="text-[var(--color-accent)]">import</span>
369
+ {" { "}
370
+ <span className="text-[var(--color-fg)]">{activeImportName}</span>
371
+ {" } "}
372
+ <span className="text-[var(--color-accent)]">from</span>
373
+ <span className="text-[var(--color-success)]">{" "}&quot;swift-rust/font/google&quot;</span>
374
+ <span className="text-[var(--color-fg-muted)]">;</span>
375
+ </div>
376
+ </div>
377
+ );
378
+ }
@@ -0,0 +1,75 @@
1
+ import { Link } from "swift-rust";
2
+ import { siteConfig } from "@/lib/site.config";
3
+ import { Logo } from "./logo";
4
+
5
+ const COLUMNS: Array<{ title: string; links: Array<{ href: string; label: string }> }> = [
6
+ {
7
+ title: "Framework",
8
+ links: [
9
+ { href: "/", label: "Overview" },
10
+ { href: "/blog", label: "Blog" },
11
+ { href: "/about", label: "About" },
12
+ { href: "/contact", label: "Contact" },
13
+ ],
14
+ },
15
+ {
16
+ title: "Examples",
17
+ links: [
18
+ { href: "/fonts", label: "Fonts" },
19
+ { href: "/images", label: "Images" },
20
+ { href: "/videos", label: "Videos" },
21
+ ],
22
+ },
23
+ {
24
+ title: "Resources",
25
+ links: [
26
+ { href: siteConfig.githubUrl, label: "GitHub" },
27
+ { href: siteConfig.docsUrl, label: "Docs" },
28
+ { href: "https://discord.gg/swift-rust", label: "Discord" },
29
+ { href: siteConfig.issuesUrl, label: "Issues" },
30
+ ],
31
+ },
32
+ ];
33
+
34
+ export function Footer() {
35
+ const year = new Date().getFullYear();
36
+ return (
37
+ <footer className="mt-32 border-t border-[var(--color-border)] bg-[var(--color-surface)]">
38
+ <div className="container-page py-16">
39
+ <div className="grid gap-12 md:grid-cols-[1.5fr_2fr]">
40
+ <div>
41
+ <Logo />
42
+ <p className="mt-4 max-w-xs text-[0.875rem] leading-relaxed text-[var(--color-fg-muted)]">
43
+ A full-stack React framework powered with Rust + Bun. TSX-first, single binary, four rendering modes.
44
+ </p>
45
+ </div>
46
+ <div className="grid grid-cols-2 gap-8 sm:grid-cols-3">
47
+ {COLUMNS.map((col) => (
48
+ <div key={col.title}>
49
+ <h3 className="text-[0.75rem] font-semibold uppercase tracking-wider text-[var(--color-fg-subtle)]">
50
+ {col.title}
51
+ </h3>
52
+ <ul className="mt-4 space-y-2.5">
53
+ {col.links.map((link) => (
54
+ <li key={link.href}>
55
+ <Link
56
+ href={link.href}
57
+ className="text-[0.875rem] text-[var(--color-fg-muted)] transition-colors hover:text-[var(--color-fg)]"
58
+ >
59
+ {link.label}
60
+ </Link>
61
+ </li>
62
+ ))}
63
+ </ul>
64
+ </div>
65
+ ))}
66
+ </div>
67
+ </div>
68
+ <div className="mt-12 flex flex-col items-start justify-between gap-4 border-t border-[var(--color-border)] pt-8 text-[0.8125rem] text-[var(--color-fg-subtle)] sm:flex-row sm:items-center">
69
+ <p>© {year} swift-rust. Built with Rust + Bun.</p>
70
+ <p className="font-mono">v{siteConfig.version} · MIT</p>
71
+ </div>
72
+ </div>
73
+ </footer>
74
+ );
75
+ }
@@ -0,0 +1,26 @@
1
+ import Image from "swift-rust/image";
2
+ import { BLUR_TRANSPARENT } from "@/lib/blur";
3
+
4
+ export function Logo({ className = "" }: { className?: string }) {
5
+ return (
6
+ <span className={`inline-flex items-center gap-2 ${className}`}>
7
+ {/* Single-sourced from the favicon file (lightning + gear mark). The
8
+ favicon lives in the app dir, so use a passthrough loader instead of
9
+ the image-optimizer endpoint. */}
10
+ <Image
11
+ src="/favicon.svg"
12
+ alt=""
13
+ aria-hidden
14
+ width={24}
15
+ height={24}
16
+ placeholder="blur"
17
+ blurDataURL={BLUR_TRANSPARENT}
18
+ loader={({ src }) => src}
19
+ className="h-6 w-6"
20
+ />
21
+ <span className="text-[0.95rem] font-semibold tracking-tight text-[var(--color-fg)]">
22
+ swift<span className="text-[var(--color-accent)]">·</span>rust
23
+ </span>
24
+ </span>
25
+ );
26
+ }
@@ -0,0 +1,102 @@
1
+ import { Link } from "swift-rust";
2
+ import { siteConfig } from "@/lib/site.config";
3
+ import { Logo } from "./logo";
4
+
5
+ const NAV = [
6
+ { href: "/", label: "Home" },
7
+ { href: "/blog", label: "Blog" },
8
+ { href: "/fonts", label: "Fonts" },
9
+ { href: "/images", label: "Images" },
10
+ { href: "/videos", label: "Videos" },
11
+ { href: "/about", label: "About" },
12
+ { href: "/contact", label: "Contact" },
13
+ ];
14
+
15
+ const GitHubIcon = () => (
16
+ <svg viewBox="0 0 24 24" className="h-4 w-4" fill="currentColor" aria-hidden>
17
+ <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" />
18
+ </svg>
19
+ );
20
+
21
+ export function Nav() {
22
+ return (
23
+ <header className="sticky top-0 z-40 border-b border-[var(--color-border)] bg-[var(--color-bg)]/80 backdrop-blur-md">
24
+ {/* CSS-only mobile menu toggle: no client JS required (these pages are
25
+ static SSR). The peer checkbox drives the panel below. */}
26
+ <input type="checkbox" id="site-menu" className="peer sr-only" aria-label="Toggle menu" />
27
+
28
+ <div className="container-page flex h-14 items-center justify-between gap-8">
29
+ <Link href="/" aria-label="Home">
30
+ <Logo />
31
+ </Link>
32
+
33
+ <nav className="hidden items-center gap-1 md:flex">
34
+ {NAV.map((item) => (
35
+ <Link
36
+ key={item.href}
37
+ href={item.href}
38
+ className="rounded-md px-3 py-1.5 text-[0.875rem] text-[var(--color-fg-muted)] transition-colors hover:bg-[var(--color-surface-2)] hover:text-[var(--color-fg)]"
39
+ >
40
+ {item.label}
41
+ </Link>
42
+ ))}
43
+ </nav>
44
+
45
+ <div className="flex items-center gap-2">
46
+ <a
47
+ href={siteConfig.githubUrl}
48
+ target="_blank"
49
+ rel="noreferrer"
50
+ className="btn btn-outline btn-sm hidden sm:inline-flex"
51
+ >
52
+ <GitHubIcon />
53
+ GitHub
54
+ </a>
55
+ <Link href={siteConfig.docsUrl} className="btn btn-accent btn-sm hidden sm:inline-flex">
56
+ Get started
57
+ </Link>
58
+
59
+ {/* Hamburger / close — a label toggling the checkbox above. */}
60
+ <label
61
+ htmlFor="site-menu"
62
+ aria-label="Toggle navigation menu"
63
+ className="inline-flex h-9 w-9 cursor-pointer items-center justify-center rounded-md border border-[var(--color-border)] text-[var(--color-fg)] md:hidden"
64
+ >
65
+ <svg viewBox="0 0 24 24" className="h-5 w-5 peer-checked:hidden" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden>
66
+ <path d="M4 7h16M4 12h16M4 17h16" strokeLinecap="round" />
67
+ </svg>
68
+ </label>
69
+ </div>
70
+ </div>
71
+
72
+ {/* Mobile dropdown panel — shown only when the checkbox is checked. */}
73
+ <nav className="hidden border-t border-[var(--color-border)] bg-[var(--color-bg)] px-4 py-3 peer-checked:block md:!hidden">
74
+ <div className="flex flex-col gap-1">
75
+ {NAV.map((item) => (
76
+ <Link
77
+ key={item.href}
78
+ href={item.href}
79
+ className="rounded-md px-3 py-2 text-[0.95rem] text-[var(--color-fg-muted)] hover:bg-[var(--color-surface-2)] hover:text-[var(--color-fg)]"
80
+ >
81
+ {item.label}
82
+ </Link>
83
+ ))}
84
+ <div className="mt-2 flex gap-2 border-t border-[var(--color-border)] pt-3">
85
+ <a
86
+ href={siteConfig.githubUrl}
87
+ target="_blank"
88
+ rel="noreferrer"
89
+ className="btn btn-outline btn-sm flex-1 justify-center"
90
+ >
91
+ <GitHubIcon />
92
+ GitHub
93
+ </a>
94
+ <Link href={siteConfig.docsUrl} className="btn btn-accent btn-sm flex-1 justify-center">
95
+ Get started
96
+ </Link>
97
+ </div>
98
+ </div>
99
+ </nav>
100
+ </header>
101
+ );
102
+ }