@ciromaciel/auth-react 1.2.0 → 1.4.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
@@ -112,6 +112,28 @@ function forgetAccount(email) {
112
112
  return next;
113
113
  }
114
114
 
115
+ /**
116
+ * Replaces the local copy with the list the worker returned.
117
+ *
118
+ * The worker's list is the one every panel shares, so when it has accounts it
119
+ * wins: an account removed on another panel must not come back from this
120
+ * panel's stale copy. An EMPTY answer does not wipe the local one — it is what
121
+ * a browser that signed in before the shared list existed gets, and those
122
+ * shortcuts are still true.
123
+ *
124
+ * @returns the list to show
125
+ */
126
+ function adoptRecentAccounts(remote) {
127
+ if (!Array.isArray(remote) || remote.length === 0) return listRecentAccounts();
128
+ const next = remote.filter(account => account && typeof account.email === 'string' && account.email.includes('@')).map(account => ({
129
+ email: normalizeEmail(account.email),
130
+ method: typeof account.method === 'string' && account.method ? account.method : 'code',
131
+ lastUsedAt: Number(account.lastUsedAt) || 0
132
+ })).sort((a, b) => b.lastUsedAt - a.lastUsedAt).slice(0, MAX_RECENT_ACCOUNTS);
133
+ writeRecentAccounts(next);
134
+ return next;
135
+ }
136
+
115
137
  /** Records the provider this tab is leaving for. */
116
138
  function markSocialDeparture(provider) {
117
139
  try {
@@ -693,6 +715,64 @@ const updateProfile = async data => {
693
715
  });
694
716
  };
695
717
 
718
+ /*--- Recent accounts ------------------------------------------------------*/
719
+
720
+ /*
721
+ * The server's copy of this browser's sign-in shortcuts.
722
+ *
723
+ * `localStorage` is per origin, so a list kept only there stayed on the panel
724
+ * where the person signed in. The worker keeps it in an HttpOnly cookie on its
725
+ * own host, which every panel of the application reaches — and answers only to
726
+ * the origins the application allows (`routes/recent-accounts.js`).
727
+ *
728
+ * All three fail soft: the shortcuts are a convenience, and a network error or
729
+ * an origin the worker refuses must never cost the sign-in. `null` means "no
730
+ * answer", which the screen reads as "keep what you have".
731
+ */
732
+
733
+ /** The list for this application, or `null` when the worker did not answer. */
734
+ const fetchRecentAccounts = async () => {
735
+ try {
736
+ const response = await api('/auth/recent-accounts');
737
+ return Array.isArray(response?.items) ? response.items : null;
738
+ } catch {
739
+ return null;
740
+ }
741
+ };
742
+
743
+ /**
744
+ * Records the account of the CURRENT session. The worker reads the email from
745
+ * the session, never from here. `keepalive` because the screen is usually
746
+ * navigating away at this very moment.
747
+ */
748
+ const saveRecentAccount = async (method = 'code') => {
749
+ try {
750
+ const response = await api('/auth/recent-accounts', {
751
+ method: 'POST',
752
+ body: JSON.stringify({
753
+ method
754
+ }),
755
+ keepalive: true
756
+ });
757
+ return Array.isArray(response?.items) ? response.items : null;
758
+ } catch {
759
+ return null;
760
+ }
761
+ };
762
+
763
+ /** Takes one account off this browser's list, on every panel. */
764
+ const deleteRecentAccount = async email => {
765
+ try {
766
+ await api(`/auth/recent-accounts/${encodeURIComponent(email)}`, {
767
+ method: 'DELETE',
768
+ keepalive: true
769
+ });
770
+ return true;
771
+ } catch {
772
+ return false;
773
+ }
774
+ };
775
+
696
776
  /*--- Social sign-in -------------------------------------------------------*/
697
777
 
698
778
  /**
@@ -801,6 +881,9 @@ const consumeSocialToken = () => {
801
881
  if (provider) {
802
882
  const email = decodeJWT(token)?.email;
803
883
  if (email) rememberAccount(email, provider);
884
+ // The shared copy, so the other panels learn it too. Not awaited: the
885
+ // token is already stored, and the sign-in must not wait on a shortcut.
886
+ saveRecentAccount(provider);
804
887
  }
805
888
  params.delete('token');
806
889
  const rest = params.toString();
@@ -2784,6 +2867,32 @@ function SignIn({
2784
2867
  // eslint-disable-next-line react-hooks/exhaustive-deps -- redirectOrigins enters through the serialized key above
2785
2868
  }, [authLoading, user, authenticatedRedirect, handleRedirect, redirectOriginsKey, navigate]);
2786
2869
 
2870
+ /*
2871
+ * The shared list, from the worker.
2872
+ *
2873
+ * The local copy renders at once; this replaces it when the answer
2874
+ * arrives. That is what makes an account used on the Auth panel appear
2875
+ * on Hoster: `localStorage` never crosses between the two origins.
2876
+ *
2877
+ * If the person has already started typing an email, the list does not
2878
+ * yank the form away from under them — it only feeds the "Contas salvas"
2879
+ * link, one click away.
2880
+ */
2881
+ react.useEffect(() => {
2882
+ if (!recentAccounts) return;
2883
+ let isActive = true;
2884
+ fetchRecentAccounts().then(remote => {
2885
+ if (!isActive || remote === null) return;
2886
+ const next = adoptRecentAccounts(remote);
2887
+ if (form$1.isDirty()) setIsChoosingOther(true);
2888
+ setAccounts(next);
2889
+ });
2890
+ return () => {
2891
+ isActive = false;
2892
+ };
2893
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- once per mount, like the local read
2894
+ }, [recentAccounts]);
2895
+
2787
2896
  // Step 1 — ask for the code.
2788
2897
  const handleRequest = async values => {
2789
2898
  if (sending) return false;
@@ -2843,6 +2952,9 @@ function SignIn({
2843
2952
  const handleForget = account => {
2844
2953
  const next = forgetAccount(account.email);
2845
2954
  setAccounts(next);
2955
+ // On every panel, not just this one. The local removal above already
2956
+ // took it off this screen, so a failure here costs nothing visible.
2957
+ deleteRecentAccount(account.email);
2846
2958
  if (next.length === 0) setIsManaging(false);
2847
2959
  };
2848
2960
 
@@ -2871,7 +2983,10 @@ function SignIn({
2871
2983
  // Only now, with a session: an email that never received a valid
2872
2984
  // code never becomes a suggestion. Written before the redirect,
2873
2985
  // which may unmount this screen.
2874
- if (recentAccounts) setAccounts(rememberAccount(sentTo, 'code'));
2986
+ if (recentAccounts) {
2987
+ setAccounts(rememberAccount(sentTo, 'code'));
2988
+ saveRecentAccount('code');
2989
+ }
2875
2990
  const target = handleRedirect ? getRedirectFromLocation(redirectOrigins) : null;
2876
2991
  if (target) applyRedirect(target, navigate);
2877
2992
  onSuccess?.(result?.user ?? null, {
@@ -3869,150 +3984,357 @@ function UserProfile({
3869
3984
  });
3870
3985
  }
3871
3986
 
3987
+ const ROLE_LABELS = {
3988
+ owner: 'Dono',
3989
+ admin: 'Administrador',
3990
+ member: 'Membro'
3991
+ };
3992
+
3993
+ /**
3994
+ * @typedef {Object} UserInformationItem
3995
+ * @property {string} [id] - Stable key
3996
+ * @property {string} label - The row's text
3997
+ * @property {Function} [icon] - A Tabler icon component
3998
+ * @property {Function} onClick
3999
+ *
4000
+ * @typedef {Object} UserInformationPlan
4001
+ * @property {string} [name] - "Pro", "Starter"… Shown as the badge
4002
+ * @property {number} [used] - How many of the plan's resources are in use
4003
+ * @property {number|null} [limit] - The plan's ceiling; `null` hides the meter
4004
+ * @property {string} [unit] - What is counted, in the plural: "projetos"
4005
+ * @property {string} [actionLabel='Ver planos']
4006
+ * @property {Function} [onClick] - Opens the plans
4007
+ *
4008
+ * @typedef {Object} UserInformationOrganization
4009
+ * @property {string} name
4010
+ * @property {string} [role] - `owner`, `admin`, `member`, or already a label
4011
+ */
4012
+
4013
+ /**
4014
+ * @param {Object} props
4015
+ * @param {Object} props.user - The signed-in user (`name`, `email`, `image`)
4016
+ * @param {Function} props.signOut
4017
+ * @param {Function} [props.onAccountClick]
4018
+ * @param {Function} [props.onBillingClick]
4019
+ * @param {UserInformationItem[]} [props.items] - The panel's own rows, shown in their own group
4020
+ * @param {UserInformationPlan} [props.plan] - The plan strip; omitted, the strip is not drawn
4021
+ * @param {UserInformationOrganization} [props.organization] - Where the person is acting
4022
+ * @param {boolean} [props.branded=true] - The "Protegido por Auth" line
4023
+ * @param {string} [props.accountLabel='Conta']
4024
+ * @param {string} [props.billingLabel='Assinatura']
4025
+ * @param {string} [props.signOutLabel='Sair']
4026
+ */
3872
4027
  function UserInformation({
3873
4028
  user,
3874
4029
  signOut,
3875
4030
  onAccountClick,
3876
4031
  onBillingClick,
4032
+ items = [],
4033
+ plan,
4034
+ organization,
4035
+ branded = true,
3877
4036
  accountLabel = 'Conta',
3878
4037
  billingLabel = 'Assinatura',
4038
+ signOutLabel = 'Sair',
4039
+ // Kept so existing calls keep working. The card has one density now: the
4040
+ // popover's. `padded={false}` still removes the outer padding.
3879
4041
  padded = true,
3880
- size = 'sm',
4042
+ size,
4043
+ // eslint-disable-line no-unused-vars -- accepted and ignored, see above
3881
4044
  style,
3882
4045
  ...others
3883
4046
  }) {
3884
4047
  if (!user) return null;
4048
+ const name = (user.fullName || user.name || '').trim();
4049
+ const email = (user.primaryEmailAddress || user.email || '').trim();
3885
4050
 
3886
- // Size mappings
3887
- const avatarSizeMap = {
3888
- sm: 32,
3889
- md: 40,
3890
- lg: 48
3891
- };
3892
- const fontSizeTitleMap = {
3893
- sm: 'sm',
3894
- md: 'md',
3895
- lg: 'lg'
3896
- };
3897
- const fontSizeEmailMap = {
3898
- sm: '10px',
3899
- md: 'xs',
3900
- lg: 'sm'
3901
- };
3902
- const btnSizeMap = {
3903
- sm: 'xs',
3904
- md: 'sm',
3905
- lg: 'md'
3906
- };
3907
- const gapMap = {
3908
- sm: 'xs',
3909
- md: 'sm',
3910
- lg: 'md'
3911
- };
3912
- const widthMap = {
3913
- sm: 280,
3914
- md: 320,
3915
- lg: 400
3916
- };
3917
- const name = user.fullName || user.name || 'User';
3918
- const email = user.primaryEmailAddress || user.email || '';
3919
- const initials = name.split(' ').map(n => n[0]).join('').toUpperCase().slice(0, 2);
3920
- return /*#__PURE__*/jsxRuntime.jsx(core.Box, {
3921
- p: padded ? gapMap[size] : 0,
3922
- w: widthMap[size],
4051
+ /*
4052
+ * A name equal to the email's local part is not a name: it is what an
4053
+ * account born from a code gets by default, and showing it on top of the
4054
+ * email repeats the same fact. It counts as "no name".
4055
+ */
4056
+ const hasRealName = Boolean(name) && name.toLowerCase() !== email.split('@')[0].toLowerCase() && name.toLowerCase() !== email.toLowerCase();
4057
+ const title = hasRealName ? name : email || name;
4058
+ const initials = hasRealName ? name.split(/\s+/).map(part => part[0]).join('').slice(0, 2).toUpperCase() : (email || name).charAt(0).toUpperCase();
4059
+ const baseRows = [onAccountClick && {
4060
+ id: 'account',
4061
+ label: accountLabel,
4062
+ icon: iconsReact.IconUser,
4063
+ onClick: onAccountClick
4064
+ }, onBillingClick && {
4065
+ id: 'billing',
4066
+ label: billingLabel,
4067
+ icon: iconsReact.IconCreditCard,
4068
+ onClick: onBillingClick
4069
+ }].filter(Boolean);
4070
+ const panelRows = items.filter(item => item && item.label && typeof item.onClick === 'function');
4071
+ return /*#__PURE__*/jsxRuntime.jsxs(core.Box, {
4072
+ w: 288,
4073
+ maw: "100%",
3923
4074
  style: style,
3924
4075
  ...others,
3925
- children: /*#__PURE__*/jsxRuntime.jsxs(core.Stack, {
3926
- gap: gapMap[size],
3927
- children: [/*#__PURE__*/jsxRuntime.jsxs(core.Group, {
3928
- wrap: "nowrap",
3929
- gap: "xs",
3930
- children: [/*#__PURE__*/jsxRuntime.jsx(core.Avatar, {
3931
- src: user.imageUrl || user.image,
3932
- size: avatarSizeMap[size],
3933
- radius: "xl",
3934
- bg: "gray.1",
3935
- c: "gray.6",
3936
- styles: {
3937
- placeholder: {
3938
- fontSize: core.rem(avatarSizeMap[size] / 2.2),
3939
- fontWeight: 600
3940
- }
4076
+ children: [/*#__PURE__*/jsxRuntime.jsxs(core.Group, {
4077
+ wrap: "nowrap",
4078
+ gap: 10,
4079
+ px: padded ? 16 : 0,
4080
+ py: 14,
4081
+ children: [/*#__PURE__*/jsxRuntime.jsx(core.Avatar, {
4082
+ src: user.imageUrl || user.image,
4083
+ alt: "",
4084
+ size: 32,
4085
+ radius: 0,
4086
+ color: "gray.9",
4087
+ variant: "filled",
4088
+ styles: {
4089
+ root: {
4090
+ borderRadius: 0
3941
4091
  },
3942
- children: initials || 'CC'
3943
- }), /*#__PURE__*/jsxRuntime.jsxs(core.Box, {
4092
+ placeholder: {
4093
+ fontSize: 12,
4094
+ fontWeight: 800
4095
+ }
4096
+ },
4097
+ children: initials
4098
+ }), /*#__PURE__*/jsxRuntime.jsxs(core.Box, {
4099
+ style: {
4100
+ flex: 1,
4101
+ minWidth: 0
4102
+ },
4103
+ children: [/*#__PURE__*/jsxRuntime.jsx(core.Text, {
4104
+ fz: 13,
4105
+ fw: 800,
4106
+ c: "gray.9",
4107
+ lh: 1.3,
4108
+ truncate: "end",
4109
+ children: title
4110
+ }), hasRealName && email && /*#__PURE__*/jsxRuntime.jsx(core.Text, {
4111
+ fz: 12,
4112
+ fw: 500,
4113
+ c: "gray.5",
4114
+ lh: 1.4,
4115
+ truncate: "end",
4116
+ children: email
4117
+ })]
4118
+ })]
4119
+ }), (organization?.name || plan) && /*#__PURE__*/jsxRuntime.jsx(ContextStrip, {
4120
+ organization: organization,
4121
+ plan: plan,
4122
+ padded: padded
4123
+ }), /*#__PURE__*/jsxRuntime.jsxs(core.Box, {
4124
+ role: "menu",
4125
+ "aria-label": "Conta",
4126
+ onKeyDown: moveFocus,
4127
+ children: [baseRows.length > 0 && /*#__PURE__*/jsxRuntime.jsx(RowGroup, {
4128
+ rows: baseRows,
4129
+ hasDivider: !organization?.name && !plan
4130
+ }), panelRows.length > 0 && /*#__PURE__*/jsxRuntime.jsx(RowGroup, {
4131
+ rows: panelRows,
4132
+ hasDivider: true
4133
+ }), signOut && /*#__PURE__*/jsxRuntime.jsx(RowGroup, {
4134
+ rows: [{
4135
+ id: 'sign-out',
4136
+ label: signOutLabel,
4137
+ icon: iconsReact.IconLogout,
4138
+ onClick: signOut
4139
+ }],
4140
+ hasDivider: true
4141
+ })]
4142
+ }), branded && /*#__PURE__*/jsxRuntime.jsxs(core.Group, {
4143
+ justify: "center",
4144
+ gap: 4,
4145
+ py: 8,
4146
+ bg: "gray.1",
4147
+ style: {
4148
+ borderTop: '1px solid var(--mantine-color-gray-2)'
4149
+ },
4150
+ children: [/*#__PURE__*/jsxRuntime.jsx(core.Text, {
4151
+ fz: 11,
4152
+ fw: 500,
4153
+ c: "gray.5",
4154
+ children: "Protegido por"
4155
+ }), /*#__PURE__*/jsxRuntime.jsx(core.Text, {
4156
+ fz: 11,
4157
+ fw: 800,
4158
+ c: "gray.7",
4159
+ children: "Auth"
4160
+ })]
4161
+ })]
4162
+ });
4163
+ }
4164
+
4165
+ /** The organization, the role and the plan's usage, on the alternate surface. */
4166
+ function ContextStrip({
4167
+ organization,
4168
+ plan,
4169
+ padded
4170
+ }) {
4171
+ const role = organization?.role ? ROLE_LABELS[organization.role] || organization.role : null;
4172
+ const hasMeter = plan && Number.isFinite(plan.used) && Number.isFinite(plan.limit) && plan.limit > 0;
4173
+ const ratio = hasMeter ? Math.min(1, Math.max(0, plan.used / plan.limit)) : 0;
4174
+ return /*#__PURE__*/jsxRuntime.jsxs(core.Stack, {
4175
+ gap: 8,
4176
+ px: padded ? 16 : 0,
4177
+ py: 12,
4178
+ bg: "gray.1",
4179
+ style: {
4180
+ borderTop: '1px solid var(--mantine-color-gray-2)',
4181
+ borderBottom: '1px solid var(--mantine-color-gray-2)'
4182
+ },
4183
+ children: [(organization?.name || plan?.name) && /*#__PURE__*/jsxRuntime.jsxs(core.Group, {
4184
+ gap: 8,
4185
+ wrap: "nowrap",
4186
+ children: [organization?.name && /*#__PURE__*/jsxRuntime.jsxs(jsxRuntime.Fragment, {
4187
+ children: [/*#__PURE__*/jsxRuntime.jsx(iconsReact.IconBuilding, {
4188
+ size: 14,
4189
+ stroke: 1.5,
4190
+ style: {
4191
+ flex: 'none',
4192
+ color: 'var(--mantine-color-gray-5)'
4193
+ }
4194
+ }), /*#__PURE__*/jsxRuntime.jsx(core.Text, {
4195
+ fz: 12,
4196
+ fw: 800,
4197
+ c: "gray.9",
4198
+ truncate: "end",
3944
4199
  style: {
3945
- flex: 1,
3946
- overflow: 'hidden'
4200
+ minWidth: 0
3947
4201
  },
3948
- children: [/*#__PURE__*/jsxRuntime.jsx(core.Text, {
3949
- size: fontSizeTitleMap[size],
3950
- fw: 700,
3951
- truncate: "end",
3952
- c: "dark.9",
3953
- lh: 1.1,
3954
- children: name
3955
- }), /*#__PURE__*/jsxRuntime.jsx(core.Text, {
3956
- size: fontSizeEmailMap[size],
3957
- c: "gray.5",
3958
- truncate: "end",
3959
- lh: 1.1,
3960
- children: email
3961
- })]
3962
- }), /*#__PURE__*/jsxRuntime.jsx(core.ActionIcon, {
3963
- size: btnSizeMap[size],
3964
- onClick: signOut,
3965
- children: /*#__PURE__*/jsxRuntime.jsx(iconsReact.IconLogout, {
3966
- size: 16,
3967
- stroke: 1.5
3968
- })
3969
- })]
3970
- }), /*#__PURE__*/jsxRuntime.jsxs(core.Group, {
3971
- grow: true,
3972
- children: [/*#__PURE__*/jsxRuntime.jsx(core.Button, {
3973
- variant: "default",
3974
- size: btnSizeMap[size],
3975
- leftSection: /*#__PURE__*/jsxRuntime.jsx(iconsReact.IconSettings, {
3976
- size: 16,
3977
- stroke: 1.5
3978
- }),
3979
- onClick: onAccountClick,
3980
- children: accountLabel
3981
- }), /*#__PURE__*/jsxRuntime.jsx(core.Button, {
3982
- variant: "default",
3983
- size: btnSizeMap[size],
3984
- leftSection: /*#__PURE__*/jsxRuntime.jsx(iconsReact.IconCreditCard, {
3985
- size: 16,
3986
- stroke: 1.5
3987
- }),
3988
- onClick: onBillingClick,
3989
- children: billingLabel
4202
+ children: organization.name
4203
+ }), role && /*#__PURE__*/jsxRuntime.jsxs(core.Text, {
4204
+ fz: 12,
4205
+ fw: 500,
4206
+ c: "gray.5",
4207
+ style: {
4208
+ flex: 'none'
4209
+ },
4210
+ children: ["\xB7 ", role]
3990
4211
  })]
4212
+ }), plan?.name && /*#__PURE__*/jsxRuntime.jsx(core.Text, {
4213
+ component: "span",
4214
+ fz: 10,
4215
+ fw: 800,
4216
+ tt: "uppercase",
4217
+ lts: "1.5px",
4218
+ c: "white",
4219
+ bg: "gray.9",
4220
+ px: 6,
4221
+ lh: 1.6,
4222
+ ml: "auto",
4223
+ style: {
4224
+ flex: 'none'
4225
+ },
4226
+ children: plan.name
4227
+ })]
4228
+ }), hasMeter && /*#__PURE__*/jsxRuntime.jsxs(jsxRuntime.Fragment, {
4229
+ children: [/*#__PURE__*/jsxRuntime.jsx(core.Box, {
4230
+ h: 4,
4231
+ bg: "gray.2",
4232
+ role: "meter",
4233
+ "aria-valuemin": 0,
4234
+ "aria-valuemax": plan.limit,
4235
+ "aria-valuenow": plan.used,
4236
+ "aria-label": plan.unit ? `${plan.unit} em uso` : 'Uso do plano',
4237
+ children: /*#__PURE__*/jsxRuntime.jsx(core.Box, {
4238
+ h: "100%",
4239
+ w: `${ratio * 100}%`,
4240
+ bg: "gray.9"
4241
+ })
3991
4242
  }), /*#__PURE__*/jsxRuntime.jsxs(core.Group, {
3992
- justify: "center",
3993
- gap: 4,
3994
- opacity: 0.3,
4243
+ justify: "space-between",
4244
+ gap: 8,
4245
+ wrap: "nowrap",
3995
4246
  children: [/*#__PURE__*/jsxRuntime.jsx(core.Text, {
3996
- size: "10px",
4247
+ fz: 12,
4248
+ fw: 500,
3997
4249
  c: "gray.6",
3998
- fw: 600,
3999
- children: "Secured by"
4000
- }), /*#__PURE__*/jsxRuntime.jsxs(core.Group, {
4001
- gap: 2,
4002
- children: [/*#__PURE__*/jsxRuntime.jsx(iconsReact.IconShieldCheck, {
4003
- size: 10,
4004
- stroke: 2
4005
- }), /*#__PURE__*/jsxRuntime.jsx(core.Text, {
4006
- size: "10px",
4007
- fw: 800,
4008
- c: "dark.9",
4009
- children: "Auth"
4010
- })]
4250
+ children: `${plan.used} de ${plan.limit}${plan.unit ? ` ${plan.unit}` : ''}`
4251
+ }), plan.onClick && /*#__PURE__*/jsxRuntime.jsx(core.Anchor, {
4252
+ component: "button",
4253
+ type: "button",
4254
+ fz: 12,
4255
+ fw: 700,
4256
+ c: "gray.9",
4257
+ underline: "always",
4258
+ onClick: plan.onClick,
4259
+ children: plan.actionLabel || 'Ver planos'
4011
4260
  })]
4012
4261
  })]
4013
- })
4262
+ })]
4263
+ });
4264
+ }
4265
+ function RowGroup({
4266
+ rows,
4267
+ hasDivider
4268
+ }) {
4269
+ return /*#__PURE__*/jsxRuntime.jsx(core.Stack, {
4270
+ gap: 0,
4271
+ p: 6,
4272
+ style: hasDivider ? {
4273
+ borderTop: '1px solid var(--mantine-color-gray-2)'
4274
+ } : undefined,
4275
+ children: rows.map(row => /*#__PURE__*/jsxRuntime.jsx(Row, {
4276
+ ...row
4277
+ }, row.id || row.label))
4014
4278
  });
4015
4279
  }
4280
+ function Row({
4281
+ label,
4282
+ icon: Icon,
4283
+ onClick
4284
+ }) {
4285
+ return /*#__PURE__*/jsxRuntime.jsxs(core.UnstyledButton, {
4286
+ role: "menuitem",
4287
+ onClick: onClick,
4288
+ px: 10,
4289
+ py: 7,
4290
+ w: "100%",
4291
+ style: {
4292
+ display: 'flex',
4293
+ alignItems: 'center',
4294
+ gap: 10,
4295
+ borderRadius: 0
4296
+ }
4297
+ /*
4298
+ * Hover and keyboard focus share the same surface: a row reached
4299
+ * with the arrows must look exactly like the one under the mouse.
4300
+ */,
4301
+ onMouseEnter: event => event.currentTarget.style.background = 'var(--mantine-color-gray-1)',
4302
+ onMouseLeave: event => event.currentTarget.style.background = 'transparent',
4303
+ onFocus: event => event.currentTarget.style.background = 'var(--mantine-color-gray-1)',
4304
+ onBlur: event => event.currentTarget.style.background = 'transparent',
4305
+ children: [Icon && /*#__PURE__*/jsxRuntime.jsx(Icon, {
4306
+ size: 16,
4307
+ stroke: 1.5,
4308
+ style: {
4309
+ flex: 'none',
4310
+ color: 'var(--mantine-color-gray-5)'
4311
+ }
4312
+ }), /*#__PURE__*/jsxRuntime.jsx(core.Text, {
4313
+ fz: 12,
4314
+ fw: 500,
4315
+ c: "gray.9",
4316
+ truncate: "end",
4317
+ children: label
4318
+ })]
4319
+ });
4320
+ }
4321
+
4322
+ /*
4323
+ * Arrow keys walk the rows, Home and End jump to the ends — the WAI-ARIA menu
4324
+ * pattern. Tab still leaves the card, so it never traps focus inside a popover
4325
+ * the panel owns.
4326
+ */
4327
+ function moveFocus(event) {
4328
+ const keys = ['ArrowDown', 'ArrowUp', 'Home', 'End'];
4329
+ if (!keys.includes(event.key)) return;
4330
+ const rows = Array.from(event.currentTarget.querySelectorAll('[role="menuitem"]'));
4331
+ if (rows.length === 0) return;
4332
+ event.preventDefault();
4333
+ const current = rows.indexOf(document.activeElement);
4334
+ const last = rows.length - 1;
4335
+ const next = event.key === 'Home' ? 0 : event.key === 'End' ? last : event.key === 'ArrowDown' ? current < last ? current + 1 : 0 : current > 0 ? current - 1 : last;
4336
+ rows[next].focus();
4337
+ }
4016
4338
 
4017
4339
  /**
4018
4340
  * Renderiza children apenas quando o usuário está autenticado
@@ -4121,6 +4443,7 @@ exports.TOKEN_STORAGE_KEY = TOKEN_STORAGE_KEY;
4121
4443
  exports.UserInformation = UserInformation;
4122
4444
  exports.UserProfile = UserProfile;
4123
4445
  exports.Wordmark = Wordmark;
4446
+ exports.adoptRecentAccounts = adoptRecentAccounts;
4124
4447
  exports.announceIdentityChange = announceIdentityChange;
4125
4448
  exports.applyRedirect = applyRedirect;
4126
4449
  exports.clearIdentitySwitching = clearIdentitySwitching;
@@ -4128,7 +4451,9 @@ exports.configure = configure;
4128
4451
  exports.consumeSocialError = consumeSocialError;
4129
4452
  exports.consumeSocialToken = consumeSocialToken;
4130
4453
  exports.decodeJWT = decodeJWT;
4454
+ exports.deleteRecentAccount = deleteRecentAccount;
4131
4455
  exports.endImpersonation = endImpersonation;
4456
+ exports.fetchRecentAccounts = fetchRecentAccounts;
4132
4457
  exports.forgetAccount = forgetAccount;
4133
4458
  exports.getApiUrl = getApiUrl;
4134
4459
  exports.getApplicationInfo = getApplicationInfo;
@@ -4150,6 +4475,7 @@ exports.requestCode = requestCode;
4150
4475
  exports.resolveRedirect = resolveRedirect;
4151
4476
  exports.revokeOtherSessions = revokeOtherSessions;
4152
4477
  exports.revokeSession = revokeSession;
4478
+ exports.saveRecentAccount = saveRecentAccount;
4153
4479
  exports.setStoredToken = setStoredToken;
4154
4480
  exports.shouldSignOutOn401 = shouldSignOutOn401;
4155
4481
  exports.signOut = signOut;