@ciromaciel/auth-react 1.3.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
@@ -3984,151 +3984,358 @@ function UserProfile({
3984
3984
  });
3985
3985
  }
3986
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
+ */
3987
4027
  function UserInformation({
3988
4028
  user,
3989
4029
  signOut,
3990
4030
  onAccountClick,
3991
4031
  onBillingClick,
4032
+ items = [],
4033
+ plan,
4034
+ organization,
4035
+ branded = true,
3992
4036
  accountLabel = 'Conta',
3993
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.
3994
4041
  padded = true,
3995
- size = 'sm',
4042
+ size,
4043
+ // eslint-disable-line no-unused-vars -- accepted and ignored, see above
3996
4044
  style,
3997
4045
  ...others
3998
4046
  }) {
3999
4047
  if (!user) return null;
4048
+ const name = (user.fullName || user.name || '').trim();
4049
+ const email = (user.primaryEmailAddress || user.email || '').trim();
4000
4050
 
4001
- // Size mappings
4002
- const avatarSizeMap = {
4003
- sm: 32,
4004
- md: 40,
4005
- lg: 48
4006
- };
4007
- const fontSizeTitleMap = {
4008
- sm: 'sm',
4009
- md: 'md',
4010
- lg: 'lg'
4011
- };
4012
- const fontSizeEmailMap = {
4013
- sm: '10px',
4014
- md: 'xs',
4015
- lg: 'sm'
4016
- };
4017
- const btnSizeMap = {
4018
- sm: 'xs',
4019
- md: 'sm',
4020
- lg: 'md'
4021
- };
4022
- const gapMap = {
4023
- sm: 'xs',
4024
- md: 'sm',
4025
- lg: 'md'
4026
- };
4027
- const widthMap = {
4028
- sm: 280,
4029
- md: 320,
4030
- lg: 400
4031
- };
4032
- const name = user.fullName || user.name || 'User';
4033
- const email = user.primaryEmailAddress || user.email || '';
4034
- const initials = name.split(' ').map(n => n[0]).join('').toUpperCase().slice(0, 2);
4035
- return /*#__PURE__*/jsxRuntime.jsx(core.Box, {
4036
- p: padded ? gapMap[size] : 0,
4037
- 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%",
4038
4074
  style: style,
4039
4075
  ...others,
4040
- children: /*#__PURE__*/jsxRuntime.jsxs(core.Stack, {
4041
- gap: gapMap[size],
4042
- children: [/*#__PURE__*/jsxRuntime.jsxs(core.Group, {
4043
- wrap: "nowrap",
4044
- gap: "xs",
4045
- children: [/*#__PURE__*/jsxRuntime.jsx(core.Avatar, {
4046
- src: user.imageUrl || user.image,
4047
- size: avatarSizeMap[size],
4048
- radius: "xl",
4049
- bg: "gray.1",
4050
- c: "gray.6",
4051
- styles: {
4052
- placeholder: {
4053
- fontSize: core.rem(avatarSizeMap[size] / 2.2),
4054
- fontWeight: 600
4055
- }
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
4056
4091
  },
4057
- children: initials || 'CC'
4058
- }), /*#__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,
4059
4190
  style: {
4060
- flex: 1,
4061
- overflow: 'hidden'
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",
4199
+ style: {
4200
+ minWidth: 0
4062
4201
  },
4063
- children: [/*#__PURE__*/jsxRuntime.jsx(core.Text, {
4064
- size: fontSizeTitleMap[size],
4065
- fw: 700,
4066
- truncate: "end",
4067
- c: "dark.9",
4068
- lh: 1.1,
4069
- children: name
4070
- }), /*#__PURE__*/jsxRuntime.jsx(core.Text, {
4071
- size: fontSizeEmailMap[size],
4072
- c: "gray.5",
4073
- truncate: "end",
4074
- lh: 1.1,
4075
- children: email
4076
- })]
4077
- }), /*#__PURE__*/jsxRuntime.jsx(core.ActionIcon, {
4078
- size: btnSizeMap[size],
4079
- onClick: signOut,
4080
- children: /*#__PURE__*/jsxRuntime.jsx(iconsReact.IconLogout, {
4081
- size: 16,
4082
- stroke: 1.5
4083
- })
4084
- })]
4085
- }), /*#__PURE__*/jsxRuntime.jsxs(core.Group, {
4086
- grow: true,
4087
- children: [/*#__PURE__*/jsxRuntime.jsx(core.Button, {
4088
- variant: "default",
4089
- size: btnSizeMap[size],
4090
- leftSection: /*#__PURE__*/jsxRuntime.jsx(iconsReact.IconSettings, {
4091
- size: 16,
4092
- stroke: 1.5
4093
- }),
4094
- onClick: onAccountClick,
4095
- children: accountLabel
4096
- }), /*#__PURE__*/jsxRuntime.jsx(core.Button, {
4097
- variant: "default",
4098
- size: btnSizeMap[size],
4099
- leftSection: /*#__PURE__*/jsxRuntime.jsx(iconsReact.IconCreditCard, {
4100
- size: 16,
4101
- stroke: 1.5
4102
- }),
4103
- onClick: onBillingClick,
4104
- 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]
4105
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
+ })
4106
4242
  }), /*#__PURE__*/jsxRuntime.jsxs(core.Group, {
4107
- justify: "center",
4108
- gap: 4,
4109
- opacity: 0.3,
4243
+ justify: "space-between",
4244
+ gap: 8,
4245
+ wrap: "nowrap",
4110
4246
  children: [/*#__PURE__*/jsxRuntime.jsx(core.Text, {
4111
- size: "10px",
4247
+ fz: 12,
4248
+ fw: 500,
4112
4249
  c: "gray.6",
4113
- fw: 600,
4114
- children: "Secured by"
4115
- }), /*#__PURE__*/jsxRuntime.jsxs(core.Group, {
4116
- gap: 2,
4117
- children: [/*#__PURE__*/jsxRuntime.jsx(iconsReact.IconShieldCheck, {
4118
- size: 10,
4119
- stroke: 2
4120
- }), /*#__PURE__*/jsxRuntime.jsx(core.Text, {
4121
- size: "10px",
4122
- fw: 800,
4123
- c: "dark.9",
4124
- children: "Auth"
4125
- })]
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'
4126
4260
  })]
4127
4261
  })]
4128
- })
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))
4278
+ });
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
+ })]
4129
4319
  });
4130
4320
  }
4131
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
+ }
4338
+
4132
4339
  /**
4133
4340
  * Renderiza children apenas quando o usuário está autenticado
4134
4341
  * Equivalente ao <SignedIn> do Clerk