@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.
@@ -7,72 +7,417 @@ export const propsSchema = defineSchema(
7
7
  z.object({
8
8
  accentColor: z
9
9
  .string()
10
- .default("#6366f1")
11
- .describe("The primary accent color used for borders and highlights"),
10
+ .default("#ff5a36")
11
+ .describe("The primary accent color used for the solid tiles"),
12
12
  background: z
13
13
  .string()
14
- .default("#0a0a0b")
15
- .describe("Background color of the card"),
14
+ .default("#191714")
15
+ .describe("Background color behind the tiles"),
16
+ brand: z.string().default("tally").describe("Wordmark in the logo tile"),
17
+ chartLabel: z
18
+ .string()
19
+ .default("Spending by week")
20
+ .describe("Caption of the bar chart tile"),
16
21
  description: z
17
22
  .string()
18
- .default("A beautiful description goes here.")
23
+ .default("Every card, every bill, sorted the second it lands.")
19
24
  .describe("Supporting description text"),
25
+ gaugeLabel: z
26
+ .string()
27
+ .default("Budget used")
28
+ .describe("Caption of the gauge tile"),
29
+ gaugePercent: z
30
+ .number()
31
+ .min(0)
32
+ .max(100)
33
+ .default(72)
34
+ .describe("How full the gauge is, 0–100"),
20
35
  headline: z
21
36
  .string()
22
37
  .min(1)
23
- .default("Featured App")
38
+ .default("Know where every dollar goes")
24
39
  .describe("Main headline for the feature card"),
25
40
  locale: z
26
41
  .string()
27
42
  .optional()
28
43
  .describe("Current locale code for RTL support"),
44
+ metricDelta: z
45
+ .string()
46
+ .default("+18%")
47
+ .describe("Change shown next to the metric"),
48
+ metricLabel: z
49
+ .string()
50
+ .default("Saved this month")
51
+ .describe("Caption of the big metric"),
52
+ metricValue: z.string().default("$1,240").describe("The big metric"),
53
+ rating: z.string().default("4.9").describe("Rating in the reviews tile"),
54
+ ratingLabel: z
55
+ .string()
56
+ .default("App Store rating")
57
+ .describe("Caption of the rating"),
58
+ receipt: z
59
+ .array(z.string())
60
+ .default(["Coffee 4.50", "Groceries 62.10", "Metro 2.75"])
61
+ .describe("Receipt lines — the last word is the amount"),
62
+ receiptNote: z
63
+ .string()
64
+ .default("Sorted automatically")
65
+ .describe("Pill under the receipt"),
29
66
  })
30
67
  );
31
68
 
32
69
  export type Props = z.infer<typeof propsSchema>;
33
70
 
71
+ const CREAM = "#f4ede1";
72
+ const INK = "#1f1b16";
73
+ const BUTTER = "#ffd166";
74
+ const SAGE = "#9fd8b3";
75
+ // Latin face first; Arabic glyphs fall through to the Arabic face.
76
+ const FONT = "Bricolage Grotesque, IBM Plex Sans Arabic, sans-serif";
77
+ const MONO = "Space Mono, IBM Plex Sans Arabic, monospace";
78
+ const GAP = 20;
79
+ const WEEKS = [
80
+ [40, 22, 18],
81
+ [52, 30, 12],
82
+ [34, 26, 20],
83
+ [60, 18, 16],
84
+ [46, 34, 10],
85
+ ];
86
+ const AVATARS = [
87
+ { bg: SAGE, initials: "AK" },
88
+ { bg: BUTTER, initials: "MJ" },
89
+ { bg: "#ffb4a2", initials: "RS" },
90
+ ];
91
+ const GAUGE_RADIUS = 40;
92
+ const GAUGE_LENGTH = Math.PI * GAUGE_RADIUS;
93
+
94
+ const Tile = ({
95
+ children,
96
+ color = CREAM,
97
+ grow,
98
+ }: {
99
+ children: React.ReactNode;
100
+ color?: string;
101
+ grow: number;
102
+ }) => (
103
+ <div
104
+ tw="flex flex-col p-7 rounded-[32px] relative overflow-hidden"
105
+ style={{
106
+ backgroundColor: color,
107
+ flexBasis: 0,
108
+ flexGrow: grow,
109
+ minHeight: 0,
110
+ }}
111
+ >
112
+ {children}
113
+ </div>
114
+ );
115
+
116
+ const Column = ({ children }: { children: React.ReactNode }) => (
117
+ <div tw="flex flex-col flex-1" style={{ flexBasis: 0, gap: GAP }}>
118
+ {children}
119
+ </div>
120
+ );
121
+
122
+ /** A cheerful coin mascot — drawn, so it needs no image fetch. */
123
+ const Coin = ({ color }: { color: string }) => (
124
+ <svg width="150" height="150" viewBox="0 0 100 100">
125
+ <ellipse cx="54" cy="54" rx="42" ry="42" fill={INK} />
126
+ <circle cx="50" cy="50" r="42" fill={BUTTER} stroke={INK} strokeWidth="4" />
127
+ <circle
128
+ cx="50"
129
+ cy="50"
130
+ r="31"
131
+ fill="none"
132
+ stroke={INK}
133
+ strokeWidth="3"
134
+ strokeDasharray="4 5"
135
+ />
136
+ <circle cx="39" cy="44" r="5" fill={INK} />
137
+ <circle cx="61" cy="44" r="5" fill={INK} />
138
+ <path
139
+ d="M36 58 Q50 72 64 58"
140
+ fill="none"
141
+ stroke={INK}
142
+ strokeWidth="5"
143
+ strokeLinecap="round"
144
+ />
145
+ <circle cx="31" cy="58" r="5" fill={color} opacity="0.7" />
146
+ <circle cx="69" cy="58" r="5" fill={color} opacity="0.7" />
147
+ </svg>
148
+ );
149
+
34
150
  const BentoFeature = ({
35
- headline,
36
- description,
37
151
  accentColor,
38
152
  background,
153
+ brand,
154
+ chartLabel,
155
+ description,
156
+ gaugeLabel,
157
+ gaugePercent,
158
+ headline,
39
159
  locale,
160
+ metricDelta,
161
+ metricLabel,
162
+ metricValue,
163
+ rating,
164
+ ratingLabel,
165
+ receipt,
166
+ receiptNote,
40
167
  }: Props) => {
41
168
  const isRtl = locale?.startsWith("ar") ?? false;
169
+ const tracking = (em: number) => (isRtl ? 0 : `${em}em`);
42
170
 
43
171
  return (
44
172
  <div
45
173
  lang={isRtl ? "ar" : undefined}
46
174
  dir={isRtl ? "rtl" : "ltr"}
47
- tw="flex w-full h-full p-8"
175
+ tw="flex w-full h-full p-10"
48
176
  style={{
49
177
  backgroundColor: background,
50
- color: "#ffffff",
51
- fontFamily: "system-ui, sans-serif",
178
+ color: INK,
179
+ fontFamily: FONT,
180
+ gap: GAP,
52
181
  }}
53
182
  >
54
- <div
55
- tw="flex flex-col justify-center items-center w-full h-full rounded-[32px] border-2 p-12"
56
- style={{ backgroundColor: `${background}80`, borderColor: accentColor }}
57
- >
58
- <div
59
- tw="flex items-center justify-center w-16 h-16 rounded-full mb-6"
60
- style={{ backgroundColor: accentColor }}
61
- >
62
- <div tw="w-6 h-6 rounded-md bg-white" />
63
- </div>
64
- <h2
65
- tw="text-4xl font-bold tracking-tight text-center mb-2"
66
- style={{ color: "#ffffff" }}
67
- >
68
- {headline}
69
- </h2>
70
- {description ? (
71
- <p tw="text-xl text-center max-w-md" style={{ color: "#a1a1aa" }}>
183
+ <Column>
184
+ <Tile color={accentColor} grow={0.75}>
185
+ <div tw="flex flex-1 items-center" style={{ gap: 10 }}>
186
+ <span
187
+ dir="ltr"
188
+ style={{
189
+ color: CREAM,
190
+ fontSize: 88,
191
+ fontWeight: 700,
192
+ letterSpacing: "-0.05em",
193
+ lineHeight: 1,
194
+ }}
195
+ >
196
+ {brand}
197
+ </span>
198
+ <div tw="flex" style={{ gap: 6 }}>
199
+ <div
200
+ tw="w-5 h-5 rounded-full"
201
+ style={{ backgroundColor: BUTTER }}
202
+ />
203
+ <div
204
+ tw="w-5 h-5 rounded-full"
205
+ style={{ backgroundColor: CREAM }}
206
+ />
207
+ </div>
208
+ </div>
209
+ </Tile>
210
+ <Tile grow={1}>
211
+ <span tw="text-2xl">{metricLabel}</span>
212
+ <div tw="flex items-end mt-auto" style={{ gap: 12 }}>
213
+ <span
214
+ style={{
215
+ fontSize: 76,
216
+ fontWeight: 700,
217
+ letterSpacing: tracking(-0.04),
218
+ lineHeight: 1,
219
+ whiteSpace: "nowrap",
220
+ }}
221
+ >
222
+ {metricValue}
223
+ </span>
224
+ <span
225
+ dir="ltr"
226
+ tw="px-3 py-1 rounded-full text-xl mb-2"
227
+ style={{ backgroundColor: SAGE, fontWeight: 700 }}
228
+ >
229
+ {metricDelta}
230
+ </span>
231
+ </div>
232
+ </Tile>
233
+ <Tile color={BUTTER} grow={1.2}>
234
+ <span tw="text-xl">{chartLabel}</span>
235
+ <div
236
+ tw="flex flex-1 items-end mt-4"
237
+ style={{ gap: 12, minHeight: 0 }}
238
+ >
239
+ {WEEKS.map((week, index) => (
240
+ <div
241
+ key={`week-${index}`}
242
+ tw="flex flex-col flex-1 justify-end"
243
+ style={{ height: "100%" }}
244
+ >
245
+ {week.map((share, part) => (
246
+ <div
247
+ key={`part-${part}`}
248
+ tw="flex w-full"
249
+ style={{
250
+ backgroundColor: [INK, accentColor, CREAM][part],
251
+ borderRadius: part === 0 ? "10px 10px 0 0" : 0,
252
+ height: `${share}%`,
253
+ }}
254
+ />
255
+ ))}
256
+ </div>
257
+ ))}
258
+ </div>
259
+ </Tile>
260
+ </Column>
261
+
262
+ <Column>
263
+ <Tile grow={0.85}>
264
+ <div tw="flex items-end" style={{ gap: 10 }}>
265
+ <span style={{ fontSize: 72, fontWeight: 700, lineHeight: 1 }}>
266
+ {rating}
267
+ </span>
268
+ <svg
269
+ width="40"
270
+ height="40"
271
+ viewBox="0 0 24 24"
272
+ style={{ marginBottom: 8 }}
273
+ >
274
+ <path
275
+ d="M12 1 15.1 8.3 23 9 17 14.2 18.8 22 12 17.9 5.2 22 7 14.2 1 9 8.9 8.3Z"
276
+ fill={accentColor}
277
+ />
278
+ </svg>
279
+ </div>
280
+ <span tw="text-lg mt-1" style={{ color: "#6f675b" }}>
281
+ {ratingLabel}
282
+ </span>
283
+ <div tw="flex mt-auto">
284
+ {AVATARS.map((avatar, index) => (
285
+ <div
286
+ key={avatar.initials}
287
+ tw="flex items-center justify-center w-16 h-16 rounded-full text-xl"
288
+ style={{
289
+ backgroundColor: avatar.bg,
290
+ border: `4px solid ${CREAM}`,
291
+ fontWeight: 700,
292
+ [isRtl ? "marginRight" : "marginLeft"]: index === 0 ? 0 : -16,
293
+ }}
294
+ >
295
+ {avatar.initials}
296
+ </div>
297
+ ))}
298
+ </div>
299
+ </Tile>
300
+ <Tile grow={2}>
301
+ <span
302
+ style={{
303
+ fontSize: 50,
304
+ fontWeight: 700,
305
+ letterSpacing: tracking(-0.03),
306
+ lineHeight: isRtl ? 1.3 : 1.05,
307
+ }}
308
+ >
309
+ {headline}
310
+ </span>
311
+ <span tw="text-xl mt-4" style={{ color: "#6f675b", lineHeight: 1.4 }}>
72
312
  {description}
73
- </p>
74
- ) : null}
75
- </div>
313
+ </span>
314
+ <div
315
+ tw="flex flex-1 items-end justify-center"
316
+ style={{ minHeight: 0 }}
317
+ >
318
+ <Coin color={accentColor} />
319
+ </div>
320
+ </Tile>
321
+ </Column>
322
+
323
+ <Column>
324
+ <Tile color={SAGE} grow={1.25}>
325
+ <div
326
+ tw="flex flex-col px-6 pt-6 pb-8"
327
+ style={{
328
+ backgroundColor: "#fffdf8",
329
+ boxShadow: "0 14px 30px rgba(31,27,22,0.18)",
330
+ fontFamily: MONO,
331
+ gap: 10,
332
+ transform: `rotate(${isRtl ? 3 : -3}deg)`,
333
+ }}
334
+ >
335
+ <span tw="text-sm" style={{ letterSpacing: isRtl ? 0 : "0.2em" }}>
336
+ {brand.toUpperCase()}
337
+ </span>
338
+ {receipt.map((line) => {
339
+ const words = line.split(" ");
340
+ const amount = words.pop() ?? "";
341
+ return (
342
+ <div
343
+ key={line}
344
+ tw="flex justify-between text-lg"
345
+ style={{ gap: 12 }}
346
+ >
347
+ <span>{words.join(" ")}</span>
348
+ <span style={{ fontWeight: 700 }}>{amount}</span>
349
+ </div>
350
+ );
351
+ })}
352
+ <div
353
+ tw="flex"
354
+ style={{ borderTop: `2px dashed ${INK}`, marginTop: 4 }}
355
+ />
356
+ </div>
357
+ <div
358
+ tw="flex self-start items-center px-4 py-2 rounded-full text-lg mt-auto"
359
+ style={{ backgroundColor: INK, color: CREAM, gap: 8 }}
360
+ >
361
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none">
362
+ <path
363
+ d="M4 12.5 9.5 18 20 6"
364
+ stroke={SAGE}
365
+ strokeWidth="3.5"
366
+ strokeLinecap="round"
367
+ strokeLinejoin="round"
368
+ />
369
+ </svg>
370
+ <span>{receiptNote}</span>
371
+ </div>
372
+ </Tile>
373
+ <Tile color={INK} grow={1}>
374
+ <span tw="text-xl" style={{ color: CREAM }}>
375
+ {gaugeLabel}
376
+ </span>
377
+ <div
378
+ tw="flex flex-1 items-end justify-center"
379
+ style={{ minHeight: 0 }}
380
+ >
381
+ <div tw="flex relative" style={{ height: 110, width: 220 }}>
382
+ <svg
383
+ width="220"
384
+ height="110"
385
+ viewBox="0 0 100 50"
386
+ style={{ left: 0, position: "absolute", top: 0 }}
387
+ >
388
+ <path
389
+ d="M10 50 A40 40 0 0 1 90 50"
390
+ fill="none"
391
+ stroke="#3a342c"
392
+ strokeWidth="14"
393
+ strokeLinecap="round"
394
+ />
395
+ <path
396
+ d="M10 50 A40 40 0 0 1 90 50"
397
+ fill="none"
398
+ stroke={accentColor}
399
+ strokeWidth="14"
400
+ strokeLinecap="round"
401
+ strokeDasharray={`${(GAUGE_LENGTH * gaugePercent) / 100} ${GAUGE_LENGTH}`}
402
+ transform={isRtl ? "translate(100 0) scale(-1 1)" : undefined}
403
+ />
404
+ </svg>
405
+ <div tw="flex absolute inset-0 items-end justify-center">
406
+ <span
407
+ style={{
408
+ color: CREAM,
409
+ fontSize: 44,
410
+ fontWeight: 700,
411
+ lineHeight: 1,
412
+ }}
413
+ >
414
+ {`${gaugePercent}%`}
415
+ </span>
416
+ </div>
417
+ </div>
418
+ </div>
419
+ </Tile>
420
+ </Column>
76
421
  </div>
77
422
  );
78
423
  };