@ciromaciel/auth-react 1.5.0 → 1.7.0

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