@slyxup/ui 0.2.6 → 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.
- package/.turbo/turbo-build.log +1 -1
- package/CHANGELOG.md +7 -0
- package/README.md +4 -1
- package/dist/components/SignIn/SignIn.d.ts.map +1 -1
- package/dist/components/SignIn/SignIn.js +2 -1
- package/dist/components/SignIn/SignIn.js.map +1 -1
- package/dist/components/SignUp/SignUp.d.ts.map +1 -1
- package/dist/components/SignUp/SignUp.js +2 -1
- package/dist/components/SignUp/SignUp.js.map +1 -1
- package/dist/components/SocialButtons/SocialButtons.d.ts.map +1 -1
- package/dist/components/SocialButtons/SocialButtons.js +2 -1
- package/dist/components/SocialButtons/SocialButtons.js.map +1 -1
- package/dist/components/UserButton/UserButton.d.ts +5 -1
- package/dist/components/UserButton/UserButton.d.ts.map +1 -1
- package/dist/components/UserButton/UserButton.js +6 -2
- package/dist/components/UserButton/UserButton.js.map +1 -1
- package/dist/components/UserProfile/UserProfile.d.ts.map +1 -1
- package/dist/components/UserProfile/UserProfile.js +80 -18
- package/dist/components/UserProfile/UserProfile.js.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/styles.d.ts +1 -1
- package/dist/styles.d.ts.map +1 -1
- package/dist/styles.js +163 -0
- package/dist/styles.js.map +1 -1
- package/package.json +3 -3
- package/src/components/SignIn/SignIn.tsx +2 -1
- package/src/components/SignUp/SignUp.tsx +2 -1
- package/src/components/SocialButtons/SocialButtons.tsx +2 -1
- package/src/components/UserButton/UserButton.tsx +12 -2
- package/src/components/UserProfile/UserProfile.tsx +204 -13
- package/src/index.ts +12 -1
- 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
|
-
|
|
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
|
-
{
|
|
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
|
-
<
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
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
|
|
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"
|
|
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
|
|
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%',
|
|
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'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 {
|
|
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;
|