@slyxup/ui 0.2.7 → 0.2.8

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.
Files changed (34) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/README.md +4 -1
  3. package/dist/components/SignIn/SignIn.d.ts.map +1 -1
  4. package/dist/components/SignIn/SignIn.js +2 -1
  5. package/dist/components/SignIn/SignIn.js.map +1 -1
  6. package/dist/components/SignUp/SignUp.d.ts.map +1 -1
  7. package/dist/components/SignUp/SignUp.js +2 -1
  8. package/dist/components/SignUp/SignUp.js.map +1 -1
  9. package/dist/components/SocialButtons/SocialButtons.d.ts.map +1 -1
  10. package/dist/components/SocialButtons/SocialButtons.js +2 -1
  11. package/dist/components/SocialButtons/SocialButtons.js.map +1 -1
  12. package/dist/components/UserButton/UserButton.d.ts +5 -1
  13. package/dist/components/UserButton/UserButton.d.ts.map +1 -1
  14. package/dist/components/UserButton/UserButton.js +6 -2
  15. package/dist/components/UserButton/UserButton.js.map +1 -1
  16. package/dist/components/UserProfile/UserProfile.d.ts.map +1 -1
  17. package/dist/components/UserProfile/UserProfile.js +80 -18
  18. package/dist/components/UserProfile/UserProfile.js.map +1 -1
  19. package/dist/index.d.ts +3 -1
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.js +3 -1
  22. package/dist/index.js.map +1 -1
  23. package/dist/styles.d.ts +1 -1
  24. package/dist/styles.d.ts.map +1 -1
  25. package/dist/styles.js +163 -0
  26. package/dist/styles.js.map +1 -1
  27. package/package.json +3 -3
  28. package/src/components/SignIn/SignIn.tsx +2 -1
  29. package/src/components/SignUp/SignUp.tsx +2 -1
  30. package/src/components/SocialButtons/SocialButtons.tsx +2 -1
  31. package/src/components/UserButton/UserButton.tsx +12 -2
  32. package/src/components/UserProfile/UserProfile.tsx +204 -13
  33. package/src/index.ts +12 -1
  34. package/src/styles.ts +163 -0
@@ -11,7 +11,7 @@ export interface UserProfileProps {
11
11
  onDeleted?: () => void;
12
12
  }
13
13
 
14
- type Tab = 'profile' | 'security';
14
+ type Tab = 'profile' | 'security' | 'billing';
15
15
 
16
16
  function initials(user: { firstName: string | null; email: string }): string {
17
17
  const n = user.firstName?.trim();
@@ -19,7 +19,6 @@ function initials(user: { firstName: string | null; email: string }): string {
19
19
  return user.email.slice(0, 1).toUpperCase();
20
20
  }
21
21
 
22
- /** Best-effort device label from a user agent string. */
23
22
  function deviceLabel(ua: string | null): string {
24
23
  if (!ua) return 'Unknown device';
25
24
  const os = /Windows/i.test(ua)
@@ -56,6 +55,13 @@ function formatDate(iso: string): string {
56
55
  });
57
56
  }
58
57
 
58
+ function formatCurrency(amount: number, currency: string): string {
59
+ return new Intl.NumberFormat(undefined, {
60
+ style: 'currency',
61
+ currency: currency.toUpperCase(),
62
+ }).format(amount / 100);
63
+ }
64
+
59
65
  export function UserProfile({
60
66
  modal = true,
61
67
  onClose,
@@ -90,6 +96,25 @@ export function UserProfile({
90
96
  const [revokingId, setRevokingId] = useState<string | null>(null);
91
97
  const [othersRevoking, setOthersRevoking] = useState(false);
92
98
 
99
+ // ── Billing state ──
100
+ const [billingLoading, setBillingLoading] = useState(true);
101
+ const [subscription, setSubscription] = useState<{
102
+ id: string;
103
+ status: string;
104
+ planName: string | null;
105
+ currentPeriodEnd: string | null;
106
+ cancelAtPeriodEnd: boolean;
107
+ } | null>(null);
108
+ const [invoices, setInvoices] = useState<
109
+ {
110
+ id: string;
111
+ amount: number;
112
+ currency: string;
113
+ status: string;
114
+ billedAt: string | null;
115
+ }[]
116
+ >([]);
117
+
93
118
  // ── Danger zone state ──
94
119
  const [confirmText, setConfirmText] = useState('');
95
120
  const [deleteBusy, setDeleteBusy] = useState(false);
@@ -123,9 +148,47 @@ export function UserProfile({
123
148
  }
124
149
  }, [client]);
125
150
 
151
+ const loadBilling = useCallback(async () => {
152
+ setBillingLoading(true);
153
+ try {
154
+ const billingUrl =
155
+ (client as unknown as { apiUrl: string }).apiUrl?.replace(
156
+ 'auth.slyxup.online',
157
+ 'billing.slyxup.online'
158
+ ) ?? 'https://billing.slyxup.online';
159
+ const token = (client as unknown as { _token?: string })?._token;
160
+ const headers: Record<string, string> = {
161
+ 'Content-Type': 'application/json',
162
+ };
163
+ if (token) headers.Authorization = `Bearer ${token}`;
164
+
165
+ const [subRes, invRes] = await Promise.allSettled([
166
+ fetch(`${billingUrl}/v1/subscription`, {
167
+ headers,
168
+ credentials: 'include',
169
+ }),
170
+ fetch(`${billingUrl}/v1/invoices`, { headers, credentials: 'include' }),
171
+ ]);
172
+
173
+ if (subRes.status === 'fulfilled' && subRes.value.ok) {
174
+ const sub = await subRes.value.json();
175
+ if (sub.ok) setSubscription(sub.subscription ?? null);
176
+ }
177
+ if (invRes.status === 'fulfilled' && invRes.value.ok) {
178
+ const inv = await invRes.value.json();
179
+ if (inv.ok) setInvoices(inv.invoices ?? []);
180
+ }
181
+ } catch {
182
+ // Billing unavailable — show empty state
183
+ } finally {
184
+ setBillingLoading(false);
185
+ }
186
+ }, [client]);
187
+
126
188
  useEffect(() => {
127
189
  if (tab === 'security') void loadSessions();
128
- }, [tab, loadSessions]);
190
+ if (tab === 'billing') void loadBilling();
191
+ }, [tab, loadSessions, loadBilling]);
129
192
 
130
193
  async function onProfileSubmit(e: FormEvent) {
131
194
  e.preventDefault();
@@ -262,10 +325,19 @@ export function UserProfile({
262
325
  >
263
326
  <ShieldIcon /> Security
264
327
  </button>
328
+ <button
329
+ type="button"
330
+ className={`slx-profile-nav-btn${tab === 'billing' ? ' on' : ''}`}
331
+ onClick={() => setTab('billing')}
332
+ aria-current={tab === 'billing' ? 'page' : undefined}
333
+ >
334
+ <CreditCardIcon /> Billing
335
+ </button>
265
336
  </nav>
266
337
 
267
338
  <div className="slx-profile-content">
268
- {tab === 'profile' ? (
339
+ {/* ── Profile Tab ── */}
340
+ {tab === 'profile' && (
269
341
  <>
270
342
  <section className="slx-profile-sec">
271
343
  <div className="slx-avatar-row">
@@ -276,14 +348,21 @@ export function UserProfile({
276
348
  initials(user)
277
349
  )}
278
350
  </div>
279
- <p className="slx-hint" style={{ margin: 0 }}>
280
- Your avatar is loaded from its URL. Paste any public image
281
- link below.
282
- </p>
351
+ <div>
352
+ <p className="slx-row-value" style={{ margin: 0 }}>
353
+ {user.firstName
354
+ ? `${user.firstName}${user.lastName ? ` ${user.lastName}` : ''}`
355
+ : user.email}
356
+ </p>
357
+ <p className="slx-row-label">{user.email}</p>
358
+ </div>
283
359
  </div>
284
360
 
285
361
  {saved && (
286
- <p className="slx-error-text" style={{ color: '#34a853' }}>
362
+ <p
363
+ className="slx-error-text"
364
+ style={{ color: 'var(--slx-success)' }}
365
+ >
287
366
  Profile saved.
288
367
  </p>
289
368
  )}
@@ -325,6 +404,9 @@ export function UserProfile({
325
404
  value={avatarUrl}
326
405
  onChange={(e) => setAvatarUrl(e.target.value)}
327
406
  />
407
+ <p className="slx-hint">
408
+ Paste a public image URL for your avatar.
409
+ </p>
328
410
  </div>
329
411
  <button className="slx-btn" type="submit" disabled={busy}>
330
412
  {busy ? 'Saving…' : 'Save changes'}
@@ -340,7 +422,7 @@ export function UserProfile({
340
422
  <p className="slx-row-label">Primary email</p>
341
423
  </div>
342
424
  {emailVerified ? (
343
- <span className="slx-badge slx-badge-ok">✓ Verified</span>
425
+ <span className="slx-badge slx-badge-ok">Verified</span>
344
426
  ) : (
345
427
  <span
346
428
  style={{
@@ -365,12 +447,18 @@ export function UserProfile({
365
447
  </div>
366
448
  </section>
367
449
  </>
368
- ) : (
450
+ )}
451
+
452
+ {/* ── Security Tab ── */}
453
+ {tab === 'security' && (
369
454
  <>
370
455
  <section className="slx-profile-sec">
371
456
  <h3 className="slx-sec-title">Change password</h3>
372
457
  {pwSaved && (
373
- <p className="slx-error-text" style={{ color: '#34a853' }}>
458
+ <p
459
+ className="slx-error-text"
460
+ style={{ color: 'var(--slx-success)' }}
461
+ >
374
462
  Password updated.
375
463
  </p>
376
464
  )}
@@ -430,6 +518,8 @@ export function UserProfile({
430
518
  <h3 className="slx-sec-title">Active sessions</h3>
431
519
  {sessionsLoading ? (
432
520
  <p className="slx-hint">Loading sessions…</p>
521
+ ) : sessions.length === 0 ? (
522
+ <p className="slx-hint">No active sessions.</p>
433
523
  ) : (
434
524
  <>
435
525
  {sessions.map((s) => (
@@ -469,7 +559,7 @@ export function UserProfile({
469
559
  <button
470
560
  type="button"
471
561
  className="slx-btn-danger-outline"
472
- style={{ width: '100%', padding: '9px 11px' }}
562
+ style={{ width: '100%', marginTop: 4 }}
473
563
  onClick={onRevokeOthers}
474
564
  disabled={othersRevoking}
475
565
  >
@@ -518,6 +608,88 @@ export function UserProfile({
518
608
  </section>
519
609
  </>
520
610
  )}
611
+
612
+ {/* ── Billing Tab ── */}
613
+ {tab === 'billing' &&
614
+ (billingLoading ? (
615
+ <section className="slx-profile-sec">
616
+ <p className="slx-hint">Loading billing information…</p>
617
+ </section>
618
+ ) : !subscription ? (
619
+ <section className="slx-profile-sec">
620
+ <h3 className="slx-sec-title">Subscription</h3>
621
+ <div className="slx-billing-card">
622
+ <p className="slx-billing-plan">No active subscription</p>
623
+ <p className="slx-billing-detail">
624
+ You don&apos;t have a subscription yet. Choose a plan to get
625
+ started.
626
+ </p>
627
+ </div>
628
+ </section>
629
+ ) : (
630
+ <>
631
+ <section className="slx-profile-sec">
632
+ <h3 className="slx-sec-title">Current plan</h3>
633
+ <div className="slx-billing-card">
634
+ <div
635
+ style={{
636
+ display: 'flex',
637
+ justifyContent: 'space-between',
638
+ alignItems: 'flex-start',
639
+ }}
640
+ >
641
+ <div>
642
+ <p className="slx-billing-plan">
643
+ {subscription.planName ?? 'Subscription'}
644
+ </p>
645
+ <p className="slx-billing-detail">
646
+ Status:{' '}
647
+ <span
648
+ className={`slx-billing-status slx-billing-status-${subscription.status}`}
649
+ >
650
+ {subscription.status}
651
+ </span>
652
+ </p>
653
+ {subscription.currentPeriodEnd && (
654
+ <p className="slx-billing-detail">
655
+ {subscription.cancelAtPeriodEnd
656
+ ? `Cancels ${formatDate(subscription.currentPeriodEnd)}`
657
+ : `Renews ${formatDate(subscription.currentPeriodEnd)}`}
658
+ </p>
659
+ )}
660
+ </div>
661
+ </div>
662
+ </div>
663
+ </section>
664
+
665
+ {invoices.length > 0 && (
666
+ <section className="slx-profile-sec">
667
+ <h3 className="slx-sec-title">Invoices</h3>
668
+ {invoices.map((inv) => (
669
+ <div key={inv.id} className="slx-invoice-row">
670
+ <span className="slx-invoice-date">
671
+ {inv.billedAt ? formatDate(inv.billedAt) : '—'}
672
+ </span>
673
+ <span className="slx-invoice-amount">
674
+ {formatCurrency(inv.amount, inv.currency)}
675
+ </span>
676
+ <span
677
+ className={`slx-badge ${
678
+ inv.status === 'paid'
679
+ ? 'slx-badge-ok'
680
+ : inv.status === 'overdue'
681
+ ? 'slx-badge-warn'
682
+ : 'slx-badge-accent'
683
+ }`}
684
+ >
685
+ {inv.status}
686
+ </span>
687
+ </div>
688
+ ))}
689
+ </section>
690
+ )}
691
+ </>
692
+ ))}
521
693
  </div>
522
694
  </div>
523
695
  </div>
@@ -573,3 +745,22 @@ function ShieldIcon() {
573
745
  </svg>
574
746
  );
575
747
  }
748
+
749
+ function CreditCardIcon() {
750
+ return (
751
+ <svg
752
+ width="15"
753
+ height="15"
754
+ viewBox="0 0 24 24"
755
+ fill="none"
756
+ stroke="currentColor"
757
+ strokeWidth="2"
758
+ strokeLinecap="round"
759
+ strokeLinejoin="round"
760
+ aria-hidden="true"
761
+ >
762
+ <rect x="1" y="4" width="22" height="16" rx="2" ry="2" />
763
+ <line x1="1" y1="10" x2="23" y2="10" />
764
+ </svg>
765
+ );
766
+ }
package/src/index.ts CHANGED
@@ -3,7 +3,10 @@ export { KeyholeMark, GoogleIcon, GitHubIcon, CheckIcon } from './icons';
3
3
 
4
4
  export { SignIn, type SignInProps } from './components/SignIn/SignIn';
5
5
  export { SignUp, type SignUpProps } from './components/SignUp/SignUp';
6
- export { UserButton } from './components/UserButton/UserButton';
6
+ export {
7
+ UserButton,
8
+ type UserButtonProps,
9
+ } from './components/UserButton/UserButton';
7
10
  export {
8
11
  UserProfile,
9
12
  type UserProfileProps,
@@ -24,6 +27,14 @@ export {
24
27
  SocialButtons,
25
28
  type SocialButtonsProps,
26
29
  } from './components/SocialButtons/SocialButtons';
30
+ export {
31
+ BillingPortal,
32
+ type BillingPortalProps,
33
+ } from './components/BillingPortal/BillingPortal';
34
+ export {
35
+ PricingTable,
36
+ type PricingTableProps,
37
+ } from './components/PricingTable/PricingTable';
27
38
 
28
39
  import { useEffect } from 'react';
29
40
  import { injectStyles } from './styles';
package/src/styles.ts CHANGED
@@ -308,6 +308,169 @@ export const CSS = `
308
308
  background: color-mix(in srgb, var(--slx-success) 9%, transparent);
309
309
  border: 1px solid color-mix(in srgb, var(--slx-success) 30%, transparent);
310
310
  }
311
+ .slx-badge-ok { color: var(--slx-success); background: color-mix(in srgb, var(--slx-success) 9%, transparent); border-color: color-mix(in srgb, var(--slx-success) 30%, transparent); }
312
+ .slx-badge-warn { color: #b45309; background: color-mix(in srgb, #f59e0b 10%, transparent); border-color: color-mix(in srgb, #f59e0b 30%, transparent); }
313
+ .slx-badge-accent { color: var(--slx-accent); background: var(--slx-accent-soft); border-color: color-mix(in srgb, var(--slx-accent) 30%, transparent); }
314
+ .slyxup-root:not(.slyxup-light) .slx-badge-warn { color: #fbbf24; }
315
+
316
+ /* ── Modal overlay ── */
317
+ .slx-overlay {
318
+ position: fixed; inset: 0; z-index: 9999;
319
+ display: flex; align-items: center; justify-content: center;
320
+ background: rgba(12, 12, 18, 0.55);
321
+ backdrop-filter: blur(6px);
322
+ animation: slx-rise .2s cubic-bezier(.22,.9,.32,1) both;
323
+ }
324
+
325
+ /* ── UserProfile ── */
326
+ .slx-profile-head {
327
+ display: flex; align-items: center; justify-content: space-between;
328
+ padding: 22px 28px 18px;
329
+ border-bottom: 1px solid var(--slx-border);
330
+ }
331
+ .slx-profile-title {
332
+ font-family: var(--slx-display);
333
+ font-size: 18px; font-weight: 650; letter-spacing: -0.02em;
334
+ color: var(--slx-ink-strong);
335
+ margin: 0;
336
+ }
337
+ .slx-profile-close {
338
+ width: 32px; height: 32px; border-radius: 8px;
339
+ display: flex; align-items: center; justify-content: center;
340
+ background: none; border: 1px solid var(--slx-border);
341
+ color: var(--slx-muted); cursor: pointer; font-size: 16px;
342
+ transition: background .12s, color .12s, border-color .12s;
343
+ }
344
+ .slx-profile-close:hover { background: var(--slx-bg-subtle); color: var(--slx-ink); border-color: var(--slx-border-strong); }
345
+ .slx-profile-close:focus-visible { outline: none; box-shadow: 0 0 0 3px var(--slx-accent-soft); }
346
+
347
+ .slx-profile-body {
348
+ display: flex; min-height: 480px; max-height: 80vh;
349
+ width: 720px; max-width: calc(100vw - 48px);
350
+ background: var(--slx-bg);
351
+ border-radius: var(--slx-radius-lg);
352
+ box-shadow: var(--slx-shadow-pop);
353
+ overflow: hidden;
354
+ }
355
+
356
+ /* ── Left nav ── */
357
+ .slx-profile-nav {
358
+ width: 200px; flex-shrink: 0;
359
+ display: flex; flex-direction: column; gap: 2px;
360
+ padding: 12px 8px;
361
+ border-right: 1px solid var(--slx-border);
362
+ background: var(--slx-bg-subtle);
363
+ }
364
+ .slx-profile-nav-btn {
365
+ display: flex; align-items: center; gap: 9px;
366
+ font: inherit; font-size: 13.5px; font-weight: 500;
367
+ color: var(--slx-muted); background: none; border: none;
368
+ border-radius: var(--slx-radius-sm);
369
+ padding: 9px 12px; cursor: pointer; text-align: left;
370
+ transition: background .12s, color .12s;
371
+ }
372
+ .slx-profile-nav-btn:hover { background: color-mix(in srgb, var(--slx-ink) 5%, transparent); color: var(--slx-ink); }
373
+ .slx-profile-nav-btn.on { background: var(--slx-bg); color: var(--slx-ink-strong); font-weight: 600; box-shadow: 0 1px 3px rgba(18,18,28,.06); }
374
+ .slx-profile-nav-btn:focus-visible { outline: none; box-shadow: inset 0 0 0 2px var(--slx-accent-soft); }
375
+
376
+ /* ── Content area ── */
377
+ .slx-profile-content {
378
+ flex: 1; overflow-y: auto; padding: 24px 28px 28px;
379
+ }
380
+ .slx-profile-sec { margin-bottom: 28px; }
381
+ .slx-profile-sec:last-child { margin-bottom: 0; }
382
+ .slx-sec-title {
383
+ font-family: var(--slx-display);
384
+ font-size: 14px; font-weight: 600; letter-spacing: -0.01em;
385
+ color: var(--slx-ink-strong);
386
+ margin: 0 0 14px; padding-bottom: 10px;
387
+ border-bottom: 1px solid var(--slx-border);
388
+ }
389
+
390
+ /* ── Avatar ── */
391
+ .slx-avatar-row {
392
+ display: flex; align-items: center; gap: 16px;
393
+ margin-bottom: 20px;
394
+ }
395
+ .slx-avatar-lg {
396
+ width: 64px; height: 64px; border-radius: 50%;
397
+ flex-shrink: 0;
398
+ display: flex; align-items: center; justify-content: center;
399
+ font-size: 24px; font-weight: 700; color: #fff;
400
+ background: linear-gradient(135deg, var(--slx-accent), #8b5cf6);
401
+ overflow: hidden;
402
+ box-shadow: 0 2px 8px rgba(91,91,214,.3);
403
+ }
404
+ .slx-avatar-lg img { width: 100%; height: 100%; object-fit: cover; }
405
+
406
+ /* ── Row (email etc.) ── */
407
+ .slx-row-value { font-size: 14px; font-weight: 500; color: var(--slx-ink-strong); margin: 0; }
408
+ .slx-row-label { font-size: 12px; color: var(--slx-muted); margin: 2px 0 0; }
409
+
410
+ /* ── Sessions ── */
411
+ .slx-session {
412
+ display: flex; align-items: center; justify-content: space-between;
413
+ padding: 12px 14px; border-radius: var(--slx-radius-sm);
414
+ border: 1px solid var(--slx-border);
415
+ background: var(--slx-bg-subtle);
416
+ margin-bottom: 8px;
417
+ transition: border-color .12s;
418
+ }
419
+ .slx-session:hover { border-color: var(--slx-border-strong); }
420
+ .slx-session-meta { display: flex; flex-direction: column; gap: 3px; min-width: 0; }
421
+ .slx-session-device { font-size: 13.5px; font-weight: 550; color: var(--slx-ink); display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
422
+ .slx-session-sub { font-size: 12px; color: var(--slx-muted); margin: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
423
+
424
+ /* ── Danger zone ── */
425
+ .slx-danger-zone {
426
+ border: 1px solid color-mix(in srgb, var(--slx-danger) 30%, transparent);
427
+ border-radius: var(--slx-radius);
428
+ padding: 18px 20px;
429
+ background: color-mix(in srgb, var(--slx-danger) 4%, transparent);
430
+ }
431
+ .slx-danger-title {
432
+ font-size: 14px; font-weight: 650; color: var(--slx-danger);
433
+ margin: 0 0 6px;
434
+ }
435
+ .slx-danger-desc {
436
+ font-size: 13px; color: var(--slx-muted); line-height: 1.5;
437
+ margin: 0 0 16px;
438
+ }
439
+ .slx-btn-danger-outline {
440
+ font: inherit; font-size: 13px; font-weight: 550;
441
+ color: var(--slx-danger); background: none;
442
+ border: 1px solid color-mix(in srgb, var(--slx-danger) 35%, transparent);
443
+ border-radius: var(--slx-radius-sm);
444
+ padding: 7px 14px; cursor: pointer;
445
+ transition: background .12s, border-color .12s;
446
+ }
447
+ .slx-btn-danger-outline:hover { background: color-mix(in srgb, var(--slx-danger) 8%, transparent); border-color: var(--slx-danger); }
448
+ .slx-btn-danger-outline:active { transform: scale(.98); }
449
+ .slx-btn-danger-outline:focus-visible { outline: none; box-shadow: 0 0 0 3px color-mix(in srgb, var(--slx-danger) 20%, transparent); }
450
+ .slx-btn-danger-outline[disabled] { opacity: .5; cursor: not-allowed; }
451
+
452
+ /* ── Billing (inside UserProfile) ── */
453
+ .slx-billing-card {
454
+ border: 1px solid var(--slx-border);
455
+ border-radius: var(--slx-radius);
456
+ padding: 18px 20px;
457
+ background: var(--slx-bg-subtle);
458
+ margin-bottom: 16px;
459
+ }
460
+ .slx-billing-plan { font-size: 15px; font-weight: 650; color: var(--slx-ink-strong); margin: 0 0 4px; }
461
+ .slx-billing-detail { font-size: 13px; color: var(--slx-muted); margin: 2px 0; }
462
+ .slx-billing-status { display: inline-flex; align-items: center; gap: 6px; font-size: 12px; font-weight: 600; }
463
+ .slx-billing-status-active { color: var(--slx-success); }
464
+ .slx-billing-status-trialing { color: var(--slx-accent); }
465
+ .slx-billing-status-canceled { color: var(--slx-danger); }
466
+ .slx-invoice-row {
467
+ display: flex; align-items: center; justify-content: space-between;
468
+ padding: 10px 0; border-bottom: 1px solid var(--slx-border);
469
+ font-size: 13px;
470
+ }
471
+ .slx-invoice-row:last-child { border-bottom: none; }
472
+ .slx-invoice-date { color: var(--slx-muted); }
473
+ .slx-invoice-amount { font-weight: 600; color: var(--slx-ink-strong); }
311
474
  `;
312
475
 
313
476
  let injected = false;