@stdout-design/cli 0.5.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 +14 -24
- 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,371 @@
|
|
|
1
|
+
import { defineSchema } from "@stdout-design/cli/schema";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
|
|
4
|
+
export const templateId = "retro-poster" as const;
|
|
5
|
+
|
|
6
|
+
export const propsSchema = defineSchema(
|
|
7
|
+
z.object({
|
|
8
|
+
body: z
|
|
9
|
+
.string()
|
|
10
|
+
.default(
|
|
11
|
+
"Ninety minutes, two sides, zero skips. Hand-picked tracks, hand-written labels, and a pencil for when the tape gets tangled."
|
|
12
|
+
)
|
|
13
|
+
.describe("Short paragraph beside the target mark"),
|
|
14
|
+
headline: z
|
|
15
|
+
.string()
|
|
16
|
+
.min(1)
|
|
17
|
+
.default("Mixtape Club")
|
|
18
|
+
.describe("Stacked headline in the bottom corner — one word per line"),
|
|
19
|
+
label: z
|
|
20
|
+
.string()
|
|
21
|
+
.default("Summer mix '89")
|
|
22
|
+
.describe("Handwritten title on the cassette label"),
|
|
23
|
+
locale: z
|
|
24
|
+
.string()
|
|
25
|
+
.optional()
|
|
26
|
+
.describe("Current locale code for RTL support"),
|
|
27
|
+
paper: z.string().default("#efe6d2").describe("Poster paper color"),
|
|
28
|
+
stamp: z
|
|
29
|
+
.string()
|
|
30
|
+
.default("Limited edition")
|
|
31
|
+
.describe("Rubber-stamp headline"),
|
|
32
|
+
stampNote: z
|
|
33
|
+
.string()
|
|
34
|
+
.default("No. 0042 / 5000")
|
|
35
|
+
.describe("Small line inside the stamp"),
|
|
36
|
+
tagline: z
|
|
37
|
+
.string()
|
|
38
|
+
.default("Analog since forever")
|
|
39
|
+
.describe("Small line under the headline"),
|
|
40
|
+
title: z
|
|
41
|
+
.string()
|
|
42
|
+
.default("Press Play")
|
|
43
|
+
.describe("Oversized title across the top"),
|
|
44
|
+
})
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
export type Props = z.infer<typeof propsSchema>;
|
|
48
|
+
|
|
49
|
+
const INK = "#1d1a17";
|
|
50
|
+
const SHELL = "#2e2b35";
|
|
51
|
+
const RUST = "#d9532c";
|
|
52
|
+
// Latin face first; Arabic glyphs fall through to the Arabic face.
|
|
53
|
+
const TITLE = "Anton, Lalezar, sans-serif";
|
|
54
|
+
const DISPLAY = "Archivo Black, Lalezar, sans-serif";
|
|
55
|
+
const SCRIPT = "Instrument Serif, Amiri, serif";
|
|
56
|
+
const MONO = "Space Mono, IBM Plex Sans Arabic, monospace";
|
|
57
|
+
const STRIPES = ["#7a2e3a", RUST, "#f0a830", "#2f8f83"];
|
|
58
|
+
const BARCODE = [
|
|
59
|
+
3, 1, 2, 1, 1, 3, 1, 2, 2, 1, 3, 1, 1, 2, 1, 3, 2, 1, 1, 2, 3, 1, 2, 1,
|
|
60
|
+
];
|
|
61
|
+
const BAR_UNIT = 1.6;
|
|
62
|
+
// Fixed, not fluid: on short canvases the stripes simply run off the
|
|
63
|
+
// bottom edge instead of the cassette squashing.
|
|
64
|
+
const CASSETTE_WIDTH = 420;
|
|
65
|
+
const CASSETTE_HEIGHT = 270;
|
|
66
|
+
const REEL_TEETH = [0, 60, 120, 180, 240, 300];
|
|
67
|
+
|
|
68
|
+
const Reel = ({ cx }: { cx: number }) => (
|
|
69
|
+
<g>
|
|
70
|
+
<circle cx={cx} cy="0" r="26" fill="#f6efe0" stroke={INK} strokeWidth="3" />
|
|
71
|
+
<circle cx={cx} cy="0" r="12" fill={SHELL} />
|
|
72
|
+
{REEL_TEETH.map((angle) => (
|
|
73
|
+
<rect
|
|
74
|
+
key={angle}
|
|
75
|
+
x={cx - 2.5}
|
|
76
|
+
y="-12"
|
|
77
|
+
width="5"
|
|
78
|
+
height="6"
|
|
79
|
+
fill="#f6efe0"
|
|
80
|
+
transform={`rotate(${angle} ${cx} 0)`}
|
|
81
|
+
/>
|
|
82
|
+
))}
|
|
83
|
+
</g>
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
const Cassette = ({ label, isRtl }: { label: string; isRtl: boolean }) => (
|
|
87
|
+
<div
|
|
88
|
+
tw="flex relative"
|
|
89
|
+
style={{
|
|
90
|
+
flexShrink: 0,
|
|
91
|
+
height: CASSETTE_HEIGHT,
|
|
92
|
+
transform: `rotate(${isRtl ? 6 : -6}deg)`,
|
|
93
|
+
width: CASSETTE_WIDTH,
|
|
94
|
+
}}
|
|
95
|
+
>
|
|
96
|
+
<svg
|
|
97
|
+
width={CASSETTE_WIDTH}
|
|
98
|
+
height={CASSETTE_HEIGHT}
|
|
99
|
+
viewBox="0 0 420 270"
|
|
100
|
+
style={{ left: 0, position: "absolute", top: 0 }}
|
|
101
|
+
>
|
|
102
|
+
{/* Offset print shadow, then the shell */}
|
|
103
|
+
<rect x="12" y="12" width="406" height="256" rx="22" fill={INK} />
|
|
104
|
+
<rect
|
|
105
|
+
x="2"
|
|
106
|
+
y="2"
|
|
107
|
+
width="406"
|
|
108
|
+
height="256"
|
|
109
|
+
rx="22"
|
|
110
|
+
fill={SHELL}
|
|
111
|
+
stroke={INK}
|
|
112
|
+
strokeWidth="4"
|
|
113
|
+
/>
|
|
114
|
+
{[22, 386].map((x) => (
|
|
115
|
+
<g key={x}>
|
|
116
|
+
<circle cx={x} cy="22" r="6" fill="#1a181f" />
|
|
117
|
+
<circle cx={x} cy="238" r="6" fill="#1a181f" />
|
|
118
|
+
</g>
|
|
119
|
+
))}
|
|
120
|
+
{/* Label */}
|
|
121
|
+
<rect x="34" y="28" width="342" height="150" rx="10" fill="#f6efe0" />
|
|
122
|
+
<rect x="34" y="28" width="342" height="26" rx="10" fill={RUST} />
|
|
123
|
+
<rect x="34" y="44" width="342" height="10" fill={RUST} />
|
|
124
|
+
{/* Window */}
|
|
125
|
+
<rect
|
|
126
|
+
x="110"
|
|
127
|
+
y="96"
|
|
128
|
+
width="190"
|
|
129
|
+
height="62"
|
|
130
|
+
rx="31"
|
|
131
|
+
fill={SHELL}
|
|
132
|
+
stroke={INK}
|
|
133
|
+
strokeWidth="3"
|
|
134
|
+
/>
|
|
135
|
+
<rect x="150" y="112" width="110" height="30" fill="#6b5a4a" />
|
|
136
|
+
<g transform="translate(0 127)">
|
|
137
|
+
<Reel cx={142} />
|
|
138
|
+
<Reel cx={268} />
|
|
139
|
+
</g>
|
|
140
|
+
{/* Bottom bevel */}
|
|
141
|
+
<path
|
|
142
|
+
d="M86 258 L110 206 H300 L324 258 Z"
|
|
143
|
+
fill="#3a3642"
|
|
144
|
+
stroke={INK}
|
|
145
|
+
strokeWidth="3"
|
|
146
|
+
/>
|
|
147
|
+
{[150, 182, 228, 260].map((x) => (
|
|
148
|
+
<circle key={x} cx={x} cy="232" r="6" fill="#1a181f" />
|
|
149
|
+
))}
|
|
150
|
+
</svg>
|
|
151
|
+
{/* Text overlays sit outside the SVG so they use the loaded fonts. */}
|
|
152
|
+
<span
|
|
153
|
+
tw="absolute"
|
|
154
|
+
style={{
|
|
155
|
+
color: "#fff6e5",
|
|
156
|
+
fontFamily: DISPLAY,
|
|
157
|
+
fontSize: 18,
|
|
158
|
+
left: 46,
|
|
159
|
+
top: 29,
|
|
160
|
+
}}
|
|
161
|
+
>
|
|
162
|
+
A
|
|
163
|
+
</span>
|
|
164
|
+
<span
|
|
165
|
+
tw="absolute"
|
|
166
|
+
style={{
|
|
167
|
+
color: "#fff6e5",
|
|
168
|
+
fontFamily: MONO,
|
|
169
|
+
fontSize: 14,
|
|
170
|
+
right: 58,
|
|
171
|
+
top: 32,
|
|
172
|
+
}}
|
|
173
|
+
>
|
|
174
|
+
C-90
|
|
175
|
+
</span>
|
|
176
|
+
<div
|
|
177
|
+
tw="flex absolute justify-center"
|
|
178
|
+
style={{ left: 40, right: 50, top: 54 }}
|
|
179
|
+
>
|
|
180
|
+
<span
|
|
181
|
+
style={{
|
|
182
|
+
color: INK,
|
|
183
|
+
fontFamily: SCRIPT,
|
|
184
|
+
fontSize: 36,
|
|
185
|
+
fontStyle: isRtl ? "normal" : "italic",
|
|
186
|
+
lineHeight: 1.1,
|
|
187
|
+
}}
|
|
188
|
+
>
|
|
189
|
+
{label}
|
|
190
|
+
</span>
|
|
191
|
+
</div>
|
|
192
|
+
</div>
|
|
193
|
+
);
|
|
194
|
+
|
|
195
|
+
const Barcode = () => {
|
|
196
|
+
let x = 0;
|
|
197
|
+
const bars: { x: number; width: number }[] = [];
|
|
198
|
+
for (const [index, units] of BARCODE.entries()) {
|
|
199
|
+
if (index % 2 === 0) {
|
|
200
|
+
bars.push({ width: units * BAR_UNIT, x });
|
|
201
|
+
}
|
|
202
|
+
x += units * BAR_UNIT + 0.8;
|
|
203
|
+
}
|
|
204
|
+
return (
|
|
205
|
+
<svg width="120" height="34" viewBox="0 0 120 34">
|
|
206
|
+
{bars.map((bar) => (
|
|
207
|
+
<rect
|
|
208
|
+
key={bar.x}
|
|
209
|
+
x={bar.x}
|
|
210
|
+
y="0"
|
|
211
|
+
width={bar.width}
|
|
212
|
+
height="34"
|
|
213
|
+
fill={INK}
|
|
214
|
+
/>
|
|
215
|
+
))}
|
|
216
|
+
</svg>
|
|
217
|
+
);
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
const RetroPoster = ({
|
|
221
|
+
body,
|
|
222
|
+
headline,
|
|
223
|
+
label,
|
|
224
|
+
locale,
|
|
225
|
+
paper,
|
|
226
|
+
stamp,
|
|
227
|
+
stampNote,
|
|
228
|
+
tagline,
|
|
229
|
+
title,
|
|
230
|
+
}: Props) => {
|
|
231
|
+
const isRtl = locale?.startsWith("ar") ?? false;
|
|
232
|
+
// Flex rows mirror themselves under dir="rtl"; absolute offsets don't.
|
|
233
|
+
const start = isRtl ? "right" : "left";
|
|
234
|
+
const words = headline.split(/\s+/u).filter(Boolean);
|
|
235
|
+
|
|
236
|
+
return (
|
|
237
|
+
<div
|
|
238
|
+
lang={isRtl ? "ar" : undefined}
|
|
239
|
+
dir={isRtl ? "rtl" : "ltr"}
|
|
240
|
+
tw="flex flex-col w-full h-full px-12 pt-10 relative overflow-hidden"
|
|
241
|
+
style={{ backgroundColor: paper, color: INK, fontFamily: MONO }}
|
|
242
|
+
>
|
|
243
|
+
{/* Paper grain */}
|
|
244
|
+
<div
|
|
245
|
+
tw="absolute inset-0"
|
|
246
|
+
style={{
|
|
247
|
+
backgroundImage:
|
|
248
|
+
"radial-gradient(rgba(29,26,23,0.07) 1px, transparent 1.5px)",
|
|
249
|
+
backgroundSize: "7px 7px",
|
|
250
|
+
}}
|
|
251
|
+
/>
|
|
252
|
+
|
|
253
|
+
<span
|
|
254
|
+
tw="relative"
|
|
255
|
+
style={{
|
|
256
|
+
fontFamily: TITLE,
|
|
257
|
+
fontSize: isRtl ? 104 : 124,
|
|
258
|
+
lineHeight: isRtl ? 1.1 : 0.9,
|
|
259
|
+
textTransform: "uppercase",
|
|
260
|
+
}}
|
|
261
|
+
>
|
|
262
|
+
{title}
|
|
263
|
+
</span>
|
|
264
|
+
<div
|
|
265
|
+
tw="flex relative my-5"
|
|
266
|
+
style={{ backgroundColor: INK, flexShrink: 0, height: 4 }}
|
|
267
|
+
/>
|
|
268
|
+
|
|
269
|
+
{/* minHeight 0: the fixed-size cassette and stripes may overflow this
|
|
270
|
+
row, but must not stretch it (and the text column) past the canvas. */}
|
|
271
|
+
<div tw="flex flex-1 relative" style={{ gap: 40, minHeight: 0 }}>
|
|
272
|
+
{/* Tape stripes run out of the cassette and off the page */}
|
|
273
|
+
<div
|
|
274
|
+
tw="flex absolute"
|
|
275
|
+
style={{ bottom: 0, [start]: 120, top: CASSETTE_HEIGHT - 50 }}
|
|
276
|
+
>
|
|
277
|
+
{STRIPES.map((color) => (
|
|
278
|
+
<div
|
|
279
|
+
key={color}
|
|
280
|
+
tw="h-full w-10"
|
|
281
|
+
style={{ backgroundColor: color }}
|
|
282
|
+
/>
|
|
283
|
+
))}
|
|
284
|
+
</div>
|
|
285
|
+
|
|
286
|
+
<div tw="flex pt-2">
|
|
287
|
+
<Cassette label={label} isRtl={isRtl} />
|
|
288
|
+
</div>
|
|
289
|
+
|
|
290
|
+
<div tw="flex flex-col flex-1 justify-between items-end pb-8 relative">
|
|
291
|
+
<div tw="flex items-start" style={{ gap: 28 }}>
|
|
292
|
+
<div
|
|
293
|
+
tw="flex rounded-full"
|
|
294
|
+
style={{
|
|
295
|
+
backgroundImage: `repeating-radial-gradient(circle, ${INK} 0 8px, transparent 8px 16px)`,
|
|
296
|
+
flexShrink: 0,
|
|
297
|
+
height: 104,
|
|
298
|
+
width: 104,
|
|
299
|
+
}}
|
|
300
|
+
/>
|
|
301
|
+
<div
|
|
302
|
+
tw="flex text-base"
|
|
303
|
+
style={{
|
|
304
|
+
[isRtl ? "borderRight" : "borderLeft"]: `3px solid ${INK}`,
|
|
305
|
+
[isRtl ? "paddingRight" : "paddingLeft"]: 20,
|
|
306
|
+
lineHeight: 1.55,
|
|
307
|
+
width: 330,
|
|
308
|
+
}}
|
|
309
|
+
>
|
|
310
|
+
{body}
|
|
311
|
+
</div>
|
|
312
|
+
</div>
|
|
313
|
+
|
|
314
|
+
<div
|
|
315
|
+
tw="flex flex-col absolute px-5 py-3"
|
|
316
|
+
style={{
|
|
317
|
+
border: `4px solid ${RUST}`,
|
|
318
|
+
color: RUST,
|
|
319
|
+
opacity: 0.92,
|
|
320
|
+
[start]: 20,
|
|
321
|
+
top: "40%",
|
|
322
|
+
transform: `rotate(${isRtl ? 8 : -8}deg)`,
|
|
323
|
+
}}
|
|
324
|
+
>
|
|
325
|
+
<span
|
|
326
|
+
style={{
|
|
327
|
+
fontFamily: DISPLAY,
|
|
328
|
+
fontSize: 30,
|
|
329
|
+
lineHeight: 1.1,
|
|
330
|
+
textTransform: "uppercase",
|
|
331
|
+
}}
|
|
332
|
+
>
|
|
333
|
+
{stamp}
|
|
334
|
+
</span>
|
|
335
|
+
<span
|
|
336
|
+
tw="text-base mt-1"
|
|
337
|
+
style={{ letterSpacing: isRtl ? 0 : "0.2em" }}
|
|
338
|
+
>
|
|
339
|
+
{stampNote}
|
|
340
|
+
</span>
|
|
341
|
+
</div>
|
|
342
|
+
|
|
343
|
+
<div tw="flex flex-col items-end">
|
|
344
|
+
{words.map((word, index) => (
|
|
345
|
+
<span
|
|
346
|
+
key={`${word}-${index}`}
|
|
347
|
+
style={{
|
|
348
|
+
fontFamily: DISPLAY,
|
|
349
|
+
fontSize: isRtl ? 84 : 92,
|
|
350
|
+
letterSpacing: isRtl ? 0 : "-0.04em",
|
|
351
|
+
lineHeight: isRtl ? 1.15 : 0.9,
|
|
352
|
+
textTransform: "uppercase",
|
|
353
|
+
}}
|
|
354
|
+
>
|
|
355
|
+
{word}
|
|
356
|
+
</span>
|
|
357
|
+
))}
|
|
358
|
+
<div tw="flex items-center mt-4" style={{ gap: 20 }}>
|
|
359
|
+
<Barcode />
|
|
360
|
+
<span tw="text-2xl" style={{ fontWeight: 700 }}>
|
|
361
|
+
{tagline}
|
|
362
|
+
</span>
|
|
363
|
+
</div>
|
|
364
|
+
</div>
|
|
365
|
+
</div>
|
|
366
|
+
</div>
|
|
367
|
+
</div>
|
|
368
|
+
);
|
|
369
|
+
};
|
|
370
|
+
|
|
371
|
+
export default RetroPoster;
|