@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,383 @@
|
|
|
1
|
+
import { defineSchema } from "@stdout-design/cli/schema";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
|
|
4
|
+
export const templateId = "trend-poster" as const;
|
|
5
|
+
|
|
6
|
+
export const propsSchema = defineSchema(
|
|
7
|
+
z.object({
|
|
8
|
+
aside: z
|
|
9
|
+
.string()
|
|
10
|
+
.default("to steal & remix")
|
|
11
|
+
.describe("Serif aside next to the edition"),
|
|
12
|
+
cta: z
|
|
13
|
+
.string()
|
|
14
|
+
.default("Save for later")
|
|
15
|
+
.describe("Button label in the footer"),
|
|
16
|
+
edition: z
|
|
17
|
+
.string()
|
|
18
|
+
.default("2027")
|
|
19
|
+
.describe("Big outlined word under the headline"),
|
|
20
|
+
footer: z
|
|
21
|
+
.string()
|
|
22
|
+
.default("Make the thumb stop.")
|
|
23
|
+
.describe("Serif line in the footer"),
|
|
24
|
+
headline: z
|
|
25
|
+
.string()
|
|
26
|
+
.min(1)
|
|
27
|
+
.default("Ways to stop the scroll")
|
|
28
|
+
.describe("Headline — prefixed with the number of items"),
|
|
29
|
+
highlight: z
|
|
30
|
+
.string()
|
|
31
|
+
.default("#ff6a3d")
|
|
32
|
+
.describe("Highlight color for the edition and button"),
|
|
33
|
+
issue: z.string().default("Issue 07").describe("Small label top-start"),
|
|
34
|
+
items: z
|
|
35
|
+
.array(z.string())
|
|
36
|
+
.default([
|
|
37
|
+
"Loud type",
|
|
38
|
+
"Halftone",
|
|
39
|
+
"Clashing color",
|
|
40
|
+
"Raw collage",
|
|
41
|
+
"Stickers",
|
|
42
|
+
"Big shapes",
|
|
43
|
+
])
|
|
44
|
+
.describe("Six item names, one per grid cell"),
|
|
45
|
+
locale: z
|
|
46
|
+
.string()
|
|
47
|
+
.optional()
|
|
48
|
+
.describe("Current locale code for RTL support"),
|
|
49
|
+
strap: z
|
|
50
|
+
.string()
|
|
51
|
+
.default("A field guide for the feed")
|
|
52
|
+
.describe("Small line top-end"),
|
|
53
|
+
})
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
export type Props = z.infer<typeof propsSchema>;
|
|
57
|
+
|
|
58
|
+
const INK = "#121212";
|
|
59
|
+
const PAPER = "#efece4";
|
|
60
|
+
const COBALT = "#3046ff";
|
|
61
|
+
// Latin face first; Arabic glyphs fall through to the Arabic face.
|
|
62
|
+
const TITLE = "Anton, Lalezar, sans-serif";
|
|
63
|
+
const HEAVY = "Archivo Black, Lalezar, sans-serif";
|
|
64
|
+
const SERIF = "Instrument Serif, Amiri, serif";
|
|
65
|
+
const MONO = "Space Mono, IBM Plex Sans Arabic, monospace";
|
|
66
|
+
const CELLS_PER_ROW = 3;
|
|
67
|
+
const SPECIMEN_SIZE = 120;
|
|
68
|
+
|
|
69
|
+
const starburst = (spikes: number, outer: number, inner: number): string =>
|
|
70
|
+
Array.from({ length: spikes * 2 }, (_, index) => {
|
|
71
|
+
const radius = index % 2 === 0 ? outer : inner;
|
|
72
|
+
const angle = (Math.PI * index) / spikes - Math.PI / 2;
|
|
73
|
+
return `${50 + radius * Math.cos(angle)},${50 + radius * Math.sin(angle)}`;
|
|
74
|
+
}).join(" ");
|
|
75
|
+
|
|
76
|
+
const Sparkle = ({ color }: { color: string }) => (
|
|
77
|
+
<svg width="16" height="16" viewBox="0 0 24 24">
|
|
78
|
+
<path d="M12 0 15 9 24 12 15 15 12 24 9 15 0 12 9 9Z" fill={color} />
|
|
79
|
+
</svg>
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
/** One small specimen per cell, picked by position so any six labels work. */
|
|
83
|
+
const Specimen = ({
|
|
84
|
+
highlight,
|
|
85
|
+
index,
|
|
86
|
+
}: {
|
|
87
|
+
highlight: string;
|
|
88
|
+
index: number;
|
|
89
|
+
}) => {
|
|
90
|
+
switch (index % 6) {
|
|
91
|
+
case 0: {
|
|
92
|
+
return (
|
|
93
|
+
<div
|
|
94
|
+
tw="flex items-center justify-center rounded-full"
|
|
95
|
+
style={{
|
|
96
|
+
backgroundColor: COBALT,
|
|
97
|
+
height: SPECIMEN_SIZE,
|
|
98
|
+
width: SPECIMEN_SIZE,
|
|
99
|
+
}}
|
|
100
|
+
>
|
|
101
|
+
<span
|
|
102
|
+
style={{
|
|
103
|
+
color: PAPER,
|
|
104
|
+
fontFamily: HEAVY,
|
|
105
|
+
fontSize: 60,
|
|
106
|
+
lineHeight: 1,
|
|
107
|
+
}}
|
|
108
|
+
>
|
|
109
|
+
Aa
|
|
110
|
+
</span>
|
|
111
|
+
</div>
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
case 1: {
|
|
115
|
+
return (
|
|
116
|
+
<div
|
|
117
|
+
tw="flex"
|
|
118
|
+
style={{
|
|
119
|
+
backgroundImage: `radial-gradient(${INK} 34%, transparent 36%)`,
|
|
120
|
+
backgroundSize: "10px 10px",
|
|
121
|
+
borderRadius: "60px 60px 0 60px",
|
|
122
|
+
height: SPECIMEN_SIZE,
|
|
123
|
+
width: SPECIMEN_SIZE,
|
|
124
|
+
}}
|
|
125
|
+
/>
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
case 2: {
|
|
129
|
+
return (
|
|
130
|
+
<svg width={SPECIMEN_SIZE} height={SPECIMEN_SIZE} viewBox="0 0 100 100">
|
|
131
|
+
<rect width="100" height="50" fill={highlight} />
|
|
132
|
+
<rect y="50" width="100" height="50" fill={COBALT} />
|
|
133
|
+
<circle
|
|
134
|
+
cx="50"
|
|
135
|
+
cy="50"
|
|
136
|
+
r="24"
|
|
137
|
+
fill="#f5c3ff"
|
|
138
|
+
stroke={INK}
|
|
139
|
+
strokeWidth="3"
|
|
140
|
+
/>
|
|
141
|
+
</svg>
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
case 3: {
|
|
145
|
+
return (
|
|
146
|
+
<div
|
|
147
|
+
tw="flex relative"
|
|
148
|
+
style={{ height: SPECIMEN_SIZE, width: SPECIMEN_SIZE + 20 }}
|
|
149
|
+
>
|
|
150
|
+
<div
|
|
151
|
+
tw="flex absolute"
|
|
152
|
+
style={{
|
|
153
|
+
backgroundColor: COBALT,
|
|
154
|
+
height: 70,
|
|
155
|
+
left: 0,
|
|
156
|
+
top: 10,
|
|
157
|
+
transform: "rotate(-8deg)",
|
|
158
|
+
width: 90,
|
|
159
|
+
}}
|
|
160
|
+
/>
|
|
161
|
+
<div
|
|
162
|
+
tw="flex absolute"
|
|
163
|
+
style={{
|
|
164
|
+
backgroundColor: "#fff",
|
|
165
|
+
border: `2px solid ${INK}`,
|
|
166
|
+
height: 70,
|
|
167
|
+
left: 40,
|
|
168
|
+
top: 40,
|
|
169
|
+
transform: "rotate(6deg)",
|
|
170
|
+
width: 90,
|
|
171
|
+
}}
|
|
172
|
+
/>
|
|
173
|
+
<div
|
|
174
|
+
tw="flex absolute"
|
|
175
|
+
style={{
|
|
176
|
+
backgroundColor: highlight,
|
|
177
|
+
height: 18,
|
|
178
|
+
left: 60,
|
|
179
|
+
top: 32,
|
|
180
|
+
transform: "rotate(-20deg)",
|
|
181
|
+
width: 50,
|
|
182
|
+
}}
|
|
183
|
+
/>
|
|
184
|
+
</div>
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
case 4: {
|
|
188
|
+
return (
|
|
189
|
+
<svg width={SPECIMEN_SIZE} height={SPECIMEN_SIZE} viewBox="0 0 100 100">
|
|
190
|
+
<polygon
|
|
191
|
+
points={starburst(10, 48, 30)}
|
|
192
|
+
fill={highlight}
|
|
193
|
+
stroke={INK}
|
|
194
|
+
strokeWidth="3"
|
|
195
|
+
/>
|
|
196
|
+
<circle
|
|
197
|
+
cx="50"
|
|
198
|
+
cy="50"
|
|
199
|
+
r="13"
|
|
200
|
+
fill={PAPER}
|
|
201
|
+
stroke={INK}
|
|
202
|
+
strokeWidth="3"
|
|
203
|
+
/>
|
|
204
|
+
</svg>
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
default: {
|
|
208
|
+
return (
|
|
209
|
+
<svg width={SPECIMEN_SIZE} height={SPECIMEN_SIZE} viewBox="0 0 100 100">
|
|
210
|
+
<path d="M0 100 V50 A50 50 0 0 1 50 0 H100 V100Z" fill={INK} />
|
|
211
|
+
<circle cx="66" cy="66" r="20" fill={highlight} />
|
|
212
|
+
</svg>
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
const TrendPoster = ({
|
|
219
|
+
aside,
|
|
220
|
+
cta,
|
|
221
|
+
edition,
|
|
222
|
+
footer,
|
|
223
|
+
headline,
|
|
224
|
+
highlight,
|
|
225
|
+
issue,
|
|
226
|
+
items,
|
|
227
|
+
locale,
|
|
228
|
+
strap,
|
|
229
|
+
}: Props) => {
|
|
230
|
+
const isRtl = locale?.startsWith("ar") ?? false;
|
|
231
|
+
// Flex rows mirror themselves under dir="rtl"; absolute offsets don't.
|
|
232
|
+
const end = isRtl ? "left" : "right";
|
|
233
|
+
const rows = [
|
|
234
|
+
items.slice(0, CELLS_PER_ROW),
|
|
235
|
+
items.slice(CELLS_PER_ROW, CELLS_PER_ROW * 2),
|
|
236
|
+
];
|
|
237
|
+
|
|
238
|
+
return (
|
|
239
|
+
<div
|
|
240
|
+
lang={isRtl ? "ar" : undefined}
|
|
241
|
+
dir={isRtl ? "rtl" : "ltr"}
|
|
242
|
+
tw="flex flex-col w-full h-full px-12 py-10"
|
|
243
|
+
style={{ backgroundColor: PAPER, color: INK, fontFamily: MONO }}
|
|
244
|
+
>
|
|
245
|
+
<div
|
|
246
|
+
tw="flex items-center justify-between text-lg mb-3"
|
|
247
|
+
style={{ flexShrink: 0 }}
|
|
248
|
+
>
|
|
249
|
+
<div tw="flex items-center" style={{ gap: 12 }}>
|
|
250
|
+
<span style={{ fontWeight: 700 }}>STDOUT</span>
|
|
251
|
+
<Sparkle color={INK} />
|
|
252
|
+
<span>{issue.toUpperCase()}</span>
|
|
253
|
+
</div>
|
|
254
|
+
<span>{strap.toUpperCase()}</span>
|
|
255
|
+
</div>
|
|
256
|
+
|
|
257
|
+
<div tw="flex flex-col" style={{ flexShrink: 0 }}>
|
|
258
|
+
<span
|
|
259
|
+
style={{
|
|
260
|
+
fontFamily: TITLE,
|
|
261
|
+
fontSize: isRtl ? 92 : 96,
|
|
262
|
+
lineHeight: isRtl ? 1.2 : 0.9,
|
|
263
|
+
textTransform: "uppercase",
|
|
264
|
+
}}
|
|
265
|
+
>
|
|
266
|
+
{`${items.length} ${headline}`}
|
|
267
|
+
</span>
|
|
268
|
+
<div tw="flex items-end" style={{ gap: 24 }}>
|
|
269
|
+
<span
|
|
270
|
+
style={{
|
|
271
|
+
WebkitTextStroke: `3px ${INK}`,
|
|
272
|
+
color: highlight,
|
|
273
|
+
fontFamily: HEAVY,
|
|
274
|
+
fontSize: 104,
|
|
275
|
+
letterSpacing: isRtl ? 0 : "-0.04em",
|
|
276
|
+
lineHeight: 1,
|
|
277
|
+
textShadow: `5px 5px 0 ${INK}`,
|
|
278
|
+
}}
|
|
279
|
+
>
|
|
280
|
+
{edition}
|
|
281
|
+
</span>
|
|
282
|
+
<span
|
|
283
|
+
tw="mb-3"
|
|
284
|
+
style={{
|
|
285
|
+
fontFamily: SERIF,
|
|
286
|
+
fontSize: 42,
|
|
287
|
+
fontStyle: isRtl ? "normal" : "italic",
|
|
288
|
+
}}
|
|
289
|
+
>
|
|
290
|
+
{aside}
|
|
291
|
+
</span>
|
|
292
|
+
</div>
|
|
293
|
+
</div>
|
|
294
|
+
|
|
295
|
+
<div tw="flex flex-col flex-1 mt-5" style={{ minHeight: 0 }}>
|
|
296
|
+
{rows.map((row, rowIndex) => (
|
|
297
|
+
<div
|
|
298
|
+
key={`row-${rowIndex}`}
|
|
299
|
+
tw="flex flex-1"
|
|
300
|
+
style={{ borderTop: `3px solid ${INK}`, minHeight: 0 }}
|
|
301
|
+
>
|
|
302
|
+
{row.map((item, cellIndex) => {
|
|
303
|
+
const index = rowIndex * CELLS_PER_ROW + cellIndex;
|
|
304
|
+
return (
|
|
305
|
+
<div
|
|
306
|
+
key={item}
|
|
307
|
+
tw="flex flex-col flex-1 pt-3 relative overflow-hidden"
|
|
308
|
+
style={{
|
|
309
|
+
[isRtl ? "borderRight" : "borderLeft"]:
|
|
310
|
+
cellIndex === 0 ? "none" : `2px solid ${INK}`,
|
|
311
|
+
[isRtl ? "paddingRight" : "paddingLeft"]:
|
|
312
|
+
cellIndex === 0 ? 0 : 18,
|
|
313
|
+
}}
|
|
314
|
+
>
|
|
315
|
+
{/* Anchored to the bottom corner: short canvases crop it
|
|
316
|
+
like a print bleed. Rendered first so the label paints
|
|
317
|
+
on top if they meet. */}
|
|
318
|
+
<div tw="flex absolute" style={{ bottom: 14, [end]: 18 }}>
|
|
319
|
+
<Specimen highlight={highlight} index={index} />
|
|
320
|
+
</div>
|
|
321
|
+
<div tw="flex items-baseline relative" style={{ gap: 12 }}>
|
|
322
|
+
<span tw="text-2xl" style={{ fontWeight: 700 }}>
|
|
323
|
+
{String(index + 1).padStart(2, "0")}
|
|
324
|
+
</span>
|
|
325
|
+
<span
|
|
326
|
+
tw="px-1"
|
|
327
|
+
style={{
|
|
328
|
+
backgroundColor: PAPER,
|
|
329
|
+
fontFamily: HEAVY,
|
|
330
|
+
fontSize: 20,
|
|
331
|
+
textTransform: "uppercase",
|
|
332
|
+
}}
|
|
333
|
+
>
|
|
334
|
+
{item}
|
|
335
|
+
</span>
|
|
336
|
+
</div>
|
|
337
|
+
</div>
|
|
338
|
+
);
|
|
339
|
+
})}
|
|
340
|
+
</div>
|
|
341
|
+
))}
|
|
342
|
+
</div>
|
|
343
|
+
|
|
344
|
+
<div
|
|
345
|
+
tw="flex items-center justify-between pt-5"
|
|
346
|
+
style={{ borderTop: `3px solid ${INK}`, flexShrink: 0 }}
|
|
347
|
+
>
|
|
348
|
+
<span style={{ fontFamily: SERIF, fontSize: 40, lineHeight: 1.1 }}>
|
|
349
|
+
{footer}
|
|
350
|
+
</span>
|
|
351
|
+
<div
|
|
352
|
+
tw="flex items-center px-6 py-3 rounded-full"
|
|
353
|
+
style={{
|
|
354
|
+
backgroundColor: highlight,
|
|
355
|
+
border: `3px solid ${INK}`,
|
|
356
|
+
fontFamily: HEAVY,
|
|
357
|
+
fontSize: 20,
|
|
358
|
+
gap: 12,
|
|
359
|
+
}}
|
|
360
|
+
>
|
|
361
|
+
<span>{cta.toUpperCase()}</span>
|
|
362
|
+
<svg
|
|
363
|
+
width="22"
|
|
364
|
+
height="22"
|
|
365
|
+
viewBox="0 0 24 24"
|
|
366
|
+
fill="none"
|
|
367
|
+
style={{ transform: isRtl ? "scaleX(-1)" : "none" }}
|
|
368
|
+
>
|
|
369
|
+
<path
|
|
370
|
+
d="M4 12h15M13 5l7 7-7 7"
|
|
371
|
+
stroke={INK}
|
|
372
|
+
strokeWidth="3"
|
|
373
|
+
strokeLinecap="round"
|
|
374
|
+
strokeLinejoin="round"
|
|
375
|
+
/>
|
|
376
|
+
</svg>
|
|
377
|
+
</div>
|
|
378
|
+
</div>
|
|
379
|
+
</div>
|
|
380
|
+
);
|
|
381
|
+
};
|
|
382
|
+
|
|
383
|
+
export default TrendPoster;
|