@ciromaciel/auth-react 1.5.0 → 1.6.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.js CHANGED
@@ -3237,47 +3237,579 @@ function SignIn({
3237
3237
  });
3238
3238
  }
3239
3239
 
3240
+ /*
3241
+ * How a session reads on the account screen: which device, since when, and
3242
+ * how many there are.
3243
+ *
3244
+ * Pure functions, kept out of the component so they can be tested against real
3245
+ * user agents. The order of the checks is the point of this file:
3246
+ * - iPhone and iPad announce themselves as "like Mac OS X", so they must be
3247
+ * recognised BEFORE macOS — the old parser showed every phone as a Mac;
3248
+ * - Edge, Opera and Samsung Internet all carry "Chrome" in the string, and
3249
+ * Chrome on iOS carries "Safari", so each is checked before the engine it
3250
+ * imitates — the old parser never reached its Opera branch.
3251
+ */
3252
+
3253
+ const BROWSERS = [['Edge', /\bEdg(?:e|A|iOS)?\//], ['Opera', /\b(?:OPR|Opera|OPT)\//], ['Samsung Internet', /\bSamsungBrowser\//], ['Firefox', /\b(?:Firefox|FxiOS)\//], ['Chrome', /\b(?:Chrome|CriOS)\//], ['Safari', /\bSafari\//]];
3254
+
3255
+ /**
3256
+ * @param {string} [userAgent]
3257
+ * @returns {{ browser: string|null, os: string|null, kind: 'desktop'|'phone'|'tablet' }}
3258
+ */
3259
+ function describeDevice(userAgent) {
3260
+ const ua = userAgent || '';
3261
+ const browser = BROWSERS.find(([, pattern]) => pattern.test(ua))?.[0] || null;
3262
+ if (/\biPad\b/.test(ua)) return {
3263
+ browser,
3264
+ os: 'iPadOS',
3265
+ kind: 'tablet'
3266
+ };
3267
+ if (/\biPhone\b|\biPod\b/.test(ua)) return {
3268
+ browser,
3269
+ os: 'iOS',
3270
+ kind: 'phone'
3271
+ };
3272
+ if (/\bAndroid\b/.test(ua)) return {
3273
+ browser,
3274
+ os: 'Android',
3275
+ kind: /\bMobile\b/.test(ua) ? 'phone' : 'tablet'
3276
+ };
3277
+ if (/\bWindows\b/.test(ua)) return {
3278
+ browser,
3279
+ os: 'Windows',
3280
+ kind: 'desktop'
3281
+ };
3282
+ if (/\bCrOS\b/.test(ua)) return {
3283
+ browser,
3284
+ os: 'ChromeOS',
3285
+ kind: 'desktop'
3286
+ };
3287
+ if (/\bMac OS X\b|\bMacintosh\b/.test(ua)) return {
3288
+ browser,
3289
+ os: 'macOS',
3290
+ kind: 'desktop'
3291
+ };
3292
+ if (/\bLinux\b/.test(ua)) return {
3293
+ browser,
3294
+ os: 'Linux',
3295
+ kind: 'desktop'
3296
+ };
3297
+ return {
3298
+ browser,
3299
+ os: null,
3300
+ kind: 'desktop'
3301
+ };
3302
+ }
3303
+
3304
+ /** "Chrome no macOS", "Safari", "iOS", or "Aparelho desconhecido". */
3305
+ function deviceLabel({
3306
+ browser,
3307
+ os
3308
+ }) {
3309
+ if (browser && os) return `${browser} no ${os}`;
3310
+ return browser || os || 'Aparelho desconhecido';
3311
+ }
3312
+ const pad = n => String(n).padStart(2, '0');
3313
+
3314
+ /**
3315
+ * "hoje, 09:12", "ontem, 21:40", "23/09, 14:05" or "23/09/2025, 14:05".
3316
+ *
3317
+ * Calendar days in the viewer's own time zone: a session opened at 23:50 is
3318
+ * "ontem" ten minutes later, which is what the person remembers.
3319
+ */
3320
+ function formatSessionStart(value, now = new Date()) {
3321
+ const date = new Date(value);
3322
+ if (Number.isNaN(date.getTime())) return null;
3323
+ const time = `${pad(date.getHours())}:${pad(date.getMinutes())}`;
3324
+ const startOfDay = d => new Date(d.getFullYear(), d.getMonth(), d.getDate()).getTime();
3325
+ const days = Math.round((startOfDay(now) - startOfDay(date)) / 86_400_000);
3326
+ if (days === 0) return `hoje, ${time}`;
3327
+ if (days === 1) return `ontem, ${time}`;
3328
+ const day = `${pad(date.getDate())}/${pad(date.getMonth() + 1)}`;
3329
+ return date.getFullYear() === now.getFullYear() ? `${day}, ${time}` : `${day}/${date.getFullYear()}, ${time}`;
3330
+ }
3331
+
3332
+ /** "1 sessão aberta", "3 sessões abertas". */
3333
+ function countSessions(count) {
3334
+ return count === 1 ? '1 sessão aberta' : `${count} sessões abertas`;
3335
+ }
3336
+
3337
+ /**
3338
+ * The current session first, then the others from the newest.
3339
+ *
3340
+ * The API orders by creation only, so the device the person is holding could
3341
+ * land anywhere in the list; it is the one they look for first.
3342
+ */
3343
+ function orderSessions(sessions, currentId) {
3344
+ return [...(sessions || [])].sort((a, b) => {
3345
+ if (a.id === currentId) return -1;
3346
+ if (b.id === currentId) return 1;
3347
+ return new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime();
3348
+ });
3349
+ }
3350
+
3351
+ /*
3352
+ * Who the signed-in person is, as the account card and the account screen
3353
+ * show it. One rule for both, so the two never disagree about the same person.
3354
+ *
3355
+ * A name equal to the email's local part is not a name: it is what an account
3356
+ * born from a code gets by default, and showing it on top of the email repeats
3357
+ * the same fact. It counts as "no name", and the email becomes the title.
3358
+ */
3359
+ function describeUser(user) {
3360
+ const name = (user?.fullName || user?.name || '').trim();
3361
+ const email = (user?.primaryEmailAddress || user?.email || '').trim();
3362
+ const hasRealName = Boolean(name) && name.toLowerCase() !== email.split('@')[0].toLowerCase() && name.toLowerCase() !== email.toLowerCase();
3363
+ const title = hasRealName ? name : email || name;
3364
+ const initials = hasRealName ? name.split(/\s+/).map(part => part[0]).join('').slice(0, 2).toUpperCase() : (email || name).charAt(0).toUpperCase();
3365
+ return {
3366
+ name,
3367
+ email,
3368
+ hasRealName,
3369
+ title,
3370
+ initials,
3371
+ image: user?.imageUrl || user?.image || null
3372
+ };
3373
+ }
3374
+
3375
+ const LABELS = {
3376
+ title: 'Conta',
3377
+ subtitle: 'Quem você é, como você entra e onde a sua sessão está aberta.',
3378
+ close: 'Fechar',
3379
+ profileSection: 'Perfil',
3380
+ avatar: 'Foto',
3381
+ name: 'Nome',
3382
+ email: 'E-mail',
3383
+ edit: 'Alterar',
3384
+ save: 'Salvar',
3385
+ cancel: 'Cancelar',
3386
+ remove: 'Remover',
3387
+ notDefined: 'Não definido',
3388
+ namePlaceholder: 'Seu nome',
3389
+ nameHint: 'Aparece no cartão da conta e para quem divide uma organização com você. Enter salva, Esc cancela.',
3390
+ nameRequired: 'Digite um nome.',
3391
+ emailHint: 'É para onde vai o código de acesso, por isso não muda por aqui.',
3392
+ avatarPrompt: 'Arraste uma imagem ou clique para escolher',
3393
+ avatarHint: 'JPG, PNG, GIF ou WebP, até {size}. Ela aparece em todos os painéis.',
3394
+ avatarInvalidType: 'Escolha uma imagem: JPG, PNG, GIF ou WebP.',
3395
+ avatarTooLarge: 'Imagem grande demais. O máximo é {size}.',
3396
+ signInSection: 'Formas de entrar',
3397
+ codeMethod: 'Código',
3398
+ codeByEmail: 'Por e-mail',
3399
+ alwaysOn: 'Sempre ativo',
3400
+ notConnected: 'Não conectado',
3401
+ connect: 'Conectar',
3402
+ disconnect: 'Desconectar',
3403
+ sessionsSection: 'Sessões',
3404
+ devices: 'Aparelhos',
3405
+ showSessions: 'Ver sessões',
3406
+ hideSessions: 'Ocultar',
3407
+ thisDevice: 'Este aparelho',
3408
+ end: 'Encerrar',
3409
+ since: 'desde',
3410
+ unknownIP: 'IP desconhecido',
3411
+ loadingSessions: 'Carregando as sessões…',
3412
+ noSessionsFound: 'Nenhuma sessão encontrada.',
3413
+ confirmEndBody: 'Quem estiver nesses aparelhos volta para a tela de entrar. Este aparelho continua conectado.',
3414
+ genericFailure: 'Não foi possível concluir. Tente de novo.'
3415
+ };
3416
+ const PROVIDER_MARKS = {
3417
+ google: iconsReact.IconBrandGoogle,
3418
+ github: iconsReact.IconBrandGithub
3419
+ };
3420
+ const DEVICE_MARKS = {
3421
+ desktop: iconsReact.IconDeviceLaptop,
3422
+ phone: iconsReact.IconDeviceMobile,
3423
+ tablet: iconsReact.IconDeviceTablet
3424
+ };
3425
+ const color = token => token === 'transparent' ? 'transparent' : `var(--mantine-color-${token.replace('.', '-')})`;
3426
+
3427
+ /*
3428
+ * The action buttons, as data. Hover and keyboard focus share the same look,
3429
+ * like the account card's rows. A destructive action keeps its red text on
3430
+ * hover and only gains a light red ground: turning the text black while the
3431
+ * border turned red read as a different button.
3432
+ */
3433
+ const TONES = {
3434
+ default: {
3435
+ rest: {
3436
+ text: 'gray.9',
3437
+ border: 'gray.3',
3438
+ ground: 'transparent'
3439
+ },
3440
+ active: {
3441
+ border: 'gray.9'
3442
+ }
3443
+ },
3444
+ dark: {
3445
+ rest: {
3446
+ text: 'white',
3447
+ border: 'gray.9',
3448
+ ground: 'gray.9'
3449
+ },
3450
+ active: {
3451
+ border: 'gray.7',
3452
+ ground: 'gray.7'
3453
+ }
3454
+ },
3455
+ quiet: {
3456
+ rest: {
3457
+ text: 'gray.6',
3458
+ border: 'transparent',
3459
+ ground: 'transparent'
3460
+ },
3461
+ active: {
3462
+ text: 'gray.9',
3463
+ border: 'gray.3'
3464
+ }
3465
+ },
3466
+ danger: {
3467
+ rest: {
3468
+ text: 'red.8',
3469
+ border: 'transparent',
3470
+ ground: 'transparent'
3471
+ },
3472
+ active: {
3473
+ ground: 'red.0',
3474
+ border: 'red.2'
3475
+ }
3476
+ },
3477
+ dangerOutline: {
3478
+ rest: {
3479
+ text: 'red.8',
3480
+ border: 'gray.3',
3481
+ ground: 'transparent'
3482
+ },
3483
+ active: {
3484
+ ground: 'red.0',
3485
+ border: 'red.2'
3486
+ }
3487
+ },
3488
+ dangerFill: {
3489
+ rest: {
3490
+ text: 'white',
3491
+ border: 'red.8',
3492
+ ground: 'red.8'
3493
+ },
3494
+ active: {
3495
+ border: 'red.9',
3496
+ ground: 'red.9'
3497
+ }
3498
+ }
3499
+ };
3500
+ function ActionButton({
3501
+ tone = 'default',
3502
+ loading = false,
3503
+ disabled = false,
3504
+ children,
3505
+ style,
3506
+ ...others
3507
+ }) {
3508
+ const [isActive, setIsActive] = react.useState(false);
3509
+ const isBlocked = disabled || loading;
3510
+ const look = {
3511
+ ...TONES[tone].rest,
3512
+ ...(isActive && !isBlocked ? TONES[tone].active : {})
3513
+ };
3514
+ return /*#__PURE__*/jsxRuntime.jsxs(core.UnstyledButton, {
3515
+ disabled: isBlocked,
3516
+ onMouseEnter: () => setIsActive(true),
3517
+ onMouseLeave: () => setIsActive(false),
3518
+ onFocus: () => setIsActive(true),
3519
+ onBlur: () => setIsActive(false),
3520
+ style: {
3521
+ display: 'inline-flex',
3522
+ alignItems: 'center',
3523
+ justifyContent: 'center',
3524
+ gap: 6,
3525
+ padding: '5px 12px',
3526
+ fontSize: 12,
3527
+ fontWeight: 700,
3528
+ lineHeight: 1.5,
3529
+ whiteSpace: 'nowrap',
3530
+ borderRadius: 0,
3531
+ color: color(look.text),
3532
+ background: color(look.ground),
3533
+ border: `1px solid ${color(look.border)}`,
3534
+ opacity: disabled ? 0.4 : 1,
3535
+ cursor: isBlocked ? 'default' : 'pointer',
3536
+ ...style
3537
+ },
3538
+ ...others,
3539
+ children: [loading && /*#__PURE__*/jsxRuntime.jsx(core.Loader, {
3540
+ size: 10,
3541
+ color: "currentColor"
3542
+ }), children]
3543
+ });
3544
+ }
3545
+ function SectionLabel({
3546
+ children,
3547
+ note
3548
+ }) {
3549
+ return /*#__PURE__*/jsxRuntime.jsxs(core.Group, {
3550
+ justify: "space-between",
3551
+ align: "baseline",
3552
+ gap: 8,
3553
+ mb: 4,
3554
+ children: [/*#__PURE__*/jsxRuntime.jsx(core.Text, {
3555
+ fz: 11,
3556
+ fw: 800,
3557
+ tt: "uppercase",
3558
+ lts: "1.5px",
3559
+ c: "gray.4",
3560
+ children: children
3561
+ }), note && /*#__PURE__*/jsxRuntime.jsx(core.Text, {
3562
+ fz: 12,
3563
+ fw: 500,
3564
+ c: "gray.5",
3565
+ children: note
3566
+ })]
3567
+ });
3568
+ }
3569
+ function Section({
3570
+ label,
3571
+ note,
3572
+ isFirst,
3573
+ children
3574
+ }) {
3575
+ return /*#__PURE__*/jsxRuntime.jsxs(core.Box, {
3576
+ px: 24,
3577
+ pt: 16,
3578
+ pb: 8,
3579
+ style: isFirst ? undefined : {
3580
+ borderTop: '1px solid var(--mantine-color-gray-2)'
3581
+ },
3582
+ children: [/*#__PURE__*/jsxRuntime.jsx(SectionLabel, {
3583
+ note: note,
3584
+ children: label
3585
+ }), children]
3586
+ });
3587
+ }
3588
+ const rowDivider = {
3589
+ borderTop: '1px solid var(--mantine-color-gray-2)'
3590
+ };
3591
+
3592
+ /** A label, a value and an optional action, on the same 88px label column. */
3593
+ function Row$1({
3594
+ label,
3595
+ children,
3596
+ action,
3597
+ isFirst
3598
+ }) {
3599
+ return /*#__PURE__*/jsxRuntime.jsxs(core.Box, {
3600
+ py: 10,
3601
+ mih: 52,
3602
+ style: {
3603
+ display: 'grid',
3604
+ gridTemplateColumns: label ? '88px 1fr auto' : '1fr auto',
3605
+ alignItems: 'center',
3606
+ gap: 12,
3607
+ ...(isFirst ? {} : rowDivider)
3608
+ },
3609
+ children: [label && /*#__PURE__*/jsxRuntime.jsx(core.Text, {
3610
+ fz: 12,
3611
+ fw: 500,
3612
+ c: "gray.5",
3613
+ children: label
3614
+ }), /*#__PURE__*/jsxRuntime.jsx(core.Box, {
3615
+ fz: 13,
3616
+ fw: 500,
3617
+ c: "gray.9",
3618
+ style: {
3619
+ minWidth: 0,
3620
+ overflowWrap: 'anywhere'
3621
+ },
3622
+ children: children
3623
+ }), action || /*#__PURE__*/jsxRuntime.jsx("span", {})]
3624
+ });
3625
+ }
3626
+ function Hint({
3627
+ children,
3628
+ c = 'gray.5'
3629
+ }) {
3630
+ return /*#__PURE__*/jsxRuntime.jsx(core.Text, {
3631
+ fz: 12,
3632
+ fw: 500,
3633
+ c: c,
3634
+ mt: 2,
3635
+ children: children
3636
+ });
3637
+ }
3638
+
3639
+ /*
3640
+ * A failure is shown where it happened, on the screen itself. Five of the
3641
+ * seven panels passed no `onError`, and a rejected photo or a failed rename
3642
+ * vanished without a word.
3643
+ */
3644
+ function FailureNote({
3645
+ failure,
3646
+ section
3647
+ }) {
3648
+ if (failure?.section !== section) return null;
3649
+ return /*#__PURE__*/jsxRuntime.jsx(core.Text, {
3650
+ role: "alert",
3651
+ fz: 12,
3652
+ fw: 600,
3653
+ c: "red.8",
3654
+ mt: 4,
3655
+ children: failure.message
3656
+ });
3657
+ }
3658
+ function Chip({
3659
+ children,
3660
+ tone = 'outline'
3661
+ }) {
3662
+ const looks = {
3663
+ outline: {
3664
+ c: 'gray.6',
3665
+ bg: 'transparent',
3666
+ border: 'gray.3'
3667
+ },
3668
+ dark: {
3669
+ c: 'white',
3670
+ bg: 'gray.9',
3671
+ border: 'gray.9'
3672
+ },
3673
+ good: {
3674
+ c: 'teal.9',
3675
+ bg: 'teal.0',
3676
+ border: 'teal.2'
3677
+ }
3678
+ };
3679
+ const look = looks[tone];
3680
+ return /*#__PURE__*/jsxRuntime.jsx(core.Text, {
3681
+ component: "span",
3682
+ fz: 10,
3683
+ fw: 800,
3684
+ tt: "uppercase",
3685
+ lts: "1.2px",
3686
+ lh: 1.6,
3687
+ px: 6,
3688
+ c: look.c,
3689
+ bg: look.bg,
3690
+ style: {
3691
+ border: `1px solid ${color(look.border)}`,
3692
+ whiteSpace: 'nowrap',
3693
+ display: 'inline-block'
3694
+ },
3695
+ children: children
3696
+ });
3697
+ }
3698
+
3699
+ /** An icon and a label, side by side, with an optional line under the label. */
3700
+ function WithIcon({
3701
+ icon: Icon,
3702
+ children,
3703
+ detail
3704
+ }) {
3705
+ return /*#__PURE__*/jsxRuntime.jsxs(core.Group, {
3706
+ gap: 10,
3707
+ wrap: "nowrap",
3708
+ align: detail ? 'flex-start' : 'center',
3709
+ children: [/*#__PURE__*/jsxRuntime.jsx(Icon, {
3710
+ size: 18,
3711
+ stroke: 1.5,
3712
+ style: {
3713
+ flex: 'none',
3714
+ color: 'var(--mantine-color-gray-5)',
3715
+ marginTop: detail ? 1 : 0
3716
+ }
3717
+ }), /*#__PURE__*/jsxRuntime.jsxs(core.Box, {
3718
+ style: {
3719
+ minWidth: 0
3720
+ },
3721
+ children: [children, detail && /*#__PURE__*/jsxRuntime.jsx(Hint, {
3722
+ children: detail
3723
+ })]
3724
+ })]
3725
+ });
3726
+ }
3727
+ function SquareAvatar({
3728
+ src,
3729
+ initials,
3730
+ size
3731
+ }) {
3732
+ return /*#__PURE__*/jsxRuntime.jsx(core.Avatar, {
3733
+ src: src || null,
3734
+ alt: "",
3735
+ size: size,
3736
+ radius: 0,
3737
+ color: "gray.9",
3738
+ variant: "filled",
3739
+ styles: {
3740
+ root: {
3741
+ borderRadius: 0,
3742
+ flex: 'none'
3743
+ },
3744
+ placeholder: {
3745
+ fontSize: Math.round(size / 3),
3746
+ fontWeight: 800
3747
+ }
3748
+ },
3749
+ children: initials
3750
+ });
3751
+ }
3752
+ const formatSize = bytes => `${Math.round(bytes / 1024)} KB`;
3753
+
3754
+ /**
3755
+ * @param {object} props
3756
+ * @param {'modal'|'card'} [props.variant='modal']
3757
+ * @param {boolean} [props.opened] - Visibility (modal only)
3758
+ * @param {Function} [props.onClose] - Closing (modal only)
3759
+ * @param {Function} [props.onProfileUpdate] - Receives `{ name }` or `{ image }`
3760
+ * @param {Function} [props.onSessionRevoked] - Receives the ended session's id
3761
+ * @param {Function} [props.onOtherSessionsRevoked]
3762
+ * @param {Function} [props.onProviderUnlinked] - Receives the provider id
3763
+ * @param {Function} [props.onError] - Receives the Error and `{ section, isShownOnScreen: true }`:
3764
+ * the screen already shows the message, so a panel should not toast it again
3765
+ * @param {boolean} [props.showAvatar=true]
3766
+ * @param {boolean} [props.showName=true]
3767
+ * @param {boolean} [props.showEmail=true]
3768
+ * @param {boolean} [props.showSignInMethods=true] - Drawn only when the application enabled a provider
3769
+ * @param {boolean} [props.showSessions=true]
3770
+ * @param {Partial<typeof LABELS>} [props.labels] - To change a word; the defaults are already Portuguese
3771
+ * @param {string} [props.title]
3772
+ * @param {string} [props.subtitle]
3773
+ * @param {string|import('react').ReactNode} [props.logo] - Card variant only
3774
+ * @param {number} [props.logoHeight=28]
3775
+ * @param {number} [props.width=520]
3776
+ * @param {number} [props.maxAvatarSize=512000] - In bytes
3777
+ * @param {import('react').ReactNode} [props.customSections] - Rendered after the built-in sections
3778
+ */
3240
3779
  function UserProfile({
3241
- // Variant
3242
3780
  variant = 'modal',
3243
3781
  opened,
3244
3782
  onClose,
3245
- // Callbacks
3246
3783
  onProfileUpdate,
3247
3784
  onSessionRevoked,
3248
3785
  onOtherSessionsRevoked,
3786
+ onProviderUnlinked,
3249
3787
  onError,
3250
- // Features toggle
3251
3788
  showAvatar = true,
3252
3789
  showName = true,
3253
3790
  showEmail = true,
3791
+ showSignInMethods = true,
3254
3792
  showSessions = true,
3255
- // Customização
3256
3793
  labels = {},
3257
- title = 'Account',
3258
- subtitle = 'Manage your account info.',
3794
+ title,
3795
+ subtitle,
3259
3796
  logo,
3260
3797
  logoHeight = 28,
3261
- width = 500,
3262
- // Avatar config
3798
+ width = 520,
3263
3799
  maxAvatarSize = 500 * 1024,
3264
- // 500KB
3265
-
3266
- // Custom sections (React nodes to render after built-in sections)
3267
3800
  customSections,
3268
3801
  ...containerProps
3269
3802
  }) {
3270
- // Local state - which section is expanded
3271
- const [editingSection, setEditingSection] = react.useState(null); // 'password' | 'email' | 'name' | 'avatar' | null
3272
-
3273
- // Hook para profile
3803
+ const t = {
3804
+ ...LABELS,
3805
+ ...labels
3806
+ };
3807
+ const isVisible = variant === 'card' || Boolean(opened);
3274
3808
  const {
3275
3809
  user,
3276
3810
  updateProfile,
3277
3811
  loadingUpdateProfile
3278
3812
  } = useUser();
3279
-
3280
- // Hook para sessions
3281
3813
  const {
3282
3814
  currentSession,
3283
3815
  sessions,
@@ -3288,698 +3820,650 @@ function UserProfile({
3288
3820
  loadingListSessions,
3289
3821
  loadingRevokeSession
3290
3822
  } = useSessions();
3823
+ const identity = describeUser(user);
3291
3824
 
3292
- // Load sessions when opened (modal) or component mounts (card), and when sessions section is opened
3293
- react.useEffect(() => {
3294
- if (showSessions && (variant === 'card' || opened)) {
3295
- // Fetch current session first to get the session ID, then list all sessions
3296
- getSession().catch(err => console.warn('Failed to get current session:', err));
3297
- listSessions().catch(err => console.warn('Failed to load sessions:', err));
3298
- }
3299
- }, [opened, showSessions, variant]);
3300
-
3301
- // Helper to parse user agent string
3302
- const parseUserAgent = ua => {
3303
- if (!ua) return {
3304
- browser: 'Unknown Browser',
3305
- os: 'Unknown OS'
3306
- };
3307
- let browser = 'Unknown Browser';
3308
- let os = 'Unknown OS';
3309
-
3310
- // Detect browser
3311
- if (ua.includes('Chrome') && !ua.includes('Edg')) browser = 'Chrome';else if (ua.includes('Firefox')) browser = 'Firefox';else if (ua.includes('Safari') && !ua.includes('Chrome')) browser = 'Safari';else if (ua.includes('Edg')) browser = 'Edge';else if (ua.includes('Opera') || ua.includes('OPR')) browser = 'Opera';
3312
-
3313
- // Detect OS
3314
- if (ua.includes('Windows')) os = 'Windows';else if (ua.includes('Mac OS')) os = 'macOS';else if (ua.includes('Linux')) os = 'Linux';else if (ua.includes('Android')) os = 'Android';else if (ua.includes('iPhone') || ua.includes('iPad')) os = 'iOS';
3315
- return {
3316
- browser,
3317
- os
3318
- };
3319
- };
3320
-
3321
- // Session handlers
3322
- const handleRevokeSession = async sessionId => {
3323
- // Check if revoking current session
3324
- const isCurrentSession = sessionId === currentSession?.id || sessions.length === 1;
3325
-
3326
- // If revoking current session, handle logout immediately after revocation
3327
- if (isCurrentSession) {
3328
- try {
3329
- await revokeSession(sessionId);
3330
- onSessionRevoked?.(sessionId);
3331
- } catch (error) {
3332
- onError?.(error);
3333
- // Even if it fails, we're revoking our own session, so just logout
3334
- // The server already revoked our session
3335
- }
3336
- // Clear auth state and redirect
3337
- localStorage.removeItem('auth:token');
3338
- window.dispatchEvent(new CustomEvent('auth:session-revoked'));
3339
- if (variant === 'modal') onClose?.();
3340
- return;
3341
- }
3342
-
3343
- // Revoking another session
3344
- try {
3345
- await revokeSession(sessionId);
3346
- onSessionRevoked?.(sessionId);
3347
- } catch (error) {
3348
- // Check for 401 error (our SDK uses error.res, axios uses error.response)
3349
- const status = error.res?.status || error.response?.status;
3350
- if (status === 401) {
3351
- // This means our session was revoked, not the target one - do logout
3352
- localStorage.removeItem('auth:token');
3353
- window.dispatchEvent(new CustomEvent('auth:session-revoked'));
3354
- if (variant === 'modal') onClose?.();
3355
- return;
3356
- }
3357
- onError?.(error);
3358
- }
3359
- };
3360
- const handleRevokeOtherSessions = async () => {
3361
- try {
3362
- await revokeOtherSessions();
3363
- onOtherSessionsRevoked?.();
3364
- } catch (error) {
3365
- onError?.(error);
3366
- }
3367
- };
3368
-
3369
- // Name form
3825
+ // One editor open at a time: 'name', 'avatar' or null.
3826
+ const [editing, setEditing] = react.useState(null);
3827
+ const [avatarPreview, setAvatarPreview] = react.useState(null);
3828
+ const [isDragging, setIsDragging] = react.useState(false);
3829
+ const [areSessionsOpen, setAreSessionsOpen] = react.useState(false);
3830
+ const [isSessionsRowActive, setIsSessionsRowActive] = react.useState(false);
3831
+ const [isConfirmingEnd, setIsConfirmingEnd] = react.useState(false);
3832
+ const [providers, setProviders] = react.useState([]);
3833
+ const [linked, setLinked] = react.useState([]);
3834
+ const [pendingProvider, setPendingProvider] = react.useState(null);
3835
+ const [failure, setFailure] = react.useState(null);
3836
+ const sessionsListId = react.useId();
3370
3837
  const nameForm = form.useForm({
3371
3838
  initialValues: {
3372
3839
  name: ''
3373
3840
  },
3374
3841
  validate: {
3375
- name: v => !v ? labels.nameRequired || 'Nome obrigatório' : null
3842
+ name: value => value.trim() ? null : t.nameRequired
3376
3843
  }
3377
3844
  });
3378
3845
 
3379
- // Avatar state (base64)
3380
- const [avatarPreview, setAvatarPreview] = react.useState(null);
3381
- const [avatarFile, setAvatarFile] = react.useState(null);
3846
+ // Sessions and sign-in methods load when the screen is shown, not before.
3847
+ react.useEffect(() => {
3848
+ if (!isVisible) return;
3849
+ if (showSessions) {
3850
+ getSession().catch(error => console.warn('[AuthSDK] Failed to read the current session:', error.message));
3851
+ listSessions().catch(error => console.warn('[AuthSDK] Failed to list sessions:', error.message));
3852
+ }
3853
+ if (showSignInMethods) refreshSignInMethods();
3854
+ }, [isVisible, showSessions, showSignInMethods]);
3382
3855
 
3383
- // Handle file selection and convert to base64
3384
- const handleAvatarFileChange = file => {
3385
- if (!file) {
3386
- setAvatarPreview(null);
3387
- setAvatarFile(null);
3388
- return;
3856
+ // Closing the modal resets it: the next opening starts at rest.
3857
+ react.useEffect(() => {
3858
+ if (isVisible) return;
3859
+ closeEditor();
3860
+ setAreSessionsOpen(false);
3861
+ setIsConfirmingEnd(false);
3862
+ }, [isVisible]);
3863
+ async function refreshSignInMethods() {
3864
+ const available = await getSocialProviders();
3865
+ setProviders(available);
3866
+ if (available.length === 0) return;
3867
+ try {
3868
+ setLinked(await getLinkedProviders());
3869
+ } catch (error) {
3870
+ console.warn('[AuthSDK] Failed to list linked providers:', error.message);
3871
+ setLinked([]);
3389
3872
  }
3873
+ }
3390
3874
 
3391
- // Validate file type
3392
- if (!file.type.startsWith('image/')) {
3393
- onError?.(new Error(labels.avatarInvalidType || 'Por favor, selecione uma imagem válida'));
3875
+ /*
3876
+ * `onError` still fires, with `isShownOnScreen: true`: a panel that wants a
3877
+ * log or a metric has it, and knows not to show a second message.
3878
+ */
3879
+ function fail(section, error) {
3880
+ setFailure({
3881
+ section,
3882
+ message: error?.message || t.genericFailure
3883
+ });
3884
+ onError?.(error, {
3885
+ section,
3886
+ isShownOnScreen: true
3887
+ });
3888
+ }
3889
+ function closeEditor() {
3890
+ setFailure(null);
3891
+ setEditing(null);
3892
+ setAvatarPreview(null);
3893
+ setIsDragging(false);
3894
+ nameForm.reset();
3895
+ }
3896
+ function openEditor(section) {
3897
+ closeEditor();
3898
+ setEditing(section);
3899
+ if (section === 'name') nameForm.setValues({
3900
+ name: identity.name
3901
+ });
3902
+ }
3903
+ async function handleSaveName(values) {
3904
+ const name = values.name.trim();
3905
+ try {
3906
+ await updateProfile({
3907
+ name
3908
+ });
3909
+ closeEditor();
3910
+ onProfileUpdate?.({
3911
+ name
3912
+ });
3913
+ } catch (error) {
3914
+ fail('name', error);
3915
+ }
3916
+ }
3917
+ function handleAvatarFile(file) {
3918
+ if (!file) return;
3919
+ setFailure(null);
3920
+ if (!file.type?.startsWith('image/')) {
3921
+ fail('avatar', new Error(t.avatarInvalidType));
3394
3922
  return;
3395
3923
  }
3396
-
3397
- // Validate file size
3398
3924
  if (file.size > maxAvatarSize) {
3399
- onError?.(new Error(labels.avatarTooLarge || `Imagem muito grande. Máximo ${Math.round(maxAvatarSize / 1024)}KB.`));
3925
+ fail('avatar', new Error(t.avatarTooLarge.replace('{size}', formatSize(maxAvatarSize))));
3400
3926
  return;
3401
3927
  }
3402
- setAvatarFile(file);
3403
-
3404
- // Convert to base64
3405
3928
  const reader = new FileReader();
3406
- reader.onloadend = () => {
3407
- setAvatarPreview(reader.result);
3408
- };
3929
+ reader.onloadend = () => setAvatarPreview(reader.result);
3409
3930
  reader.readAsDataURL(file);
3410
- };
3411
-
3412
- // Populate forms when user data is available or section opens
3413
- react.useEffect(() => {
3414
- if (editingSection === 'name' && user?.name) {
3415
- nameForm.setValues({
3416
- name: user.name
3417
- });
3418
- }
3419
- if (editingSection === 'avatar' && user?.image) {
3420
- setAvatarPreview(user.image);
3421
- }
3422
- }, [editingSection, user]);
3423
- const handleToggleSection = section => {
3424
- if (editingSection === section) {
3425
- setEditingSection(null);
3426
- nameForm.reset();
3427
- setAvatarPreview(null);
3428
- setAvatarFile(null);
3429
- } else {
3430
- setEditingSection(section);
3431
- }
3432
- };
3433
- const handleChangeName = async values => {
3931
+ }
3932
+ async function saveAvatar(image) {
3434
3933
  try {
3435
3934
  await updateProfile({
3436
- name: values.name
3935
+ image
3437
3936
  });
3438
- nameForm.reset();
3439
- setEditingSection(null);
3937
+ closeEditor();
3440
3938
  onProfileUpdate?.({
3441
- name: values.name
3939
+ image
3442
3940
  });
3443
3941
  } catch (error) {
3444
- onError?.(error);
3942
+ fail('avatar', error);
3445
3943
  }
3446
- };
3447
- const handleChangeAvatar = async () => {
3448
- if (!avatarPreview) {
3449
- // Optionally handle this validation error via callback or just return?
3450
- // Since it's a validation error before async call, we might want to expose it too?
3451
- // The original code used notifications. Let's send to onError for consistency or just return if it's UI state.
3452
- // Actually, for validation within the component, maybe we can just let it be silent or use form error if applicable?
3453
- // But this is outside form context. Let's use onError with a custom error object.
3454
- onError?.(new Error(labels.avatarRequired || 'Selecione uma imagem'));
3455
- return;
3944
+ }
3945
+ async function handleUnlink(provider) {
3946
+ setPendingProvider(provider);
3947
+ setFailure(null);
3948
+ try {
3949
+ await unlinkSocialProvider(provider);
3950
+ setLinked(current => current.filter(item => item.provider !== provider));
3951
+ onProviderUnlinked?.(provider);
3952
+ } catch (error) {
3953
+ fail('signIn', error);
3954
+ } finally {
3955
+ setPendingProvider(null);
3456
3956
  }
3957
+ }
3958
+ async function handleLink(provider) {
3959
+ setPendingProvider(provider);
3960
+ setFailure(null);
3457
3961
  try {
3458
- await updateProfile({
3459
- image: avatarPreview
3460
- });
3461
- setAvatarPreview(null);
3462
- setAvatarFile(null);
3463
- setEditingSection(null);
3464
- onProfileUpdate?.({
3465
- image: avatarPreview
3466
- });
3962
+ // Leaves for the provider's consent screen; the page navigates away.
3963
+ await startSocialLink(provider);
3467
3964
  } catch (error) {
3468
- onError?.(error);
3965
+ setPendingProvider(null);
3966
+ fail('signIn', error);
3469
3967
  }
3470
- };
3471
- const handleRemoveAvatar = async () => {
3968
+ }
3969
+ async function handleEndSession(sessionId) {
3970
+ setFailure(null);
3472
3971
  try {
3473
- await updateProfile({
3474
- image: ''
3475
- });
3476
- setAvatarPreview(null);
3477
- setAvatarFile(null);
3478
- setEditingSection(null);
3479
- onProfileUpdate?.({
3480
- image: ''
3481
- });
3972
+ await revokeSession(sessionId);
3973
+ onSessionRevoked?.(sessionId);
3482
3974
  } catch (error) {
3483
- onError?.(error);
3975
+ fail('sessions', error);
3484
3976
  }
3485
- };
3486
-
3487
- // Reusable Section Header
3488
- const SectionHeader = ({
3489
- icon: Icon,
3490
- sectionTitle,
3491
- description
3492
- }) => /*#__PURE__*/jsxRuntime.jsxs(core.Group, {
3493
- gap: "sm",
3494
- mb: "lg",
3495
- children: [/*#__PURE__*/jsxRuntime.jsx(core.ThemeIcon, {
3496
- size: 36,
3497
- variant: "subtle",
3498
- color: "gray",
3499
- children: /*#__PURE__*/jsxRuntime.jsx(Icon, {
3500
- size: 28,
3977
+ }
3978
+ async function handleEndOthers() {
3979
+ setFailure(null);
3980
+ try {
3981
+ await revokeOtherSessions();
3982
+ setIsConfirmingEnd(false);
3983
+ onOtherSessionsRevoked?.();
3984
+ } catch (error) {
3985
+ fail('sessions', error);
3986
+ }
3987
+ }
3988
+ if (!user) return null;
3989
+ const ordered = orderSessions(sessions, currentSession?.id);
3990
+ const current = ordered.find(item => item.id === currentSession?.id);
3991
+ const othersCount = ordered.filter(item => item.id !== currentSession?.id).length;
3992
+ const hasProfile = showAvatar || showName || showEmail;
3993
+ const hasSignInMethods = showSignInMethods && providers.length > 0;
3994
+ const sessionsSummary = current ? `${deviceLabel(describeDevice(current.userAgent))} (${t.thisDevice.toLowerCase()})${othersCount ? ` e mais ${othersCount}` : ''}` : null;
3995
+ const header = /*#__PURE__*/jsxRuntime.jsxs(core.Group, {
3996
+ align: "flex-start",
3997
+ wrap: "nowrap",
3998
+ gap: 12,
3999
+ px: 24,
4000
+ pt: 20,
4001
+ pb: 16,
4002
+ style: {
4003
+ borderBottom: '1px solid var(--mantine-color-gray-2)'
4004
+ },
4005
+ children: [variant === 'card' && logo && (typeof logo === 'string' ? /*#__PURE__*/jsxRuntime.jsx(core.Image, {
4006
+ src: logo,
4007
+ alt: "",
4008
+ h: logoHeight,
4009
+ w: "auto",
4010
+ fit: "contain"
4011
+ }) : logo), /*#__PURE__*/jsxRuntime.jsxs(core.Box, {
4012
+ style: {
4013
+ flex: 1,
4014
+ minWidth: 0
4015
+ },
4016
+ children: [/*#__PURE__*/jsxRuntime.jsx(core.Text, {
4017
+ component: variant === 'modal' ? core.Modal.Title : 'h2',
4018
+ m: 0,
4019
+ fz: 20,
4020
+ fw: 900,
4021
+ lts: "-0.03em",
4022
+ lh: 1.2,
4023
+ c: "gray.9",
4024
+ children: title || t.title
4025
+ }), /*#__PURE__*/jsxRuntime.jsx(Hint, {
4026
+ children: subtitle || t.subtitle
4027
+ })]
4028
+ }), variant === 'modal' && /*#__PURE__*/jsxRuntime.jsx(ActionButton, {
4029
+ "aria-label": t.close,
4030
+ onClick: onClose,
4031
+ style: {
4032
+ width: 32,
4033
+ height: 32,
4034
+ padding: 0,
4035
+ flex: 'none'
4036
+ },
4037
+ children: /*#__PURE__*/jsxRuntime.jsx(iconsReact.IconX, {
4038
+ size: 16,
3501
4039
  stroke: 1.5
3502
4040
  })
3503
- }), /*#__PURE__*/jsxRuntime.jsxs(core.Stack, {
3504
- gap: 0,
4041
+ })]
4042
+ });
4043
+ const identityStrip = /*#__PURE__*/jsxRuntime.jsxs(core.Group, {
4044
+ gap: 14,
4045
+ wrap: "nowrap",
4046
+ px: 24,
4047
+ py: 18,
4048
+ bg: "gray.1",
4049
+ style: {
4050
+ borderBottom: '1px solid var(--mantine-color-gray-2)'
4051
+ },
4052
+ children: [/*#__PURE__*/jsxRuntime.jsx(SquareAvatar, {
4053
+ src: identity.image,
4054
+ initials: identity.initials,
4055
+ size: 48
4056
+ }), /*#__PURE__*/jsxRuntime.jsxs(core.Box, {
4057
+ style: {
4058
+ minWidth: 0
4059
+ },
3505
4060
  children: [/*#__PURE__*/jsxRuntime.jsx(core.Text, {
3506
- fw: 600,
3507
- size: "sm",
3508
- children: sectionTitle
3509
- }), description && /*#__PURE__*/jsxRuntime.jsx(core.Text, {
3510
- size: "xs",
3511
- c: "dimmed",
3512
- children: description
4061
+ fz: 15,
4062
+ fw: 800,
4063
+ c: "gray.9",
4064
+ lh: 1.3,
4065
+ truncate: "end",
4066
+ children: identity.title
4067
+ }), identity.hasRealName && identity.email && /*#__PURE__*/jsxRuntime.jsx(core.Text, {
4068
+ fz: 12,
4069
+ fw: 500,
4070
+ c: "gray.5",
4071
+ truncate: "end",
4072
+ children: identity.email
3513
4073
  })]
3514
4074
  })]
3515
4075
  });
3516
-
3517
- // Reusable Row component
3518
- const SettingRow = ({
3519
- label,
3520
- children,
3521
- action,
3522
- actionLabel,
3523
- onClick,
3524
- expanded
3525
- }) => /*#__PURE__*/jsxRuntime.jsx(core.Box, {
3526
- py: "xs",
3527
- children: /*#__PURE__*/jsxRuntime.jsxs(core.Group, {
3528
- justify: "space-between",
3529
- wrap: "nowrap",
3530
- align: "center",
3531
- children: [/*#__PURE__*/jsxRuntime.jsxs(core.Group, {
3532
- gap: "xl",
3533
- wrap: "nowrap",
3534
- flex: 1,
3535
- children: [/*#__PURE__*/jsxRuntime.jsx(core.Text, {
3536
- size: "sm",
3537
- c: "dimmed",
3538
- w: 100,
3539
- children: label
3540
- }), /*#__PURE__*/jsxRuntime.jsx(core.Box, {
3541
- flex: 1,
3542
- children: children
4076
+ const avatarEditor = /*#__PURE__*/jsxRuntime.jsxs(core.Stack, {
4077
+ gap: 10,
4078
+ py: 12,
4079
+ children: [/*#__PURE__*/jsxRuntime.jsx(core.Text, {
4080
+ fz: 12,
4081
+ fw: 700,
4082
+ c: "gray.9",
4083
+ children: t.avatar
4084
+ }), /*#__PURE__*/jsxRuntime.jsx(core.FileButton, {
4085
+ onChange: handleAvatarFile,
4086
+ accept: "image/png,image/jpeg,image/gif,image/webp",
4087
+ children: props => /*#__PURE__*/jsxRuntime.jsxs(core.UnstyledButton, {
4088
+ ...props,
4089
+ onDragOver: event => {
4090
+ event.preventDefault();
4091
+ setIsDragging(true);
4092
+ },
4093
+ onDragLeave: () => setIsDragging(false),
4094
+ onDrop: event => {
4095
+ event.preventDefault();
4096
+ setIsDragging(false);
4097
+ handleAvatarFile(event.dataTransfer.files?.[0]);
4098
+ },
4099
+ style: {
4100
+ display: 'flex',
4101
+ alignItems: 'center',
4102
+ gap: 14,
4103
+ padding: 14,
4104
+ borderRadius: 0,
4105
+ background: isDragging ? 'var(--mantine-color-gray-2)' : 'var(--mantine-color-gray-1)',
4106
+ border: `1px dashed var(--mantine-color-${isDragging ? 'gray-9' : 'gray-4'})`
4107
+ },
4108
+ children: [/*#__PURE__*/jsxRuntime.jsx(SquareAvatar, {
4109
+ src: avatarPreview || identity.image,
4110
+ initials: identity.initials,
4111
+ size: 64
4112
+ }), /*#__PURE__*/jsxRuntime.jsxs(core.Box, {
4113
+ children: [/*#__PURE__*/jsxRuntime.jsx(core.Text, {
4114
+ fz: 13,
4115
+ fw: 700,
4116
+ c: "gray.9",
4117
+ children: t.avatarPrompt
4118
+ }), /*#__PURE__*/jsxRuntime.jsx(Hint, {
4119
+ children: t.avatarHint.replace('{size}', formatSize(maxAvatarSize))
4120
+ })]
4121
+ })]
4122
+ })
4123
+ }), /*#__PURE__*/jsxRuntime.jsx(FailureNote, {
4124
+ failure: failure,
4125
+ section: "avatar"
4126
+ }), /*#__PURE__*/jsxRuntime.jsxs(core.Group, {
4127
+ justify: "flex-end",
4128
+ gap: 8,
4129
+ children: [identity.image && !avatarPreview && /*#__PURE__*/jsxRuntime.jsx(ActionButton, {
4130
+ tone: "danger",
4131
+ loading: loadingUpdateProfile,
4132
+ onClick: () => saveAvatar(''),
4133
+ style: {
4134
+ marginRight: 'auto'
4135
+ },
4136
+ children: t.remove
4137
+ }), /*#__PURE__*/jsxRuntime.jsx(ActionButton, {
4138
+ tone: "quiet",
4139
+ onClick: closeEditor,
4140
+ children: t.cancel
4141
+ }), /*#__PURE__*/jsxRuntime.jsx(ActionButton, {
4142
+ tone: "dark",
4143
+ loading: loadingUpdateProfile,
4144
+ disabled: !avatarPreview,
4145
+ onClick: () => saveAvatar(avatarPreview),
4146
+ children: t.save
4147
+ })]
4148
+ })]
4149
+ });
4150
+ const nameEditor = /*#__PURE__*/jsxRuntime.jsx("form", {
4151
+ onSubmit: nameForm.onSubmit(handleSaveName),
4152
+ style: {
4153
+ ...rowDivider,
4154
+ padding: '12px 0 14px'
4155
+ },
4156
+ children: /*#__PURE__*/jsxRuntime.jsxs(core.Stack, {
4157
+ gap: 10,
4158
+ children: [/*#__PURE__*/jsxRuntime.jsx(core.TextInput, {
4159
+ label: t.name,
4160
+ placeholder: t.namePlaceholder,
4161
+ autoComplete: "name",
4162
+ "data-autofocus": true,
4163
+ autoFocus: true,
4164
+ radius: 0,
4165
+ size: "sm",
4166
+ styles: {
4167
+ label: {
4168
+ fontSize: 12,
4169
+ fontWeight: 700,
4170
+ color: 'var(--mantine-color-gray-9)',
4171
+ marginBottom: 6
4172
+ }
4173
+ },
4174
+ onKeyDown: event => {
4175
+ if (event.key !== 'Escape') return;
4176
+ event.stopPropagation();
4177
+ closeEditor();
4178
+ },
4179
+ ...nameForm.getInputProps('name')
4180
+ }), /*#__PURE__*/jsxRuntime.jsx(Hint, {
4181
+ children: t.nameHint
4182
+ }), /*#__PURE__*/jsxRuntime.jsx(FailureNote, {
4183
+ failure: failure,
4184
+ section: "name"
4185
+ }), /*#__PURE__*/jsxRuntime.jsxs(core.Group, {
4186
+ justify: "flex-end",
4187
+ gap: 8,
4188
+ children: [/*#__PURE__*/jsxRuntime.jsx(ActionButton, {
4189
+ tone: "quiet",
4190
+ onClick: closeEditor,
4191
+ children: t.cancel
4192
+ }), /*#__PURE__*/jsxRuntime.jsx(ActionButton, {
4193
+ tone: "dark",
4194
+ type: "submit",
4195
+ loading: loadingUpdateProfile,
4196
+ children: t.save
3543
4197
  })]
3544
- }), action && /*#__PURE__*/jsxRuntime.jsx(core.Tooltip, {
3545
- label: actionLabel || action,
3546
- position: "left",
3547
- children: /*#__PURE__*/jsxRuntime.jsx(core.Anchor, {
3548
- size: "xs",
3549
- fw: 300,
3550
- onClick: onClick,
3551
- c: "gray",
3552
- underline: "none",
3553
- children: expanded ? labels.cancel || 'Cancel' : action
3554
- })
3555
4198
  })]
3556
4199
  })
3557
4200
  });
3558
-
3559
- // Conteúdo interno compartilhado
3560
- const profileContent = /*#__PURE__*/jsxRuntime.jsxs(jsxRuntime.Fragment, {
3561
- children: [(showAvatar || showName || showEmail) && /*#__PURE__*/jsxRuntime.jsxs(core.Box, {
3562
- mb: "lg",
3563
- children: [/*#__PURE__*/jsxRuntime.jsx(SectionHeader, {
3564
- icon: iconsReact.IconUser,
3565
- sectionTitle: labels.profileSection || 'Profile',
3566
- description: labels.profileDescription || 'Your personal information'
3567
- }), /*#__PURE__*/jsxRuntime.jsxs(core.Stack, {
3568
- gap: "sm",
3569
- children: [showAvatar && /*#__PURE__*/jsxRuntime.jsxs(jsxRuntime.Fragment, {
3570
- children: [/*#__PURE__*/jsxRuntime.jsx(SettingRow, {
3571
- label: labels.avatar || 'Avatar',
3572
- action: labels.update || 'Update',
3573
- actionLabel: labels.updateAvatar || 'Update your profile picture',
3574
- onClick: () => handleToggleSection('avatar'),
3575
- expanded: editingSection === 'avatar',
3576
- children: /*#__PURE__*/jsxRuntime.jsx(core.Group, {
3577
- gap: "md",
3578
- children: /*#__PURE__*/jsxRuntime.jsx(core.Avatar, {
3579
- src: user?.image,
3580
- name: user?.name || user?.email,
3581
- size: 48,
3582
- radius: "xl"
3583
- // color="initials"
3584
- })
3585
- })
3586
- }), /*#__PURE__*/jsxRuntime.jsx(core.Collapse, {
3587
- in: editingSection === 'avatar',
3588
- children: /*#__PURE__*/jsxRuntime.jsx(core.Paper, {
3589
- p: "sm",
3590
- withBorder: true,
3591
- radius: "sm",
3592
- children: /*#__PURE__*/jsxRuntime.jsxs(core.Stack, {
3593
- gap: "md",
3594
- align: "center",
3595
- children: [/*#__PURE__*/jsxRuntime.jsx(core.FileButton, {
3596
- onChange: handleAvatarFileChange,
3597
- accept: "image/*",
3598
- children: props => /*#__PURE__*/jsxRuntime.jsx(core.Tooltip, {
3599
- label: labels.clickToChange || 'Clique para alterar',
3600
- position: "bottom",
3601
- children: /*#__PURE__*/jsxRuntime.jsxs(core.Box, {
3602
- ...props,
3603
- pos: "relative",
3604
- style: {
3605
- cursor: 'pointer'
3606
- },
3607
- children: [/*#__PURE__*/jsxRuntime.jsx(core.Avatar, {
3608
- src: avatarPreview || user?.image,
3609
- name: user?.name || user?.email,
3610
- size: 80,
3611
- radius: 80,
3612
- color: "gray"
3613
- }), /*#__PURE__*/jsxRuntime.jsx(core.ThemeIcon, {
3614
- size: 26,
3615
- radius: "xl",
3616
- color: "gray",
3617
- pos: "absolute",
3618
- bottom: 0,
3619
- right: 0,
3620
- bd: "2px solid body",
3621
- children: /*#__PURE__*/jsxRuntime.jsx(iconsReact.IconPhoto, {
3622
- size: 14,
3623
- stroke: 1.5
3624
- })
3625
- })]
3626
- })
3627
- })
3628
- }), /*#__PURE__*/jsxRuntime.jsx(core.Text, {
3629
- size: "xs",
3630
- c: "dimmed",
3631
- ta: "center",
3632
- children: labels.avatarHint || `Máximo ${Math.round(maxAvatarSize / 1024)}KB • JPG, PNG, GIF, WebP`
3633
- }), /*#__PURE__*/jsxRuntime.jsxs(core.Group, {
3634
- justify: "center",
3635
- gap: "xs",
3636
- children: [user?.image && /*#__PURE__*/jsxRuntime.jsx(core.Button, {
3637
- variant: "subtle",
3638
- color: "gray",
3639
- size: "xs",
3640
- onClick: handleRemoveAvatar,
3641
- loading: loadingUpdateProfile,
3642
- loaderProps: {
3643
- size: 12
3644
- },
3645
- leftSection: /*#__PURE__*/jsxRuntime.jsx(iconsReact.IconTrash, {
3646
- size: 14,
3647
- stroke: 1.5
3648
- }),
3649
- children: labels.remove || 'Remover'
3650
- }), /*#__PURE__*/jsxRuntime.jsx(core.Button, {
3651
- variant: "default",
3652
- size: "xs",
3653
- onClick: () => handleToggleSection('avatar'),
3654
- children: labels.cancel || 'Cancelar'
3655
- }), /*#__PURE__*/jsxRuntime.jsx(core.Button, {
3656
- size: "xs",
3657
- loading: loadingUpdateProfile,
3658
- loaderProps: {
3659
- size: 12
3660
- },
3661
- leftSection: /*#__PURE__*/jsxRuntime.jsx(iconsReact.IconCheck, {
3662
- size: 14,
3663
- stroke: 1.5
3664
- }),
3665
- onClick: handleChangeAvatar,
3666
- disabled: !avatarPreview || avatarPreview === user?.image,
3667
- children: labels.save || 'Salvar'
3668
- })]
3669
- })]
3670
- })
3671
- })
3672
- })]
3673
- }), showName && /*#__PURE__*/jsxRuntime.jsxs(jsxRuntime.Fragment, {
3674
- children: [/*#__PURE__*/jsxRuntime.jsx(SettingRow, {
3675
- label: labels.name || 'Nome',
3676
- action: labels.change || 'Change',
3677
- actionLabel: labels.changeName || 'Change your display name',
3678
- onClick: () => handleToggleSection('name'),
3679
- expanded: editingSection === 'name',
3680
- children: /*#__PURE__*/jsxRuntime.jsxs(core.Group, {
3681
- gap: "xs",
3682
- children: [/*#__PURE__*/jsxRuntime.jsx(iconsReact.IconPencil, {
3683
- size: 18,
3684
- stroke: 1.5,
3685
- color: "var(--mantine-color-dimmed)"
3686
- }), /*#__PURE__*/jsxRuntime.jsx(core.Text, {
3687
- size: "sm",
3688
- children: user?.name || labels.notDefined || 'Não definido'
3689
- })]
3690
- })
3691
- }), /*#__PURE__*/jsxRuntime.jsx(core.Collapse, {
3692
- in: editingSection === 'name',
3693
- children: /*#__PURE__*/jsxRuntime.jsx(core.Paper, {
3694
- p: "sm",
3695
- withBorder: true,
3696
- radius: "sm",
3697
- children: /*#__PURE__*/jsxRuntime.jsx("form", {
3698
- onSubmit: nameForm.onSubmit(handleChangeName),
3699
- children: /*#__PURE__*/jsxRuntime.jsxs(core.Stack, {
3700
- gap: "sm",
3701
- children: [/*#__PURE__*/jsxRuntime.jsx(core.TextInput, {
3702
- label: labels.name || 'Nome',
3703
- placeholder: labels.namePlaceholder || 'Digite seu nome',
3704
- leftSection: /*#__PURE__*/jsxRuntime.jsx(iconsReact.IconUser, {
3705
- size: 16,
3706
- stroke: 1.5
3707
- }),
3708
- ...nameForm.getInputProps('name')
3709
- }), /*#__PURE__*/jsxRuntime.jsxs(core.Group, {
3710
- justify: "flex-end",
3711
- gap: "xs",
3712
- children: [/*#__PURE__*/jsxRuntime.jsx(core.Button, {
3713
- variant: "default",
3714
- size: "xs",
3715
- onClick: () => handleToggleSection('name'),
3716
- children: labels.cancel || 'Cancelar'
3717
- }), /*#__PURE__*/jsxRuntime.jsx(core.Button, {
3718
- type: "submit",
3719
- size: "xs",
3720
- loading: loadingUpdateProfile,
3721
- loaderProps: {
3722
- size: 12
3723
- },
3724
- leftSection: /*#__PURE__*/jsxRuntime.jsx(iconsReact.IconCheck, {
3725
- size: 14,
3726
- stroke: 1.5
3727
- }),
3728
- children: labels.save || 'Salvar'
3729
- })]
3730
- })]
3731
- })
3732
- })
3733
- })
3734
- })]
3735
- }), showEmail && /*#__PURE__*/jsxRuntime.jsx(SettingRow, {
3736
- label: labels.email || 'Email',
4201
+ const sessionsList = /*#__PURE__*/jsxRuntime.jsxs(core.Box, {
4202
+ id: sessionsListId,
4203
+ mb: 10,
4204
+ children: [loadingListSessions && ordered.length === 0 ? /*#__PURE__*/jsxRuntime.jsx(Hint, {
4205
+ children: t.loadingSessions
4206
+ }) : ordered.length === 0 ? /*#__PURE__*/jsxRuntime.jsx(Hint, {
4207
+ children: t.noSessionsFound
4208
+ }) : ordered.map((item, index) => {
4209
+ const device = describeDevice(item.userAgent);
4210
+ const isCurrent = item.id === currentSession?.id;
4211
+ const since = formatSessionStart(item.createdAt);
4212
+ return /*#__PURE__*/jsxRuntime.jsx(Row$1, {
4213
+ isFirst: index === 0,
4214
+ action: isCurrent ? null : /*#__PURE__*/jsxRuntime.jsx(ActionButton, {
4215
+ tone: "danger",
4216
+ loading: loadingRevokeSession === item.id,
4217
+ onClick: () => handleEndSession(item.id),
4218
+ "aria-label": `${t.end} ${deviceLabel(device)}`,
4219
+ children: t.end
4220
+ }),
4221
+ children: /*#__PURE__*/jsxRuntime.jsx(WithIcon, {
4222
+ icon: DEVICE_MARKS[device.kind] || iconsReact.IconDeviceDesktop,
4223
+ detail: `${item.ipAddress ? `IP ${item.ipAddress}` : t.unknownIP}${since ? ` · ${t.since} ${since}` : ''}`,
3737
4224
  children: /*#__PURE__*/jsxRuntime.jsxs(core.Group, {
3738
- gap: "xs",
3739
- children: [/*#__PURE__*/jsxRuntime.jsx(iconsReact.IconMail, {
3740
- size: 18,
3741
- stroke: 1.5,
3742
- color: "var(--mantine-color-dimmed)"
3743
- }), /*#__PURE__*/jsxRuntime.jsx(core.Text, {
3744
- size: "sm",
3745
- children: user?.email || 'email@exemplo.com'
4225
+ gap: 6,
4226
+ wrap: "wrap",
4227
+ children: [/*#__PURE__*/jsxRuntime.jsx("span", {
4228
+ children: deviceLabel(device)
4229
+ }), isCurrent && /*#__PURE__*/jsxRuntime.jsx(Chip, {
4230
+ tone: "dark",
4231
+ children: t.thisDevice
3746
4232
  })]
3747
4233
  })
4234
+ })
4235
+ }, item.id);
4236
+ }), /*#__PURE__*/jsxRuntime.jsx(FailureNote, {
4237
+ failure: failure,
4238
+ section: "sessions"
4239
+ }), othersCount > 0 && current && (isConfirmingEnd ? /*#__PURE__*/jsxRuntime.jsxs(core.Stack, {
4240
+ role: "alertdialog",
4241
+ "aria-label": endOthersQuestion(othersCount),
4242
+ gap: 10,
4243
+ p: 12,
4244
+ mt: 4,
4245
+ bg: "red.0",
4246
+ style: {
4247
+ border: '1px solid var(--mantine-color-red-2)'
4248
+ },
4249
+ children: [/*#__PURE__*/jsxRuntime.jsxs(core.Text, {
4250
+ fz: 12,
4251
+ fw: 500,
4252
+ c: "gray.7",
4253
+ children: [/*#__PURE__*/jsxRuntime.jsx(core.Text, {
4254
+ span: true,
4255
+ inherit: true,
4256
+ fw: 800,
4257
+ c: "gray.9",
4258
+ children: endOthersQuestion(othersCount)
4259
+ }), ' ', t.confirmEndBody]
4260
+ }), /*#__PURE__*/jsxRuntime.jsxs(core.Group, {
4261
+ justify: "flex-end",
4262
+ gap: 8,
4263
+ children: [/*#__PURE__*/jsxRuntime.jsx(ActionButton, {
4264
+ tone: "quiet",
4265
+ onClick: () => setIsConfirmingEnd(false),
4266
+ children: t.cancel
4267
+ }), /*#__PURE__*/jsxRuntime.jsx(ActionButton, {
4268
+ tone: "dangerFill",
4269
+ loading: loadingRevokeSession === 'all',
4270
+ onClick: handleEndOthers,
4271
+ children: endOthersConfirm(othersCount)
3748
4272
  })]
3749
4273
  })]
3750
- }), showSessions && /*#__PURE__*/jsxRuntime.jsxs(core.Box, {
3751
- mb: "md",
3752
- children: [/*#__PURE__*/jsxRuntime.jsx(SectionHeader, {
3753
- icon: iconsReact.IconShield,
3754
- sectionTitle: labels.securitySection || 'Security',
3755
- description: labels.securityDescription || 'Protect your account'
3756
- }), /*#__PURE__*/jsxRuntime.jsx(core.Stack, {
3757
- gap: "sm",
3758
- children: showSessions && /*#__PURE__*/jsxRuntime.jsxs(jsxRuntime.Fragment, {
3759
- children: [/*#__PURE__*/jsxRuntime.jsx(SettingRow, {
3760
- label: labels.sessions || 'Sessions',
3761
- action: editingSection === 'sessions' ? labels.close || 'Close' : labels.manage || 'Manage',
3762
- actionLabel: labels.manageSessions || 'Manage your active sessions',
3763
- onClick: () => handleToggleSection('sessions'),
3764
- expanded: editingSection === 'sessions',
3765
- children: /*#__PURE__*/jsxRuntime.jsxs(core.Group, {
3766
- gap: "xs",
3767
- children: [/*#__PURE__*/jsxRuntime.jsx(iconsReact.IconDevices, {
3768
- size: 18,
3769
- stroke: 1.5,
3770
- color: "var(--mantine-color-dimmed)"
3771
- }), /*#__PURE__*/jsxRuntime.jsx(core.Text, {
3772
- size: "sm",
3773
- c: "dimmed",
3774
- children: sessions.length > 0 ? `${sessions.length} active session${sessions.length > 1 ? 's' : ''}` : loadingListSessions ? 'Loading...' : 'No sessions'
3775
- })]
3776
- })
3777
- }), /*#__PURE__*/jsxRuntime.jsx(core.Collapse, {
3778
- in: editingSection === 'sessions',
3779
- children: /*#__PURE__*/jsxRuntime.jsx(core.Paper, {
3780
- p: "sm",
3781
- withBorder: true,
3782
- radius: "sm",
3783
- children: /*#__PURE__*/jsxRuntime.jsx(core.Stack, {
3784
- gap: "xs",
3785
- children: loadingListSessions ? /*#__PURE__*/jsxRuntime.jsx(core.Text, {
3786
- size: "xs",
3787
- c: "dimmed",
3788
- ta: "center",
3789
- py: "md",
3790
- children: labels.loadingSessions || 'Carregando sessões...'
3791
- }) : sessions.length === 0 ? /*#__PURE__*/jsxRuntime.jsx(core.Text, {
3792
- size: "xs",
3793
- c: "dimmed",
3794
- ta: "center",
3795
- py: "md",
3796
- children: labels.noSessionsFound || 'Nenhuma sessão encontrada'
3797
- }) : /*#__PURE__*/jsxRuntime.jsxs(jsxRuntime.Fragment, {
3798
- children: [sessions.map(sessionItem => {
3799
- const isCurrentSession = sessionItem.id === currentSession?.id;
3800
- const deviceInfo = parseUserAgent(sessionItem.userAgent);
3801
- const createdDate = new Date(sessionItem.createdAt);
3802
- return /*#__PURE__*/jsxRuntime.jsx(core.Paper, {
3803
- p: "xs",
3804
- withBorder: isCurrentSession,
3805
- bd: isCurrentSession ? '1px solid gray' : undefined,
3806
- radius: "sm",
3807
- children: /*#__PURE__*/jsxRuntime.jsxs(core.Group, {
3808
- justify: "space-between",
3809
- wrap: "nowrap",
3810
- align: "center",
3811
- children: [/*#__PURE__*/jsxRuntime.jsxs(core.Group, {
3812
- gap: "sm",
3813
- wrap: "nowrap",
3814
- flex: 1,
3815
- children: [/*#__PURE__*/jsxRuntime.jsx(core.ThemeIcon, {
3816
- size: 32,
3817
- variant: "subtle",
3818
- color: "gray",
3819
- children: /*#__PURE__*/jsxRuntime.jsx(iconsReact.IconDeviceMobile, {
3820
- size: 18,
3821
- stroke: 1.5
3822
- })
3823
- }), /*#__PURE__*/jsxRuntime.jsxs(core.Box, {
3824
- flex: 1,
3825
- children: [/*#__PURE__*/jsxRuntime.jsxs(core.Group, {
3826
- gap: "xs",
3827
- children: [/*#__PURE__*/jsxRuntime.jsx(core.Text, {
3828
- size: "xs",
3829
- fw: 600,
3830
- children: deviceInfo.browser
3831
- }), isCurrentSession && /*#__PURE__*/jsxRuntime.jsx(core.Badge, {
3832
- size: "xs",
3833
- variant: "light",
3834
- color: "gray",
3835
- children: labels.thisDevice || 'Este dispositivo'
3836
- })]
3837
- }), /*#__PURE__*/jsxRuntime.jsxs(core.Text, {
3838
- size: "xs",
3839
- c: "dimmed",
3840
- children: [deviceInfo.os, " \u2022 ", sessionItem.ipAddress || labels.unknownIP || 'IP desconhecido']
3841
- }), /*#__PURE__*/jsxRuntime.jsxs(core.Text, {
3842
- size: "xs",
3843
- c: "dimmed",
3844
- children: [labels.createdAt || 'Criada em', " ", createdDate.toLocaleDateString('pt-BR'), " ", labels.at || 'às', ' ', createdDate.toLocaleTimeString('pt-BR', {
3845
- hour: '2-digit',
3846
- minute: '2-digit'
3847
- })]
3848
- })]
3849
- })]
3850
- }), /*#__PURE__*/jsxRuntime.jsx(core.Tooltip, {
3851
- label: isCurrentSession ? labels.signOutAndEnd || 'Encerrar e sair' : labels.endSession || 'Encerrar sessão',
3852
- children: /*#__PURE__*/jsxRuntime.jsx(core.Button, {
3853
- variant: "subtle",
3854
- color: "gray",
3855
- size: "xs",
3856
- onClick: () => handleRevokeSession(sessionItem.id),
3857
- loading: loadingRevokeSession === sessionItem.id,
3858
- loaderProps: {
3859
- size: 12
3860
- },
3861
- leftSection: /*#__PURE__*/jsxRuntime.jsx(iconsReact.IconLogout, {
3862
- size: 14,
3863
- stroke: 1.5
3864
- }),
3865
- children: labels.end || 'Encerrar'
3866
- })
3867
- })]
3868
- })
3869
- }, sessionItem.id);
3870
- }), sessions.length > 1 && /*#__PURE__*/jsxRuntime.jsx(core.Group, {
3871
- justify: "flex-end",
3872
- mt: "xs",
3873
- children: /*#__PURE__*/jsxRuntime.jsx(core.Button, {
3874
- variant: "subtle",
3875
- color: "gray",
3876
- size: "xs",
3877
- onClick: handleRevokeOtherSessions,
3878
- loading: loadingRevokeSession === 'all',
3879
- loaderProps: {
3880
- size: 12
3881
- },
3882
- leftSection: /*#__PURE__*/jsxRuntime.jsx(iconsReact.IconLogout, {
3883
- size: 14,
3884
- stroke: 1.5
3885
- }),
3886
- children: labels.endOtherSessions || 'Encerrar todas as outras sessões'
3887
- })
3888
- })]
3889
- })
3890
- })
3891
- })
3892
- })]
4274
+ }) : /*#__PURE__*/jsxRuntime.jsx(core.Group, {
4275
+ justify: "flex-end",
4276
+ pt: 4,
4277
+ pb: 4,
4278
+ children: /*#__PURE__*/jsxRuntime.jsx(ActionButton, {
4279
+ tone: "dangerOutline",
4280
+ onClick: () => setIsConfirmingEnd(true),
4281
+ children: endOthersAction(othersCount)
4282
+ })
4283
+ }))]
4284
+ });
4285
+ const content = /*#__PURE__*/jsxRuntime.jsxs(jsxRuntime.Fragment, {
4286
+ children: [header, identityStrip, hasProfile && /*#__PURE__*/jsxRuntime.jsxs(Section, {
4287
+ label: t.profileSection,
4288
+ isFirst: true,
4289
+ children: [showAvatar && (editing === 'avatar' ? avatarEditor : /*#__PURE__*/jsxRuntime.jsx(Row$1, {
4290
+ label: t.avatar,
4291
+ isFirst: true,
4292
+ action: /*#__PURE__*/jsxRuntime.jsx(ActionButton, {
4293
+ onClick: () => openEditor('avatar'),
4294
+ children: t.edit
4295
+ }),
4296
+ children: /*#__PURE__*/jsxRuntime.jsx(SquareAvatar, {
4297
+ src: identity.image,
4298
+ initials: identity.initials,
4299
+ size: 32
3893
4300
  })
4301
+ })), showName && (editing === 'name' ? nameEditor : /*#__PURE__*/jsxRuntime.jsx(Row$1, {
4302
+ label: t.name,
4303
+ isFirst: !showAvatar,
4304
+ action: /*#__PURE__*/jsxRuntime.jsx(ActionButton, {
4305
+ onClick: () => openEditor('name'),
4306
+ children: t.edit
4307
+ }),
4308
+ children: identity.name || /*#__PURE__*/jsxRuntime.jsx(core.Text, {
4309
+ span: true,
4310
+ inherit: true,
4311
+ c: "gray.5",
4312
+ children: t.notDefined
4313
+ })
4314
+ })), showEmail && /*#__PURE__*/jsxRuntime.jsxs(Row$1, {
4315
+ label: t.email,
4316
+ isFirst: !showAvatar && !showName,
4317
+ children: [identity.email, /*#__PURE__*/jsxRuntime.jsx(Hint, {
4318
+ children: t.emailHint
4319
+ })]
4320
+ })]
4321
+ }), hasSignInMethods && /*#__PURE__*/jsxRuntime.jsxs(Section, {
4322
+ label: t.signInSection,
4323
+ isFirst: !hasProfile,
4324
+ children: [/*#__PURE__*/jsxRuntime.jsx(Row$1, {
4325
+ label: t.codeMethod,
4326
+ isFirst: true,
4327
+ action: /*#__PURE__*/jsxRuntime.jsx(Chip, {
4328
+ tone: "good",
4329
+ children: t.alwaysOn
4330
+ }),
4331
+ children: /*#__PURE__*/jsxRuntime.jsx(WithIcon, {
4332
+ icon: iconsReact.IconMail,
4333
+ children: t.codeByEmail
4334
+ })
4335
+ }), providers.map(item => {
4336
+ const link = linked.find(entry => entry.provider === item.provider);
4337
+ return /*#__PURE__*/jsxRuntime.jsx(Row$1, {
4338
+ label: item.name,
4339
+ action: link ? /*#__PURE__*/jsxRuntime.jsx(ActionButton, {
4340
+ tone: "quiet",
4341
+ loading: pendingProvider === item.provider,
4342
+ onClick: () => handleUnlink(item.provider),
4343
+ "aria-label": `${t.disconnect} ${item.name}`,
4344
+ children: t.disconnect
4345
+ }) : /*#__PURE__*/jsxRuntime.jsx(ActionButton, {
4346
+ loading: pendingProvider === item.provider,
4347
+ onClick: () => handleLink(item.provider),
4348
+ "aria-label": `${t.connect} ${item.name}`,
4349
+ children: t.connect
4350
+ }),
4351
+ children: /*#__PURE__*/jsxRuntime.jsx(WithIcon, {
4352
+ icon: PROVIDER_MARKS[item.provider] || iconsReact.IconLink,
4353
+ children: link ? link.email || item.name : /*#__PURE__*/jsxRuntime.jsx(core.Text, {
4354
+ span: true,
4355
+ inherit: true,
4356
+ c: "gray.5",
4357
+ children: t.notConnected
4358
+ })
4359
+ })
4360
+ }, item.provider);
4361
+ }), /*#__PURE__*/jsxRuntime.jsx(FailureNote, {
4362
+ failure: failure,
4363
+ section: "signIn"
3894
4364
  })]
4365
+ }), showSessions && /*#__PURE__*/jsxRuntime.jsxs(Section, {
4366
+ label: t.sessionsSection,
4367
+ isFirst: !hasProfile && !hasSignInMethods,
4368
+ children: [/*#__PURE__*/jsxRuntime.jsx(core.UnstyledButton, {
4369
+ "aria-expanded": areSessionsOpen,
4370
+ "aria-controls": sessionsListId,
4371
+ onClick: () => {
4372
+ setAreSessionsOpen(isOpen => !isOpen);
4373
+ setIsConfirmingEnd(false);
4374
+ },
4375
+ onMouseEnter: () => setIsSessionsRowActive(true),
4376
+ onMouseLeave: () => setIsSessionsRowActive(false),
4377
+ onFocus: () => setIsSessionsRowActive(true),
4378
+ onBlur: () => setIsSessionsRowActive(false),
4379
+ w: "100%",
4380
+ style: {
4381
+ display: 'block',
4382
+ borderRadius: 0
4383
+ },
4384
+ children: /*#__PURE__*/jsxRuntime.jsx(Row$1, {
4385
+ label: t.devices,
4386
+ isFirst: true,
4387
+ action: /*#__PURE__*/jsxRuntime.jsxs(core.Box, {
4388
+ component: "span",
4389
+ style: {
4390
+ display: 'inline-flex',
4391
+ alignItems: 'center',
4392
+ gap: 4,
4393
+ padding: '5px 12px',
4394
+ fontSize: 12,
4395
+ fontWeight: 700,
4396
+ color: 'var(--mantine-color-gray-9)',
4397
+ // The whole line is the button; the pill only shows it.
4398
+ border: `1px solid var(--mantine-color-${isSessionsRowActive ? 'gray-9' : 'gray-3'})`,
4399
+ whiteSpace: 'nowrap'
4400
+ },
4401
+ children: [areSessionsOpen ? t.hideSessions : t.showSessions, /*#__PURE__*/jsxRuntime.jsx(iconsReact.IconChevronDown, {
4402
+ size: 14,
4403
+ stroke: 1.5,
4404
+ style: {
4405
+ transform: areSessionsOpen ? 'rotate(180deg)' : 'none',
4406
+ transition: 'transform 150ms ease'
4407
+ }
4408
+ })]
4409
+ }),
4410
+ children: /*#__PURE__*/jsxRuntime.jsx(WithIcon, {
4411
+ icon: iconsReact.IconDeviceLaptop,
4412
+ detail: sessionsSummary,
4413
+ children: loadingListSessions && ordered.length === 0 ? t.loadingSessions : countSessions(ordered.length)
4414
+ })
4415
+ })
4416
+ }), areSessionsOpen && sessionsList]
3895
4417
  }), customSections]
3896
4418
  });
3897
-
3898
- // Renderizar como Modal
3899
4419
  if (variant === 'modal') {
3900
- return /*#__PURE__*/jsxRuntime.jsxs(core.Modal, {
3901
- opened: opened,
4420
+ return /*#__PURE__*/jsxRuntime.jsxs(core.Modal.Root, {
4421
+ opened: Boolean(opened),
3902
4422
  onClose: onClose,
3903
- size: width,
3904
- withCloseButton: true,
3905
- radius: "md",
3906
- overlayProps: {
4423
+ size: width
4424
+ // With an editor open, Esc belongs to the editor: it cancels the
4425
+ // edit and leaves the screen open. Mantine listens for Esc on its
4426
+ // own, so stopping the key in the field is not enough.
4427
+ ,
4428
+ closeOnEscape: !editing && !isConfirmingEnd,
4429
+ ...containerProps,
4430
+ children: [/*#__PURE__*/jsxRuntime.jsx(core.Modal.Overlay, {
3907
4431
  backgroundOpacity: 0.5,
3908
4432
  blur: 4
3909
- },
3910
- title: /*#__PURE__*/jsxRuntime.jsxs(core.Group, {
3911
- gap: "sm",
3912
- children: [/*#__PURE__*/jsxRuntime.jsx(core.ThemeIcon, {
3913
- size: 32,
3914
- variant: "subtle",
3915
- color: "gray",
3916
- children: /*#__PURE__*/jsxRuntime.jsx(iconsReact.IconUserCircle, {
3917
- size: 24,
3918
- stroke: 1.5
3919
- })
3920
- }), /*#__PURE__*/jsxRuntime.jsxs(core.Stack, {
3921
- gap: 0,
3922
- children: [/*#__PURE__*/jsxRuntime.jsx(core.Title, {
3923
- order: 5,
3924
- fw: 600,
3925
- children: title
3926
- }), /*#__PURE__*/jsxRuntime.jsx(core.Text, {
3927
- size: "xs",
3928
- c: "dimmed",
3929
- children: subtitle
3930
- })]
3931
- })]
3932
- }),
3933
- ...containerProps,
3934
- children: [/*#__PURE__*/jsxRuntime.jsx(core.Divider, {
3935
- mb: "md"
3936
- }), profileContent]
4433
+ }), /*#__PURE__*/jsxRuntime.jsx(core.Modal.Content, {
4434
+ radius: 0,
4435
+ style: {
4436
+ border: '1px solid var(--mantine-color-gray-3)'
4437
+ },
4438
+ children: /*#__PURE__*/jsxRuntime.jsx(core.Modal.Body, {
4439
+ p: 0,
4440
+ children: content
4441
+ })
4442
+ })]
3937
4443
  });
3938
4444
  }
3939
-
3940
- // Renderizar como Card
3941
4445
  return /*#__PURE__*/jsxRuntime.jsx(core.Paper, {
3942
- withBorder: true
3943
- // shadow="md"
3944
- ,
3945
- p: "md",
4446
+ withBorder: true,
4447
+ radius: 0,
4448
+ p: 0,
3946
4449
  w: width,
3947
- radius: "md",
4450
+ maw: "100%",
3948
4451
  ...containerProps,
3949
- children: /*#__PURE__*/jsxRuntime.jsxs(core.Stack, {
3950
- gap: "sm",
3951
- children: [/*#__PURE__*/jsxRuntime.jsxs(core.Group, {
3952
- gap: "sm",
3953
- children: [logo ? /*#__PURE__*/jsxRuntime.jsx(core.Image, {
3954
- src: logo,
3955
- alt: "Auth",
3956
- h: logoHeight,
3957
- fit: "contain"
3958
- }) : /*#__PURE__*/jsxRuntime.jsx(core.ThemeIcon, {
3959
- size: 32,
3960
- variant: "subtle",
3961
- color: "gray",
3962
- children: /*#__PURE__*/jsxRuntime.jsx(iconsReact.IconUserCircle, {
3963
- size: 24,
3964
- stroke: 1.5
3965
- })
3966
- }), /*#__PURE__*/jsxRuntime.jsxs(core.Stack, {
3967
- gap: 0,
3968
- children: [/*#__PURE__*/jsxRuntime.jsx(core.Title, {
3969
- order: 5,
3970
- fw: 600,
3971
- children: title
3972
- }), /*#__PURE__*/jsxRuntime.jsx(core.Text, {
3973
- size: "xs",
3974
- c: "dimmed",
3975
- children: subtitle
3976
- })]
3977
- })]
3978
- }), /*#__PURE__*/jsxRuntime.jsx(core.Divider, {}), profileContent]
3979
- })
4452
+ children: content
3980
4453
  });
3981
4454
  }
3982
4455
 
4456
+ /* The only texts that change with the count, so they are functions, not labels. */
4457
+ function endOthersAction(count) {
4458
+ return count === 1 ? 'Encerrar a outra' : `Encerrar as outras ${count}`;
4459
+ }
4460
+ function endOthersQuestion(count) {
4461
+ return count === 1 ? 'Encerrar 1 sessão?' : `Encerrar ${count} sessões?`;
4462
+ }
4463
+ function endOthersConfirm(count) {
4464
+ return count === 1 ? 'Encerrar 1 sessão' : `Encerrar ${count} sessões`;
4465
+ }
4466
+
3983
4467
  const ROLE_LABELS = {
3984
4468
  owner: 'Dono',
3985
4469
  admin: 'Administrador',
@@ -4056,17 +4540,13 @@ function UserInformation({
4056
4540
  ...others
4057
4541
  }) {
4058
4542
  if (!user) return null;
4059
- const name = (user.fullName || user.name || '').trim();
4060
- const email = (user.primaryEmailAddress || user.email || '').trim();
4061
-
4062
- /*
4063
- * A name equal to the email's local part is not a name: it is what an
4064
- * account born from a code gets by default, and showing it on top of the
4065
- * email repeats the same fact. It counts as "no name".
4066
- */
4067
- const hasRealName = Boolean(name) && name.toLowerCase() !== email.split('@')[0].toLowerCase() && name.toLowerCase() !== email.toLowerCase();
4068
- const title = hasRealName ? name : email || name;
4069
- const initials = hasRealName ? name.split(/\s+/).map(part => part[0]).join('').slice(0, 2).toUpperCase() : (email || name).charAt(0).toUpperCase();
4543
+ const {
4544
+ email,
4545
+ hasRealName,
4546
+ title,
4547
+ initials,
4548
+ image
4549
+ } = describeUser(user);
4070
4550
  const baseRows = [onAccountClick && {
4071
4551
  id: 'account',
4072
4552
  label: accountLabel,
@@ -4090,7 +4570,7 @@ function UserInformation({
4090
4570
  px: padded ? 16 : 0,
4091
4571
  py: 14,
4092
4572
  children: [/*#__PURE__*/jsxRuntime.jsx(core.Avatar, {
4093
- src: user.imageUrl || user.image,
4573
+ src: image,
4094
4574
  alt: "",
4095
4575
  size: 32,
4096
4576
  radius: 0,