@ciromaciel/auth-react 1.3.0 → 1.4.1

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
@@ -2535,17 +2535,13 @@ function Wordmark({
2535
2535
  });
2536
2536
  }
2537
2537
 
2538
+ // Where the terms live. Hardcoded on purpose, like `API_BASE` in authSdk.js:
2539
+ // this ships inside the published bundle, and every screen of the seven panels
2540
+ // that links to the terms — the sign-in notice and the account card's footer —
2541
+ // must point at the same document. `termsUrl` overrides it for whoever hosts
2542
+ // their own.
2538
2543
  const TERMS_URL = 'https://myinfrastructure.click/legal/terms';
2539
2544
 
2540
- /**
2541
- * The terms notice under the first step.
2542
- *
2543
- * It sits on step ONE and nowhere else: the account is born on the first
2544
- * `verify` and there is no separate sign-up screen to put it on — this form IS
2545
- * the sign-up for whoever has never entered. On step two the person already
2546
- * agreed by asking for the code; repeating it there would only push the code
2547
- * field down.
2548
- */
2549
2545
  function TermsNotice({
2550
2546
  url,
2551
2547
  text,
@@ -3984,150 +3980,377 @@ function UserProfile({
3984
3980
  });
3985
3981
  }
3986
3982
 
3983
+ const ROLE_LABELS = {
3984
+ owner: 'Dono',
3985
+ admin: 'Administrador',
3986
+ member: 'Membro'
3987
+ };
3988
+
3989
+ /**
3990
+ * @typedef {Object} UserInformationItem
3991
+ * @property {string} [id] - Stable key
3992
+ * @property {string} label - The row's text
3993
+ * @property {Function} [icon] - A Tabler icon component
3994
+ * @property {Function} onClick
3995
+ *
3996
+ * @typedef {Object} UserInformationPlan
3997
+ * @property {string} [name] - "Pro", "Starter"… Shown as the badge
3998
+ * @property {number} [used] - How many of the plan's resources are in use
3999
+ * @property {number|null} [limit] - The plan's ceiling; `null` hides the meter
4000
+ * @property {string} [unit] - What is counted, in the plural: "projetos"
4001
+ * @property {string} [actionLabel='Ver planos']
4002
+ * @property {Function} [onClick] - Opens the plans
4003
+ *
4004
+ * @typedef {Object} UserInformationOrganization
4005
+ * @property {string} name
4006
+ * @property {string} [role] - `owner`, `admin`, `member`, or already a label
4007
+ */
4008
+
4009
+ /**
4010
+ * @param {Object} props
4011
+ * @param {Object} props.user - The signed-in user (`name`, `email`, `image`)
4012
+ * @param {Function} props.signOut
4013
+ * @param {Function} [props.onAccountClick]
4014
+ * @param {Function} [props.onBillingClick]
4015
+ * @param {UserInformationItem[]} [props.items] - The panel's own rows, shown in their own group
4016
+ * @param {UserInformationPlan} [props.plan] - The plan strip; omitted, the strip is not drawn
4017
+ * @param {UserInformationOrganization} [props.organization] - Where the person is acting
4018
+ * @param {boolean} [props.branded=true] - The footer: "Protegido por Auth" and the terms link
4019
+ * @param {string|null} [props.termsUrl] - Where "Termos" points; `null` removes the link
4020
+ * @param {string} [props.termsLabel='Termos']
4021
+ * @param {string} [props.accountLabel='Conta']
4022
+ * @param {string} [props.billingLabel='Assinatura']
4023
+ * @param {string} [props.signOutLabel='Sair']
4024
+ */
3987
4025
  function UserInformation({
3988
4026
  user,
3989
4027
  signOut,
3990
4028
  onAccountClick,
3991
4029
  onBillingClick,
4030
+ items = [],
4031
+ plan,
4032
+ organization,
4033
+ branded = true,
4034
+ termsUrl = TERMS_URL,
4035
+ termsLabel = 'Termos',
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
4091
+ },
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: termsUrl ? 'space-between' : 'center',
4144
+ gap: 8,
4145
+ px: 16,
4146
+ py: 10,
4147
+ bg: "gray.1",
4148
+ style: {
4149
+ borderTop: '1px solid var(--mantine-color-gray-2)'
4150
+ },
4151
+ children: [/*#__PURE__*/jsxRuntime.jsxs(core.Text, {
4152
+ fz: 11,
4153
+ fw: 500,
4154
+ c: "gray.5",
4155
+ children: ["Protegido por", ' ', /*#__PURE__*/jsxRuntime.jsx(core.Text, {
4156
+ span: true,
4157
+ inherit: true,
4158
+ fw: 800,
4159
+ c: "gray.7",
4160
+ children: "Auth"
4161
+ })]
4162
+ }), termsUrl && /*#__PURE__*/jsxRuntime.jsx(core.Anchor, {
4163
+ href: termsUrl
4164
+ /*
4165
+ * A new tab, like the sign-in's notice: the card sits
4166
+ * over a working panel, and reading the terms must
4167
+ * not navigate away from it.
4168
+ */,
4169
+ target: "_blank",
4170
+ rel: "noopener noreferrer",
4171
+ fz: 11,
4172
+ fw: 500,
4173
+ c: "gray.5",
4174
+ underline: "always",
4175
+ children: termsLabel
4176
+ })]
4177
+ })]
4178
+ });
4179
+ }
4180
+
4181
+ /** The organization, the role and the plan's usage, on the alternate surface. */
4182
+ function ContextStrip({
4183
+ organization,
4184
+ plan,
4185
+ padded
4186
+ }) {
4187
+ const role = organization?.role ? ROLE_LABELS[organization.role] || organization.role : null;
4188
+ const hasMeter = plan && Number.isFinite(plan.used) && Number.isFinite(plan.limit) && plan.limit > 0;
4189
+ const ratio = hasMeter ? Math.min(1, Math.max(0, plan.used / plan.limit)) : 0;
4190
+ return /*#__PURE__*/jsxRuntime.jsxs(core.Stack, {
4191
+ gap: 8,
4192
+ px: padded ? 16 : 0,
4193
+ py: 12,
4194
+ bg: "gray.1",
4195
+ style: {
4196
+ borderTop: '1px solid var(--mantine-color-gray-2)',
4197
+ borderBottom: '1px solid var(--mantine-color-gray-2)'
4198
+ },
4199
+ children: [(organization?.name || plan?.name) && /*#__PURE__*/jsxRuntime.jsxs(core.Group, {
4200
+ gap: 8,
4201
+ wrap: "nowrap",
4202
+ children: [organization?.name && /*#__PURE__*/jsxRuntime.jsxs(jsxRuntime.Fragment, {
4203
+ children: [/*#__PURE__*/jsxRuntime.jsx(iconsReact.IconBuilding, {
4204
+ size: 14,
4205
+ stroke: 1.5,
4206
+ style: {
4207
+ flex: 'none',
4208
+ color: 'var(--mantine-color-gray-5)'
4209
+ }
4210
+ }), /*#__PURE__*/jsxRuntime.jsx(core.Text, {
4211
+ fz: 12,
4212
+ fw: 800,
4213
+ c: "gray.9",
4214
+ truncate: "end",
4215
+ style: {
4216
+ minWidth: 0
4056
4217
  },
4057
- children: initials || 'CC'
4058
- }), /*#__PURE__*/jsxRuntime.jsxs(core.Box, {
4218
+ children: organization.name
4219
+ }), role && /*#__PURE__*/jsxRuntime.jsxs(core.Text, {
4220
+ fz: 12,
4221
+ fw: 500,
4222
+ c: "gray.5",
4059
4223
  style: {
4060
- flex: 1,
4061
- overflow: 'hidden'
4224
+ flex: 'none'
4062
4225
  },
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
4226
+ children: ["\xB7 ", role]
4105
4227
  })]
4228
+ }), plan?.name && /*#__PURE__*/jsxRuntime.jsx(core.Text, {
4229
+ component: "span",
4230
+ fz: 10,
4231
+ fw: 800,
4232
+ tt: "uppercase",
4233
+ lts: "1.5px",
4234
+ c: "white",
4235
+ bg: "gray.9",
4236
+ px: 6,
4237
+ lh: 1.6,
4238
+ ml: "auto",
4239
+ style: {
4240
+ flex: 'none'
4241
+ },
4242
+ children: plan.name
4243
+ })]
4244
+ }), hasMeter && /*#__PURE__*/jsxRuntime.jsxs(jsxRuntime.Fragment, {
4245
+ children: [/*#__PURE__*/jsxRuntime.jsx(core.Box, {
4246
+ h: 4,
4247
+ bg: "gray.2",
4248
+ role: "meter",
4249
+ "aria-valuemin": 0,
4250
+ "aria-valuemax": plan.limit,
4251
+ "aria-valuenow": plan.used,
4252
+ "aria-label": plan.unit ? `${plan.unit} em uso` : 'Uso do plano',
4253
+ children: /*#__PURE__*/jsxRuntime.jsx(core.Box, {
4254
+ h: "100%",
4255
+ w: `${ratio * 100}%`,
4256
+ bg: "gray.9"
4257
+ })
4106
4258
  }), /*#__PURE__*/jsxRuntime.jsxs(core.Group, {
4107
- justify: "center",
4108
- gap: 4,
4109
- opacity: 0.3,
4259
+ justify: "space-between",
4260
+ gap: 8,
4261
+ wrap: "nowrap",
4110
4262
  children: [/*#__PURE__*/jsxRuntime.jsx(core.Text, {
4111
- size: "10px",
4263
+ fz: 12,
4264
+ fw: 500,
4112
4265
  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
- })]
4266
+ children: `${plan.used} de ${plan.limit}${plan.unit ? ` ${plan.unit}` : ''}`
4267
+ }), plan.onClick && /*#__PURE__*/jsxRuntime.jsx(core.Anchor, {
4268
+ component: "button",
4269
+ type: "button",
4270
+ fz: 12,
4271
+ fw: 700,
4272
+ c: "gray.9",
4273
+ underline: "always",
4274
+ onClick: plan.onClick,
4275
+ children: plan.actionLabel || 'Ver planos'
4126
4276
  })]
4127
4277
  })]
4128
- })
4278
+ })]
4279
+ });
4280
+ }
4281
+ function RowGroup({
4282
+ rows,
4283
+ hasDivider
4284
+ }) {
4285
+ return /*#__PURE__*/jsxRuntime.jsx(core.Stack, {
4286
+ gap: 0,
4287
+ p: 6,
4288
+ style: hasDivider ? {
4289
+ borderTop: '1px solid var(--mantine-color-gray-2)'
4290
+ } : undefined,
4291
+ children: rows.map(row => /*#__PURE__*/jsxRuntime.jsx(Row, {
4292
+ ...row
4293
+ }, row.id || row.label))
4129
4294
  });
4130
4295
  }
4296
+ function Row({
4297
+ label,
4298
+ icon: Icon,
4299
+ onClick
4300
+ }) {
4301
+ return /*#__PURE__*/jsxRuntime.jsxs(core.UnstyledButton, {
4302
+ role: "menuitem",
4303
+ onClick: onClick,
4304
+ px: 10,
4305
+ py: 7,
4306
+ w: "100%",
4307
+ style: {
4308
+ display: 'flex',
4309
+ alignItems: 'center',
4310
+ gap: 10,
4311
+ borderRadius: 0
4312
+ }
4313
+ /*
4314
+ * Hover and keyboard focus share the same surface: a row reached
4315
+ * with the arrows must look exactly like the one under the mouse.
4316
+ */,
4317
+ onMouseEnter: event => event.currentTarget.style.background = 'var(--mantine-color-gray-1)',
4318
+ onMouseLeave: event => event.currentTarget.style.background = 'transparent',
4319
+ onFocus: event => event.currentTarget.style.background = 'var(--mantine-color-gray-1)',
4320
+ onBlur: event => event.currentTarget.style.background = 'transparent',
4321
+ children: [Icon && /*#__PURE__*/jsxRuntime.jsx(Icon, {
4322
+ size: 16,
4323
+ stroke: 1.5,
4324
+ style: {
4325
+ flex: 'none',
4326
+ color: 'var(--mantine-color-gray-5)'
4327
+ }
4328
+ }), /*#__PURE__*/jsxRuntime.jsx(core.Text, {
4329
+ fz: 12,
4330
+ fw: 500,
4331
+ c: "gray.9",
4332
+ truncate: "end",
4333
+ children: label
4334
+ })]
4335
+ });
4336
+ }
4337
+
4338
+ /*
4339
+ * Arrow keys walk the rows, Home and End jump to the ends — the WAI-ARIA menu
4340
+ * pattern. Tab still leaves the card, so it never traps focus inside a popover
4341
+ * the panel owns.
4342
+ */
4343
+ function moveFocus(event) {
4344
+ const keys = ['ArrowDown', 'ArrowUp', 'Home', 'End'];
4345
+ if (!keys.includes(event.key)) return;
4346
+ const rows = Array.from(event.currentTarget.querySelectorAll('[role="menuitem"]'));
4347
+ if (rows.length === 0) return;
4348
+ event.preventDefault();
4349
+ const current = rows.indexOf(document.activeElement);
4350
+ const last = rows.length - 1;
4351
+ const next = event.key === 'Home' ? 0 : event.key === 'End' ? last : event.key === 'ArrowDown' ? current < last ? current + 1 : 0 : current > 0 ? current - 1 : last;
4352
+ rows[next].focus();
4353
+ }
4131
4354
 
4132
4355
  /**
4133
4356
  * Renderiza children apenas quando o usuário está autenticado