@stdout-design/cli 0.6.0 → 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.mjs +6 -23
- package/package.json +2 -2
- package/template/README.md +37 -0
- package/template/locales/ar.json +49 -0
- package/template/locales/en.json +49 -0
- package/template/templates/bento-feature.tsx +377 -32
- package/template/templates/launch-hero.tsx +362 -0
- package/template/templates/retro-poster.tsx +371 -0
- package/template/templates/trend-poster.tsx +383 -0
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
import { defineSchema } from "@stdout-design/cli/schema";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
|
|
4
|
+
export const templateId = "launch-hero" as const;
|
|
5
|
+
|
|
6
|
+
export const propsSchema = defineSchema(
|
|
7
|
+
z.object({
|
|
8
|
+
accentColor: z
|
|
9
|
+
.string()
|
|
10
|
+
.default("#2b3bff")
|
|
11
|
+
.describe("Page background color"),
|
|
12
|
+
badge: z
|
|
13
|
+
.string()
|
|
14
|
+
.default("Drop 07")
|
|
15
|
+
.describe("Text on the starburst sticker"),
|
|
16
|
+
brand: z.string().default("OFFBEAT®").describe("Wordmark in the nav bar"),
|
|
17
|
+
categories: z
|
|
18
|
+
.array(z.string())
|
|
19
|
+
.default(["Tees", "Hoodies", "Caps"])
|
|
20
|
+
.describe("Three category card labels"),
|
|
21
|
+
cta: z.string().default("Shop now").describe("Primary button label"),
|
|
22
|
+
headline: z
|
|
23
|
+
.string()
|
|
24
|
+
.min(1)
|
|
25
|
+
.default("Wear It Loud")
|
|
26
|
+
.describe("Hero headline — each word gets its own line"),
|
|
27
|
+
links: z
|
|
28
|
+
.array(z.string())
|
|
29
|
+
.default(["Drops", "Archive", "Club"])
|
|
30
|
+
.describe("Nav bar links"),
|
|
31
|
+
locale: z
|
|
32
|
+
.string()
|
|
33
|
+
.optional()
|
|
34
|
+
.describe("Current locale code for RTL support"),
|
|
35
|
+
note: z
|
|
36
|
+
.string()
|
|
37
|
+
.default("Ships worldwide")
|
|
38
|
+
.describe("Small note at the end of the stats strip"),
|
|
39
|
+
stats: z
|
|
40
|
+
.array(z.string())
|
|
41
|
+
.default(["12 drops a year", "0 restocks", "100% cotton"])
|
|
42
|
+
.describe("Stats strip — the first word is the big number"),
|
|
43
|
+
subheadline: z
|
|
44
|
+
.string()
|
|
45
|
+
.default("Small-batch streetwear, printed in a city that never whispers.")
|
|
46
|
+
.describe("Supporting line under the headline"),
|
|
47
|
+
})
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
export type Props = z.infer<typeof propsSchema>;
|
|
51
|
+
|
|
52
|
+
const INK = "#0e0e12";
|
|
53
|
+
const CREAM = "#fff6e5";
|
|
54
|
+
const PINK = "#ff4fa3";
|
|
55
|
+
const LIME = "#c8f560";
|
|
56
|
+
// Latin face first; Arabic glyphs fall through to the Arabic face.
|
|
57
|
+
const DISPLAY = "Archivo Black, Lalezar, sans-serif";
|
|
58
|
+
const MONO = "Space Mono, IBM Plex Sans Arabic, monospace";
|
|
59
|
+
const CARD_STYLES = [
|
|
60
|
+
{ art: "bolt", color: CREAM, tilt: -5 },
|
|
61
|
+
{ art: "spiral", color: PINK, tilt: 4 },
|
|
62
|
+
{ art: "eye", color: LIME, tilt: -2 },
|
|
63
|
+
] as const;
|
|
64
|
+
|
|
65
|
+
type Art = (typeof CARD_STYLES)[number]["art"];
|
|
66
|
+
|
|
67
|
+
const starburst = (spikes: number, outer: number, inner: number): string =>
|
|
68
|
+
Array.from({ length: spikes * 2 }, (_, index) => {
|
|
69
|
+
const radius = index % 2 === 0 ? outer : inner;
|
|
70
|
+
const angle = (Math.PI * index) / spikes - Math.PI / 2;
|
|
71
|
+
return `${50 + radius * Math.cos(angle)},${50 + radius * Math.sin(angle)}`;
|
|
72
|
+
}).join(" ");
|
|
73
|
+
|
|
74
|
+
// Arrows and sparkles are drawn, not typed: display faces rarely ship
|
|
75
|
+
// those glyphs, and a missing glyph renders as tofu.
|
|
76
|
+
const Arrow = ({ color, flip }: { color: string; flip: boolean }) => (
|
|
77
|
+
<svg
|
|
78
|
+
width="22"
|
|
79
|
+
height="22"
|
|
80
|
+
viewBox="0 0 24 24"
|
|
81
|
+
fill="none"
|
|
82
|
+
style={{ transform: flip ? "scaleX(-1)" : "none" }}
|
|
83
|
+
>
|
|
84
|
+
<path
|
|
85
|
+
d="M4 12h15M13 5l7 7-7 7"
|
|
86
|
+
stroke={color}
|
|
87
|
+
strokeWidth="3"
|
|
88
|
+
strokeLinecap="round"
|
|
89
|
+
strokeLinejoin="round"
|
|
90
|
+
/>
|
|
91
|
+
</svg>
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
const Sparkle = ({ color }: { color: string }) => (
|
|
95
|
+
<svg width="18" height="18" viewBox="0 0 24 24">
|
|
96
|
+
<path d="M12 0 15 9 24 12 15 15 12 24 9 15 0 12 9 9Z" fill={color} />
|
|
97
|
+
</svg>
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
const CardArt = ({ art }: { art: Art }) => {
|
|
101
|
+
if (art === "bolt") {
|
|
102
|
+
return (
|
|
103
|
+
<svg width="110" height="130" viewBox="0 0 80 100">
|
|
104
|
+
<path
|
|
105
|
+
d="M48 2 8 58h26l-6 40 44-60H44Z"
|
|
106
|
+
fill={LIME}
|
|
107
|
+
stroke={INK}
|
|
108
|
+
strokeWidth="5"
|
|
109
|
+
strokeLinejoin="round"
|
|
110
|
+
/>
|
|
111
|
+
</svg>
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
if (art === "spiral") {
|
|
115
|
+
return (
|
|
116
|
+
<svg width="130" height="130" viewBox="0 0 100 100">
|
|
117
|
+
<path
|
|
118
|
+
d="M50 50m0 0a6 6 0 0 1 12 0a12 12 0 0 1-24 0a18 18 0 0 1 36 0a24 24 0 0 1-48 0a30 30 0 0 1 60 0a36 36 0 0 1-72 0"
|
|
119
|
+
fill="none"
|
|
120
|
+
stroke={INK}
|
|
121
|
+
strokeWidth="6"
|
|
122
|
+
strokeLinecap="round"
|
|
123
|
+
/>
|
|
124
|
+
</svg>
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
return (
|
|
128
|
+
<svg width="140" height="100" viewBox="0 0 140 100">
|
|
129
|
+
<path
|
|
130
|
+
d="M6 50Q70-14 134 50Q70 114 6 50Z"
|
|
131
|
+
fill={CREAM}
|
|
132
|
+
stroke={INK}
|
|
133
|
+
strokeWidth="6"
|
|
134
|
+
/>
|
|
135
|
+
<circle
|
|
136
|
+
cx="70"
|
|
137
|
+
cy="50"
|
|
138
|
+
r="22"
|
|
139
|
+
fill="#2b3bff"
|
|
140
|
+
stroke={INK}
|
|
141
|
+
strokeWidth="5"
|
|
142
|
+
/>
|
|
143
|
+
<circle cx="70" cy="50" r="9" fill={INK} />
|
|
144
|
+
</svg>
|
|
145
|
+
);
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
const LaunchHero = ({
|
|
149
|
+
accentColor,
|
|
150
|
+
badge,
|
|
151
|
+
brand,
|
|
152
|
+
categories,
|
|
153
|
+
cta,
|
|
154
|
+
headline,
|
|
155
|
+
links,
|
|
156
|
+
locale,
|
|
157
|
+
note,
|
|
158
|
+
stats,
|
|
159
|
+
subheadline,
|
|
160
|
+
}: Props) => {
|
|
161
|
+
const isRtl = locale?.startsWith("ar") ?? false;
|
|
162
|
+
// Flex rows mirror themselves under dir="rtl"; absolute offsets don't.
|
|
163
|
+
const start = isRtl ? "right" : "left";
|
|
164
|
+
const end = isRtl ? "left" : "right";
|
|
165
|
+
const marginStart = isRtl ? "marginRight" : "marginLeft";
|
|
166
|
+
const words = headline.split(/\s+/u).filter(Boolean);
|
|
167
|
+
|
|
168
|
+
return (
|
|
169
|
+
<div
|
|
170
|
+
lang={isRtl ? "ar" : undefined}
|
|
171
|
+
dir={isRtl ? "rtl" : "ltr"}
|
|
172
|
+
tw="flex flex-col w-full h-full overflow-hidden relative"
|
|
173
|
+
style={{ backgroundColor: accentColor, color: CREAM, fontFamily: MONO }}
|
|
174
|
+
>
|
|
175
|
+
{/* Halftone corner */}
|
|
176
|
+
<div
|
|
177
|
+
tw="absolute"
|
|
178
|
+
style={{
|
|
179
|
+
backgroundImage: `radial-gradient(${INK} 30%, transparent 32%)`,
|
|
180
|
+
backgroundSize: "18px 18px",
|
|
181
|
+
bottom: -60,
|
|
182
|
+
[end]: -60,
|
|
183
|
+
height: 460,
|
|
184
|
+
opacity: 0.22,
|
|
185
|
+
width: 560,
|
|
186
|
+
}}
|
|
187
|
+
/>
|
|
188
|
+
|
|
189
|
+
{/* Nav */}
|
|
190
|
+
<div
|
|
191
|
+
tw="flex items-center justify-between px-12 py-6"
|
|
192
|
+
style={{ borderBottom: `3px solid ${CREAM}` }}
|
|
193
|
+
>
|
|
194
|
+
{/* Pinned LTR so bidi doesn't move the ® to the front in Arabic. */}
|
|
195
|
+
<span dir="ltr" style={{ fontFamily: DISPLAY, fontSize: 38 }}>
|
|
196
|
+
{brand}
|
|
197
|
+
</span>
|
|
198
|
+
<div tw="flex items-center text-xl" style={{ gap: 32 }}>
|
|
199
|
+
{links.map((link) => (
|
|
200
|
+
<span key={link}>{link}</span>
|
|
201
|
+
))}
|
|
202
|
+
<div
|
|
203
|
+
tw="flex items-center px-6 py-3 rounded-full text-xl"
|
|
204
|
+
style={{
|
|
205
|
+
backgroundColor: CREAM,
|
|
206
|
+
color: INK,
|
|
207
|
+
fontWeight: 700,
|
|
208
|
+
gap: 12,
|
|
209
|
+
}}
|
|
210
|
+
>
|
|
211
|
+
<span>{cta}</span>
|
|
212
|
+
<Arrow color={INK} flip={isRtl} />
|
|
213
|
+
</div>
|
|
214
|
+
</div>
|
|
215
|
+
</div>
|
|
216
|
+
|
|
217
|
+
{/* Hero */}
|
|
218
|
+
<div tw="flex flex-1 px-12 py-8 relative" style={{ minHeight: 0 }}>
|
|
219
|
+
<div tw="flex flex-col justify-center flex-1">
|
|
220
|
+
{words.map((word, index) => (
|
|
221
|
+
<span
|
|
222
|
+
key={`${word}-${index}`}
|
|
223
|
+
style={{
|
|
224
|
+
WebkitTextStroke: `3px ${INK}`,
|
|
225
|
+
color: index % 2 === 1 ? PINK : CREAM,
|
|
226
|
+
fontFamily: DISPLAY,
|
|
227
|
+
fontSize: isRtl ? 112 : 132,
|
|
228
|
+
letterSpacing: isRtl ? 0 : "-0.03em",
|
|
229
|
+
lineHeight: isRtl ? 1.02 : 0.95,
|
|
230
|
+
[marginStart]: index * 36,
|
|
231
|
+
textShadow: `7px 7px 0 ${INK}`,
|
|
232
|
+
textTransform: "uppercase",
|
|
233
|
+
}}
|
|
234
|
+
>
|
|
235
|
+
{word}
|
|
236
|
+
</span>
|
|
237
|
+
))}
|
|
238
|
+
<div
|
|
239
|
+
tw="flex text-2xl mt-7"
|
|
240
|
+
style={{ lineHeight: 1.4, maxWidth: 600 }}
|
|
241
|
+
>
|
|
242
|
+
{subheadline}
|
|
243
|
+
</div>
|
|
244
|
+
</div>
|
|
245
|
+
|
|
246
|
+
{/* Collage */}
|
|
247
|
+
<div tw="flex w-[36%] relative">
|
|
248
|
+
{categories.slice(0, CARD_STYLES.length).map((category, index) => {
|
|
249
|
+
const style = CARD_STYLES[index] ?? CARD_STYLES[0];
|
|
250
|
+
return (
|
|
251
|
+
<div
|
|
252
|
+
key={category}
|
|
253
|
+
tw="flex flex-col justify-between absolute rounded-3xl p-6"
|
|
254
|
+
style={{
|
|
255
|
+
backgroundColor: style.color,
|
|
256
|
+
border: `3px solid ${INK}`,
|
|
257
|
+
boxShadow: `9px 9px 0 ${INK}`,
|
|
258
|
+
color: INK,
|
|
259
|
+
height: "38%",
|
|
260
|
+
[start]: index % 2 === 0 ? 10 : 90,
|
|
261
|
+
top: `${2 + index * 30}%`,
|
|
262
|
+
transform: `rotate(${isRtl ? -style.tilt : style.tilt}deg)`,
|
|
263
|
+
width: 270,
|
|
264
|
+
}}
|
|
265
|
+
>
|
|
266
|
+
<div tw="flex items-start justify-between" style={{ gap: 12 }}>
|
|
267
|
+
<span
|
|
268
|
+
style={{
|
|
269
|
+
fontFamily: DISPLAY,
|
|
270
|
+
fontSize: 44,
|
|
271
|
+
lineHeight: 1.1,
|
|
272
|
+
}}
|
|
273
|
+
>
|
|
274
|
+
{category}
|
|
275
|
+
</span>
|
|
276
|
+
<span tw="text-lg">{`0${index + 1}`}</span>
|
|
277
|
+
</div>
|
|
278
|
+
<div tw="flex justify-end">
|
|
279
|
+
<CardArt art={style.art} />
|
|
280
|
+
</div>
|
|
281
|
+
</div>
|
|
282
|
+
);
|
|
283
|
+
})}
|
|
284
|
+
</div>
|
|
285
|
+
<div
|
|
286
|
+
tw="flex absolute items-center justify-center"
|
|
287
|
+
style={{
|
|
288
|
+
height: 170,
|
|
289
|
+
[start]: "50%",
|
|
290
|
+
top: 18,
|
|
291
|
+
transform: `rotate(${isRtl ? -12 : 12}deg)`,
|
|
292
|
+
width: 170,
|
|
293
|
+
}}
|
|
294
|
+
>
|
|
295
|
+
<svg
|
|
296
|
+
width="170"
|
|
297
|
+
height="170"
|
|
298
|
+
viewBox="0 0 100 100"
|
|
299
|
+
style={{ left: 0, position: "absolute", top: 0 }}
|
|
300
|
+
>
|
|
301
|
+
<polygon
|
|
302
|
+
points={starburst(16, 49, 39)}
|
|
303
|
+
fill={LIME}
|
|
304
|
+
stroke={INK}
|
|
305
|
+
strokeWidth="2.5"
|
|
306
|
+
/>
|
|
307
|
+
</svg>
|
|
308
|
+
{/* Absolute so it paints above the (absolute) starburst. */}
|
|
309
|
+
<div tw="flex absolute inset-0 items-center justify-center">
|
|
310
|
+
<span
|
|
311
|
+
tw="text-center"
|
|
312
|
+
style={{
|
|
313
|
+
color: INK,
|
|
314
|
+
fontFamily: DISPLAY,
|
|
315
|
+
fontSize: 28,
|
|
316
|
+
lineHeight: 1.05,
|
|
317
|
+
maxWidth: 110,
|
|
318
|
+
}}
|
|
319
|
+
>
|
|
320
|
+
{badge}
|
|
321
|
+
</span>
|
|
322
|
+
</div>
|
|
323
|
+
</div>
|
|
324
|
+
</div>
|
|
325
|
+
|
|
326
|
+
{/* Stats strip */}
|
|
327
|
+
<div
|
|
328
|
+
tw="flex items-center px-12 py-5"
|
|
329
|
+
style={{ backgroundColor: INK, gap: 48 }}
|
|
330
|
+
>
|
|
331
|
+
{stats.map((stat) => {
|
|
332
|
+
const [value = "", ...label] = stat.split(" ");
|
|
333
|
+
return (
|
|
334
|
+
<div key={stat} tw="flex items-end" style={{ gap: 12 }}>
|
|
335
|
+
<span
|
|
336
|
+
style={{ fontFamily: DISPLAY, fontSize: 40, lineHeight: 1.1 }}
|
|
337
|
+
>
|
|
338
|
+
{value}
|
|
339
|
+
</span>
|
|
340
|
+
<span tw="text-lg mb-1" style={{ opacity: 0.7 }}>
|
|
341
|
+
{label.join(" ")}
|
|
342
|
+
</span>
|
|
343
|
+
</div>
|
|
344
|
+
);
|
|
345
|
+
})}
|
|
346
|
+
<div
|
|
347
|
+
tw="flex items-center text-lg"
|
|
348
|
+
style={{
|
|
349
|
+
color: LIME,
|
|
350
|
+
gap: 8,
|
|
351
|
+
[marginStart]: "auto",
|
|
352
|
+
}}
|
|
353
|
+
>
|
|
354
|
+
<Sparkle color={LIME} />
|
|
355
|
+
<span>{note.toUpperCase()}</span>
|
|
356
|
+
</div>
|
|
357
|
+
</div>
|
|
358
|
+
</div>
|
|
359
|
+
);
|
|
360
|
+
};
|
|
361
|
+
|
|
362
|
+
export default LaunchHero;
|