@reviewlico/cli 0.1.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.
@@ -0,0 +1,492 @@
1
+ /* reviewlico components — plain CSS variant
2
+ Import this file once in your app, then use the components freely.
3
+ Override any --rc-* variable to theme the components. */
4
+
5
+ .rc-root {
6
+ /* Color tokens */
7
+ --rc-bg: #0f1318;
8
+ --rc-surface: #0f1318;
9
+ --rc-border: #1e2530;
10
+ --rc-text: #e8edf5;
11
+ --rc-text-muted: #8a98ab;
12
+ --rc-accent: #3b82f6;
13
+ --rc-accent-hover: #2563eb;
14
+ --rc-success: #22c55e;
15
+ --rc-error: #ef4444;
16
+ --rc-error-bg: rgba(239, 68, 68, 0.10);
17
+ --rc-error-border: rgba(239, 68, 68, 0.30);
18
+ --rc-star-filled: #facc15;
19
+ --rc-star-empty: #4b5563;
20
+
21
+ /* Typography */
22
+ --rc-font-family: 'DM Sans', system-ui, -apple-system, sans-serif;
23
+
24
+ /* Shape */
25
+ --rc-radius: 16px;
26
+ --rc-radius-card: 12px;
27
+ --rc-radius-sm: 8px;
28
+
29
+ box-sizing: border-box;
30
+ font-family: var(--rc-font-family);
31
+ color: var(--rc-text);
32
+ }
33
+
34
+ .rc-root *,
35
+ .rc-root *::before,
36
+ .rc-root *::after {
37
+ box-sizing: inherit;
38
+ }
39
+
40
+ /* ─── Form ────────────────────────────────────────────────────────────────── */
41
+
42
+ .rc-form {
43
+ background:
44
+ linear-gradient(180deg, rgba(255, 255, 255, 0.02), rgba(255, 255, 255, 0)),
45
+ var(--rc-bg);
46
+ border: 1px solid var(--rc-border);
47
+ border-radius: var(--rc-radius);
48
+ padding: 1.25rem;
49
+ display: flex;
50
+ flex-direction: column;
51
+ gap: 1.25rem;
52
+ width: 100%;
53
+ margin: 0 auto;
54
+ max-width: 72rem;
55
+ }
56
+
57
+ .rc-form__title {
58
+ font-size: 1.125rem;
59
+ font-weight: 600;
60
+ letter-spacing: -0.025em;
61
+ color: var(--rc-text);
62
+ margin: 0;
63
+ }
64
+
65
+ .rc-form__success {
66
+ text-align: center;
67
+ padding: 2rem 1rem;
68
+ display: flex;
69
+ flex-direction: column;
70
+ align-items: center;
71
+ gap: 0.75rem;
72
+ animation: rc-success-in 0.4s ease-out both;
73
+ }
74
+
75
+ .rc-form__success-icon {
76
+ font-size: 2rem;
77
+ line-height: 1;
78
+ color: var(--rc-success);
79
+ animation: rc-check-bounce 0.5s cubic-bezier(0.34, 1.56, 0.64, 1) 0.15s both;
80
+ display: inline-block;
81
+ }
82
+
83
+ .rc-form__success-heading {
84
+ font-size: 1.125rem;
85
+ font-weight: 600;
86
+ color: var(--rc-success);
87
+ margin: 0;
88
+ }
89
+
90
+ .rc-form__success-message {
91
+ font-size: 0.875rem;
92
+ color: var(--rc-text-muted);
93
+ margin: 0;
94
+ }
95
+
96
+ /* ─── Field ───────────────────────────────────────────────────────────────── */
97
+
98
+ .rc-field {
99
+ display: flex;
100
+ flex-direction: column;
101
+ gap: 0.375rem;
102
+ }
103
+
104
+ .rc-label {
105
+ font-size: 0.875rem;
106
+ font-weight: 500;
107
+ color: var(--rc-text);
108
+ }
109
+
110
+ .rc-input,
111
+ .rc-textarea {
112
+ background: var(--rc-surface);
113
+ border: 1px solid var(--rc-border);
114
+ border-radius: var(--rc-radius-sm);
115
+ color: var(--rc-text);
116
+ font-size: 0.875rem;
117
+ padding: 0 0.75rem;
118
+ width: 100%;
119
+ transition: border-color 0.15s;
120
+ }
121
+
122
+ .rc-input {
123
+ height: 2.5rem;
124
+ }
125
+
126
+ .rc-input::placeholder,
127
+ .rc-textarea::placeholder {
128
+ color: var(--rc-text-muted);
129
+ }
130
+
131
+ .rc-input:focus,
132
+ .rc-textarea:focus {
133
+ outline: 2px solid var(--rc-accent);
134
+ outline-offset: 2px;
135
+ border-color: var(--rc-border);
136
+ animation: rc-focus-pulse 0.3s ease-out;
137
+ }
138
+
139
+ .rc-textarea {
140
+ min-height: 7rem;
141
+ resize: vertical;
142
+ padding: 0.5rem 0.75rem;
143
+ }
144
+
145
+ .rc-field__error {
146
+ font-size: 0.75rem;
147
+ color: var(--rc-error);
148
+ animation: rc-error-in 0.25s ease-out both;
149
+ }
150
+
151
+ /* ─── Stars ───────────────────────────────────────────────────────────────── */
152
+
153
+ .rc-stars {
154
+ display: inline-flex;
155
+ align-items: center;
156
+ gap: 0.125rem;
157
+ }
158
+
159
+ .rc-star {
160
+ background: none;
161
+ border: none;
162
+ padding: 0.125rem;
163
+ font-size: 1.25rem;
164
+ line-height: 1;
165
+ color: var(--rc-star-empty);
166
+ transition: color 0.1s, transform 0.1s;
167
+ }
168
+
169
+ .rc-star--filled {
170
+ color: var(--rc-star-filled);
171
+ }
172
+
173
+ .rc-star--interactive {
174
+ cursor: pointer;
175
+ }
176
+
177
+ .rc-star--interactive:hover {
178
+ transform: scale(1.15);
179
+ filter: drop-shadow(0 0 5px rgba(250, 204, 21, 0.55));
180
+ }
181
+
182
+ .rc-star--just-selected {
183
+ animation: rc-star-pop 0.4s cubic-bezier(0.34, 1.56, 0.64, 1);
184
+ }
185
+
186
+ .rc-stars--sm .rc-star {
187
+ font-size: 1rem;
188
+ }
189
+
190
+ /* ─── Button ──────────────────────────────────────────────────────────────── */
191
+
192
+ .rc-button {
193
+ background: var(--rc-accent);
194
+ border: 1px solid var(--rc-accent);
195
+ border-radius: var(--rc-radius-sm);
196
+ color: #fff;
197
+ cursor: pointer;
198
+ font-size: 0.875rem;
199
+ font-weight: 500;
200
+ height: 2.25rem;
201
+ padding: 0 0.75rem;
202
+ display: inline-flex;
203
+ align-items: center;
204
+ transition: background 0.15s, border-color 0.15s, opacity 0.15s;
205
+ align-self: flex-start;
206
+ }
207
+
208
+ .rc-button:hover:not(:disabled) {
209
+ background: var(--rc-accent-hover);
210
+ border-color: var(--rc-accent-hover);
211
+ }
212
+
213
+ .rc-button:focus-visible {
214
+ outline: 2px solid var(--rc-accent);
215
+ outline-offset: 2px;
216
+ }
217
+
218
+ .rc-button:disabled {
219
+ opacity: 0.50;
220
+ cursor: not-allowed;
221
+ pointer-events: none;
222
+ }
223
+
224
+ .rc-button--loading::before {
225
+ content: '';
226
+ display: inline-block;
227
+ width: 0.875rem;
228
+ height: 0.875rem;
229
+ border: 2px solid rgba(255, 255, 255, 0.35);
230
+ border-top-color: #fff;
231
+ border-radius: 50%;
232
+ animation: rc-spin 0.8s linear infinite;
233
+ margin-right: 0.5rem;
234
+ vertical-align: middle;
235
+ }
236
+
237
+ .rc-error-message {
238
+ font-size: 0.875rem;
239
+ color: #fca5a5;
240
+ background: var(--rc-error-bg);
241
+ border: 1px solid var(--rc-error-border);
242
+ border-radius: var(--rc-radius-sm);
243
+ padding: 0.5rem 0.75rem;
244
+ margin: 0;
245
+ animation: rc-shake 0.5s ease-in-out;
246
+ }
247
+
248
+ /* ─── List ────────────────────────────────────────────────────────────────── */
249
+
250
+ .rc-list {
251
+ display: flex;
252
+ flex-direction: column;
253
+ gap: 1.25rem;
254
+ padding: 1rem;
255
+ margin: 0 auto;
256
+ max-width: 72rem;
257
+ }
258
+
259
+ @media (min-width: 640px) {
260
+ .rc-list {
261
+ padding: 1.5rem;
262
+ }
263
+ }
264
+
265
+ .rc-list__header {
266
+ display: flex;
267
+ align-items: center;
268
+ justify-content: space-between;
269
+ gap: 1rem;
270
+ }
271
+
272
+ .rc-list__title {
273
+ font-size: 1.25rem;
274
+ font-weight: 600;
275
+ letter-spacing: -0.025em;
276
+ color: var(--rc-text);
277
+ margin: 0;
278
+ }
279
+
280
+ .rc-list__toggle-form {
281
+ background: transparent;
282
+ border: 1px solid var(--rc-border);
283
+ border-radius: var(--rc-radius-sm);
284
+ color: var(--rc-text-muted);
285
+ cursor: pointer;
286
+ font-size: 0.875rem;
287
+ height: 2.25rem;
288
+ padding: 0 0.75rem;
289
+ display: inline-flex;
290
+ align-items: center;
291
+ transition: background 0.15s, color 0.15s;
292
+ }
293
+
294
+ .rc-list__toggle-form:hover {
295
+ background: rgba(255, 255, 255, 0.05);
296
+ color: var(--rc-text);
297
+ }
298
+
299
+ .rc-list__empty {
300
+ font-size: 0.875rem;
301
+ color: var(--rc-text-muted);
302
+ padding: 2rem 0;
303
+ text-align: center;
304
+ margin: 0;
305
+ }
306
+
307
+ .rc-list__error {
308
+ font-size: 0.875rem;
309
+ color: #fca5a5;
310
+ background: var(--rc-error-bg);
311
+ border: 1px solid var(--rc-error-border);
312
+ border-radius: var(--rc-radius-sm);
313
+ padding: 0.5rem 0.75rem;
314
+ text-align: center;
315
+ margin: 0;
316
+ }
317
+
318
+ .rc-cards {
319
+ display: flex;
320
+ flex-direction: column;
321
+ gap: 0.75rem;
322
+ }
323
+
324
+ /* ─── Card ────────────────────────────────────────────────────────────────── */
325
+
326
+ .rc-card {
327
+ background: rgba(255, 255, 255, 0.02);
328
+ border: 1px solid rgba(255, 255, 255, 0.10);
329
+ border-radius: var(--rc-radius-card);
330
+ padding: 1rem;
331
+ display: flex;
332
+ flex-direction: column;
333
+ gap: 0.75rem;
334
+ margin: 0 auto;
335
+ max-width: 72rem;
336
+ width: 100%;
337
+ }
338
+
339
+ .rc-card__header {
340
+ display: flex;
341
+ align-items: center;
342
+ justify-content: space-between;
343
+ gap: 0.75rem;
344
+ }
345
+
346
+ .rc-card__reviewer {
347
+ font-size: 0.875rem;
348
+ font-weight: 500;
349
+ color: var(--rc-text);
350
+ }
351
+
352
+ .rc-card__date {
353
+ font-size: 0.75rem;
354
+ color: var(--rc-text-muted);
355
+ }
356
+
357
+ .rc-card__text {
358
+ font-size: 0.875rem;
359
+ color: var(--rc-text);
360
+ line-height: 1.6;
361
+ margin: 0;
362
+ }
363
+
364
+ /* ─── Pagination ──────────────────────────────────────────────────────────── */
365
+
366
+ .rc-pagination {
367
+ display: flex;
368
+ align-items: center;
369
+ justify-content: space-between;
370
+ gap: 1rem;
371
+ padding-top: 0.5rem;
372
+ }
373
+
374
+ .rc-pagination__btn {
375
+ background: transparent;
376
+ border: 1px solid var(--rc-border);
377
+ border-radius: var(--rc-radius-sm);
378
+ color: var(--rc-text-muted);
379
+ cursor: pointer;
380
+ font-size: 0.875rem;
381
+ height: 2.25rem;
382
+ padding: 0 0.75rem;
383
+ display: inline-flex;
384
+ align-items: center;
385
+ transition: background 0.15s, color 0.15s;
386
+ }
387
+
388
+ .rc-pagination__btn:hover:not(:disabled) {
389
+ background: rgba(255, 255, 255, 0.05);
390
+ color: var(--rc-text);
391
+ }
392
+
393
+ .rc-pagination__btn:disabled {
394
+ opacity: 0.4;
395
+ cursor: not-allowed;
396
+ pointer-events: none;
397
+ }
398
+
399
+ .rc-pagination__info {
400
+ font-size: 0.8125rem;
401
+ color: var(--rc-text-muted);
402
+ }
403
+
404
+ /* ─── Skeleton ────────────────────────────────────────────────────────────── */
405
+
406
+ .rc-skeleton {
407
+ background: rgba(255, 255, 255, 0.05);
408
+ border-radius: var(--rc-radius-sm);
409
+ animation: rc-pulse 1.5s ease-in-out infinite alternate;
410
+ }
411
+
412
+ @keyframes rc-pulse {
413
+ from {
414
+ opacity: 1;
415
+ }
416
+ to {
417
+ opacity: 0.35;
418
+ }
419
+ }
420
+
421
+ /* ─── Animation keyframes ─────────────────────────────────────────────────── */
422
+
423
+ @keyframes rc-star-pop {
424
+ 0% { transform: scale(1); }
425
+ 40% { transform: scale(1.4); }
426
+ 100% { transform: scale(1); }
427
+ }
428
+
429
+ @keyframes rc-spin {
430
+ to { transform: rotate(360deg); }
431
+ }
432
+
433
+ @keyframes rc-error-in {
434
+ from { opacity: 0; transform: translateY(-6px); }
435
+ to { opacity: 1; transform: translateY(0); }
436
+ }
437
+
438
+ @keyframes rc-shake {
439
+ 0%, 100% { transform: translateX(0); }
440
+ 15% { transform: translateX(-6px); }
441
+ 30% { transform: translateX(5px); }
442
+ 45% { transform: translateX(-4px); }
443
+ 60% { transform: translateX(3px); }
444
+ 75% { transform: translateX(-2px); }
445
+ }
446
+
447
+ @keyframes rc-success-in {
448
+ from { opacity: 0; transform: scale(0.92); }
449
+ to { opacity: 1; transform: scale(1); }
450
+ }
451
+
452
+ @keyframes rc-check-bounce {
453
+ 0% { transform: scale(0); opacity: 0; }
454
+ 60% { transform: scale(1.3); opacity: 1; }
455
+ 100% { transform: scale(1); opacity: 1; }
456
+ }
457
+
458
+ @keyframes rc-focus-pulse {
459
+ 0% { outline-width: 4px; outline-offset: 4px; }
460
+ 100% { outline-width: 2px; outline-offset: 2px; }
461
+ }
462
+
463
+ /* ─── Reduced motion ──────────────────────────────────────────────────────── */
464
+
465
+ @media (prefers-reduced-motion: reduce) {
466
+ .rc-star--just-selected,
467
+ .rc-button--loading::before,
468
+ .rc-field__error,
469
+ .rc-error-message,
470
+ .rc-form__success,
471
+ .rc-form__success-icon,
472
+ .rc-input:focus,
473
+ .rc-textarea:focus,
474
+ .rc-skeleton {
475
+ animation: none;
476
+ }
477
+
478
+ .rc-star,
479
+ .rc-button,
480
+ .rc-input,
481
+ .rc-textarea,
482
+ .rc-list__toggle-form,
483
+ .rc-pagination__btn {
484
+ transition: none;
485
+ }
486
+ }
487
+
488
+ .rc-skeleton--card {
489
+ height: 6rem;
490
+ border-radius: var(--rc-radius-card);
491
+ border: 1px solid rgba(255, 255, 255, 0.10);
492
+ }
@@ -0,0 +1,36 @@
1
+ import type { PublicReview } from '../../shared/types';
2
+ import { StarRating } from './StarRating';
3
+
4
+ function formatRelative(dateStr: string | null | undefined): string {
5
+ if (!dateStr) return '';
6
+ const timestamp = new Date(dateStr).getTime();
7
+ if (!Number.isFinite(timestamp)) return '';
8
+ const diff = Date.now() - timestamp;
9
+ const rtf = new Intl.RelativeTimeFormat('en', { numeric: 'auto' });
10
+ const minutes = Math.round(diff / 60_000);
11
+ if (Math.abs(minutes) < 60) return rtf.format(-minutes, 'minute');
12
+ const hours = Math.round(diff / 3_600_000);
13
+ if (Math.abs(hours) < 24) return rtf.format(-hours, 'hour');
14
+ const days = Math.round(diff / 86_400_000);
15
+ if (Math.abs(days) < 30) return rtf.format(-days, 'day');
16
+ const months = Math.round(diff / 2_592_000_000);
17
+ if (Math.abs(months) < 12) return rtf.format(-months, 'month');
18
+ return rtf.format(-Math.round(diff / 31_536_000_000), 'year');
19
+ }
20
+
21
+ interface ReviewCardProps {
22
+ review: PublicReview;
23
+ }
24
+
25
+ export function ReviewCard({ review }: ReviewCardProps) {
26
+ return (
27
+ <div className="rounded-xl border border-white/10 bg-white/[0.02] p-4 flex flex-col gap-3 w-full max-w-[72rem] mx-auto">
28
+ <div className="flex items-center justify-between gap-3">
29
+ <StarRating value={review.rating} size="sm" />
30
+ <span className="text-xs text-[#8a98ab]">{formatRelative(review.createdAt)}</span>
31
+ </div>
32
+ <span className="text-sm font-medium text-[#e8edf5]">{review.reviewerName}</span>
33
+ <p className="text-sm text-[#e8edf5] leading-relaxed">{review.text}</p>
34
+ </div>
35
+ );
36
+ }