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.
- package/dist/index.js +87 -881
- package/dist/index.js.map +1 -1
- package/package.json +8 -6
- package/templates/default/README.md +29 -0
- package/templates/default/_env.example +5 -0
- package/templates/default/_env.local +5 -0
- package/templates/default/_gitignore +14 -0
- package/templates/default/biome.json +12 -0
- package/templates/default/globals.d.ts +19 -0
- package/templates/default/package.json +30 -0
- package/templates/default/postcss.config.mjs +7 -0
- package/templates/default/public/blog/introducing-swift-rust.svg +12 -0
- package/templates/default/public/og-image.svg +25 -0
- package/templates/default/public/samples/axelborg-towers-modern-architecture.jpg +0 -0
- package/templates/default/public/samples/brown-high-skyscrapers.jpg +0 -0
- package/templates/default/public/samples/close-up-portrait-gorgeous-young-woman.jpg +0 -0
- package/templates/default/public/samples/full-shot-woman-taking-selfie.jpg +0 -0
- package/templates/default/public/samples/happy-black-parents-with-kids-making-video-call-smart-phone-home.jpg +0 -0
- package/templates/default/public/samples/metaverse-concept-collage-design.jpg +0 -0
- package/templates/default/src/app/about/layout.tsx +5 -0
- package/templates/default/src/app/about/page.tsx +74 -0
- package/templates/default/src/app/about/team/page.tsx +88 -0
- package/templates/default/src/app/api/contact/route.ts +15 -0
- package/templates/default/src/app/api/health/route.ts +7 -0
- package/templates/default/src/app/api/posts/[id]/route.ts +33 -0
- package/templates/default/src/app/api/posts/route.ts +29 -0
- package/templates/default/src/app/api/search/route.ts +15 -0
- package/templates/default/src/app/api/stats/route.ts +16 -0
- package/templates/default/src/app/blog/[slug]/page.tsx +116 -0
- package/templates/default/src/app/blog/page.tsx +101 -0
- package/templates/default/src/app/blog/tag/[tag]/page.tsx +82 -0
- package/templates/default/src/app/contact/page.tsx +131 -0
- package/templates/default/src/app/dashboard/analytics/page.tsx +103 -0
- package/templates/default/src/app/dashboard/layout.tsx +11 -0
- package/templates/default/src/app/dashboard/loading.tsx +18 -0
- package/templates/default/src/app/dashboard/page.tsx +181 -0
- package/templates/default/src/app/dashboard/posts/[id]/not-found.tsx +23 -0
- package/templates/default/src/app/dashboard/posts/[id]/page.tsx +87 -0
- package/templates/default/src/app/dashboard/posts/new/page.tsx +103 -0
- package/templates/default/src/app/dashboard/posts/page.tsx +81 -0
- package/templates/default/src/app/dashboard/settings/page.tsx +119 -0
- package/templates/default/src/app/error.tsx +42 -0
- package/templates/default/src/app/favicon.ico +0 -0
- package/templates/default/src/app/favicon.svg +8 -0
- package/templates/default/src/app/fonts/page.tsx +610 -0
- package/templates/default/src/app/globals.css +238 -0
- package/templates/default/src/app/images/page.tsx +56 -0
- package/templates/default/src/app/layout.tsx +44 -0
- package/templates/default/src/app/loading.tsx +13 -0
- package/templates/default/src/app/not-found.tsx +25 -0
- package/templates/default/src/app/page.tsx +410 -0
- package/templates/default/src/app/videos/page.tsx +182 -0
- package/templates/default/src/components/font-studio.tsx +378 -0
- package/templates/default/src/components/footer.tsx +75 -0
- package/templates/default/src/components/logo.tsx +26 -0
- package/templates/default/src/components/nav.tsx +102 -0
- package/templates/default/src/components/sidebar.tsx +109 -0
- package/templates/default/src/lib/blur.ts +5 -0
- package/templates/default/src/lib/format.ts +31 -0
- package/templates/default/src/lib/posts.ts +132 -0
- package/templates/default/src/lib/site.config.ts +39 -0
- package/templates/default/src/lib/stats.ts +25 -0
- package/templates/default/swift-rust.config.json +8 -0
- package/templates/default/tsconfig.json +29 -0
- 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)]">{" "}"swift-rust/font/google"</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
|
+
}
|