@poly-x/react 0.1.0-alpha.3 → 0.1.0-alpha.4

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.cjs CHANGED
@@ -355,6 +355,7 @@ function Protected({ children, fallback = null, loading = null }) {
355
355
  const DEFAULT_LABELS = {
356
356
  heading: "Sign in",
357
357
  subheading: "Enter your credentials to continue.",
358
+ tagline: "",
358
359
  emailLabel: "Email",
359
360
  emailPlaceholder: "you@company.com",
360
361
  passwordLabel: "Password",
@@ -363,11 +364,21 @@ const DEFAULT_LABELS = {
363
364
  submitting: "Signing in…",
364
365
  chooseWorkspace: "Choose a workspace",
365
366
  chooseWorkspaceSubheading: "You have access to more than one workspace.",
367
+ securedBy: "Secured by PolyX",
368
+ setPasswordHeading: "Choose a new password",
369
+ setPasswordSubheading: "Your temporary password must be replaced before you continue.",
370
+ newPasswordLabel: "New password",
371
+ confirmPasswordLabel: "Confirm new password",
372
+ setPasswordSubmit: "Set password and sign in",
373
+ passwordMismatch: "Those passwords don't match.",
374
+ weakPassword: "Password must be at least 6 characters.",
366
375
  invalidCredentials: "Invalid email or password.",
367
376
  noAccess: "You don't have access to this application.",
368
377
  passwordChangeRequired: "You must reset your password before you can sign in.",
369
378
  genericError: "Something went wrong. Please try again."
370
379
  };
380
+ /** Mirrors poly-auth's own floor (Joi: min 6). */
381
+ const MIN_PASSWORD_LENGTH = 6;
371
382
  /**
372
383
  * The embedded, Clerk-style sign-in form (D2b). Renders email/password inside the
373
384
  * consuming app and POSTs to the SDK's BFF (`@poly-x/next` `authenticate`), which
@@ -387,10 +398,18 @@ function SignIn(props) {
387
398
  };
388
399
  const [email, setEmail] = (0, react.useState)("");
389
400
  const [password, setPassword] = (0, react.useState)("");
401
+ const [newPassword, setNewPassword] = (0, react.useState)("");
402
+ const [confirmPassword, setConfirmPassword] = (0, react.useState)("");
403
+ const [mustSetPassword, setMustSetPassword] = (0, react.useState)(false);
390
404
  const [tenants, setTenants] = (0, react.useState)(null);
391
405
  const [submitting, setSubmitting] = (0, react.useState)(false);
392
406
  const [error, setError] = (0, react.useState)(null);
393
- async function authenticate(tenantId) {
407
+ /**
408
+ * One round-trip to the BFF. `tenantId` picks a workspace; `passwords` completes
409
+ * the temp-password flow (the BFF re-verifies the temporary password, sets the
410
+ * new one, and signs the user straight in).
411
+ */
412
+ async function authenticate(options = {}) {
394
413
  setSubmitting(true);
395
414
  setError(null);
396
415
  try {
@@ -401,7 +420,8 @@ function SignIn(props) {
401
420
  email,
402
421
  password,
403
422
  organizationCode: props.organizationCode,
404
- tenantId
423
+ tenantId: options.tenantId,
424
+ ...options.passwords
405
425
  })
406
426
  })).json().catch(() => ({}));
407
427
  switch (body.status) {
@@ -411,16 +431,22 @@ function SignIn(props) {
411
431
  case "select_tenant":
412
432
  setTenants(body.tenants ?? []);
413
433
  return;
434
+ case "password_change_required":
435
+ setMustSetPassword(true);
436
+ if (body.userId) props.onPasswordChangeRequired?.(body.userId);
437
+ return;
438
+ case "password_mismatch":
439
+ setError(labels.passwordMismatch);
440
+ return;
441
+ case "weak_password":
442
+ setError(labels.weakPassword);
443
+ return;
414
444
  case "invalid_credentials":
415
445
  setError(labels.invalidCredentials);
416
446
  return;
417
447
  case "no_access":
418
448
  setError(labels.noAccess);
419
449
  return;
420
- case "password_change_required":
421
- setError(labels.passwordChangeRequired);
422
- if (body.userId) props.onPasswordChangeRequired?.(body.userId);
423
- return;
424
450
  default:
425
451
  setError(labels.genericError);
426
452
  return;
@@ -435,26 +461,55 @@ function SignIn(props) {
435
461
  event.preventDefault();
436
462
  authenticate();
437
463
  }
464
+ function onSetPasswordSubmit(event) {
465
+ event.preventDefault();
466
+ if (newPassword !== confirmPassword) {
467
+ setError(labels.passwordMismatch);
468
+ return;
469
+ }
470
+ authenticate({ passwords: {
471
+ newPassword,
472
+ confirmPassword
473
+ } });
474
+ }
475
+ const picking = tenants !== null;
476
+ const hasPanel = variant === "page" && Boolean(props.panel || props.logo || labels.tagline);
477
+ const showHeaderLogo = Boolean(props.logo) && !hasPanel;
438
478
  const rootClass = [
439
479
  "polyx-signin",
440
480
  `polyx-signin--${variant}`,
481
+ hasPanel && "polyx-signin--split",
441
482
  props.className
442
483
  ].filter(Boolean).join(" ");
443
- const picking = tenants !== null;
484
+ const step = picking ? "select_tenant" : mustSetPassword ? "set_password" : "credentials";
485
+ const HEADINGS = {
486
+ credentials: {
487
+ heading: labels.heading,
488
+ subheading: labels.subheading
489
+ },
490
+ select_tenant: {
491
+ heading: labels.chooseWorkspace,
492
+ subheading: labels.chooseWorkspaceSubheading
493
+ },
494
+ set_password: {
495
+ heading: labels.setPasswordHeading,
496
+ subheading: labels.setPasswordSubheading
497
+ }
498
+ };
444
499
  const header = /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
445
500
  className: "polyx-signin__header",
446
501
  children: [
447
- props.logo ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
502
+ showHeaderLogo ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
448
503
  className: "polyx-signin__logo",
449
504
  children: props.logo
450
505
  }) : null,
451
506
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h1", {
452
507
  className: "polyx-signin__heading",
453
- children: picking ? labels.chooseWorkspace : labels.heading
508
+ children: HEADINGS[step].heading
454
509
  }),
455
510
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
456
511
  className: "polyx-signin__subheading",
457
- children: picking ? labels.chooseWorkspaceSubheading : labels.subheading
512
+ children: HEADINGS[step].subheading
458
513
  })
459
514
  ]
460
515
  });
@@ -463,24 +518,84 @@ function SignIn(props) {
463
518
  className: "polyx-signin__error",
464
519
  children: error
465
520
  }) : null;
466
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
467
- className: rootClass,
468
- "data-polyx-signin": picking ? "select-tenant" : "credentials",
469
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
470
- className: "polyx-signin__card",
471
- children: [header, picking ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
472
- className: "polyx-signin__form",
473
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
474
- className: "polyx-signin__tenants",
475
- children: tenants.map((tenant) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
476
- type: "button",
477
- className: "polyx-signin__tenant",
478
- disabled: submitting,
479
- onClick: () => void authenticate(tenant.tenantId),
480
- children: tenant.name ?? tenant.slug ?? tenant.organizationCode ?? tenant.tenantId
481
- }) }, tenant.tenantId))
482
- }), errorNode]
483
- }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("form", {
521
+ const footer = props.showSecuredBy === false ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
522
+ className: "polyx-signin__footer",
523
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
524
+ className: "polyx-signin__shield",
525
+ viewBox: "0 0 16 16",
526
+ fill: "currentColor",
527
+ "aria-hidden": "true",
528
+ focusable: "false",
529
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M8 0.5 2 3v4.2c0 3.5 2.5 6.7 6 7.8 3.5-1.1 6-4.3 6-7.8V3L8 .5Zm2.9 5.6-3.4 3.4a.7.7 0 0 1-1 0L5.1 8.1a.7.7 0 1 1 1-1l.9.9 2.9-2.9a.7.7 0 0 1 1 1Z" })
530
+ }), labels.securedBy]
531
+ });
532
+ const tenantPicker = /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
533
+ className: "polyx-signin__form",
534
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
535
+ className: "polyx-signin__tenants",
536
+ children: (tenants ?? []).map((tenant) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
537
+ type: "button",
538
+ className: "polyx-signin__tenant",
539
+ disabled: submitting,
540
+ onClick: () => void authenticate({ tenantId: tenant.tenantId }),
541
+ children: tenant.name ?? tenant.slug ?? tenant.organizationCode ?? tenant.tenantId
542
+ }) }, tenant.tenantId))
543
+ }), errorNode]
544
+ });
545
+ const setPasswordForm = /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("form", {
546
+ className: "polyx-signin__form",
547
+ onSubmit: onSetPasswordSubmit,
548
+ children: [
549
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
550
+ className: "polyx-signin__field",
551
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
552
+ className: "polyx-signin__label",
553
+ children: labels.newPasswordLabel
554
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
555
+ className: "polyx-signin__input",
556
+ type: "password",
557
+ name: "newPassword",
558
+ autoComplete: "new-password",
559
+ placeholder: labels.passwordPlaceholder,
560
+ required: true,
561
+ minLength: MIN_PASSWORD_LENGTH,
562
+ value: newPassword,
563
+ disabled: submitting,
564
+ onChange: (event) => setNewPassword(event.target.value)
565
+ })]
566
+ }),
567
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
568
+ className: "polyx-signin__field",
569
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
570
+ className: "polyx-signin__label",
571
+ children: labels.confirmPasswordLabel
572
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
573
+ className: "polyx-signin__input",
574
+ type: "password",
575
+ name: "confirmPassword",
576
+ autoComplete: "new-password",
577
+ placeholder: labels.passwordPlaceholder,
578
+ required: true,
579
+ minLength: MIN_PASSWORD_LENGTH,
580
+ value: confirmPassword,
581
+ disabled: submitting,
582
+ onChange: (event) => setConfirmPassword(event.target.value)
583
+ })]
584
+ }),
585
+ errorNode,
586
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
587
+ type: "submit",
588
+ className: "polyx-signin__submit",
589
+ disabled: submitting,
590
+ children: submitting ? labels.submitting : labels.setPasswordSubmit
591
+ })
592
+ ]
593
+ });
594
+ const card = /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
595
+ className: "polyx-signin__card",
596
+ children: [
597
+ header,
598
+ step === "select_tenant" ? tenantPicker : step === "set_password" ? setPasswordForm : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("form", {
484
599
  className: "polyx-signin__form",
485
600
  onSubmit,
486
601
  children: [
@@ -526,8 +641,31 @@ function SignIn(props) {
526
641
  children: submitting ? labels.submitting : labels.submit
527
642
  })
528
643
  ]
529
- })]
530
- })
644
+ }),
645
+ footer
646
+ ]
647
+ });
648
+ if (variant === "page") return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
649
+ className: rootClass,
650
+ "data-polyx-signin": step,
651
+ children: [hasPanel ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("aside", {
652
+ className: "polyx-signin__panel",
653
+ children: props.panel ?? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [props.logo ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
654
+ className: "polyx-signin__panel-logo",
655
+ children: props.logo
656
+ }) : null, labels.tagline ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
657
+ className: "polyx-signin__tagline",
658
+ children: labels.tagline
659
+ }) : null] })
660
+ }) : null, /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
661
+ className: "polyx-signin__main",
662
+ children: card
663
+ })]
664
+ });
665
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
666
+ className: rootClass,
667
+ "data-polyx-signin": step,
668
+ children: card
531
669
  });
532
670
  }
533
671
  //#endregion
package/dist/index.d.cts CHANGED
@@ -195,6 +195,8 @@ declare function Protected({
195
195
  interface SignInLabels {
196
196
  heading: string;
197
197
  subheading: string;
198
+ /** Headline on the `page` variant's brand panel. Empty renders a clean brand panel. */
199
+ tagline: string;
198
200
  emailLabel: string;
199
201
  emailPlaceholder: string;
200
202
  passwordLabel: string;
@@ -203,6 +205,14 @@ interface SignInLabels {
203
205
  submitting: string;
204
206
  chooseWorkspace: string;
205
207
  chooseWorkspaceSubheading: string;
208
+ securedBy: string;
209
+ setPasswordHeading: string;
210
+ setPasswordSubheading: string;
211
+ newPasswordLabel: string;
212
+ confirmPasswordLabel: string;
213
+ setPasswordSubmit: string;
214
+ passwordMismatch: string;
215
+ weakPassword: string;
206
216
  invalidCredentials: string;
207
217
  noAccess: string;
208
218
  passwordChangeRequired: string;
@@ -210,12 +220,19 @@ interface SignInLabels {
210
220
  }
211
221
  /**
212
222
  * `card` renders just the card — drop it into your own layout.
213
- * `page` owns the viewport — a centered card on a full-bleed background.
223
+ * `page` owns the viewport — a split layout with a brand panel beside the form
224
+ * (the panel collapses away under 64rem, leaving the form on its own).
214
225
  */
215
226
  type SignInVariant = "card" | "page";
216
227
  interface SignInProps {
217
228
  /** Layout preset. Default `card`. */
218
229
  variant?: SignInVariant;
230
+ /**
231
+ * Content for the `page` variant's brand panel. Defaults to your `logo` plus
232
+ * `labels.tagline`; pass your own node to take the panel over completely.
233
+ * Ignored by the `card` variant.
234
+ */
235
+ panel?: ReactNode;
219
236
  /** BFF endpoint backed by `createAuthHandlers().authenticate`. Default `/api/polyx/authenticate`. */
220
237
  action?: string;
221
238
  /** Where to send the browser after success. Overrides the server's default redirect. */
@@ -224,6 +241,8 @@ interface SignInProps {
224
241
  organizationCode?: string;
225
242
  /** Rendered above the heading — your wordmark or logo. */
226
243
  logo?: ReactNode;
244
+ /** Show the "Secured by PolyX" attribution under the form. Default true. */
245
+ showSecuredBy?: boolean;
227
246
  /** Appended to the root element's class list, for escape-hatch styling. */
228
247
  className?: string;
229
248
  labels?: Partial<SignInLabels>;
package/dist/index.d.mts CHANGED
@@ -195,6 +195,8 @@ declare function Protected({
195
195
  interface SignInLabels {
196
196
  heading: string;
197
197
  subheading: string;
198
+ /** Headline on the `page` variant's brand panel. Empty renders a clean brand panel. */
199
+ tagline: string;
198
200
  emailLabel: string;
199
201
  emailPlaceholder: string;
200
202
  passwordLabel: string;
@@ -203,6 +205,14 @@ interface SignInLabels {
203
205
  submitting: string;
204
206
  chooseWorkspace: string;
205
207
  chooseWorkspaceSubheading: string;
208
+ securedBy: string;
209
+ setPasswordHeading: string;
210
+ setPasswordSubheading: string;
211
+ newPasswordLabel: string;
212
+ confirmPasswordLabel: string;
213
+ setPasswordSubmit: string;
214
+ passwordMismatch: string;
215
+ weakPassword: string;
206
216
  invalidCredentials: string;
207
217
  noAccess: string;
208
218
  passwordChangeRequired: string;
@@ -210,12 +220,19 @@ interface SignInLabels {
210
220
  }
211
221
  /**
212
222
  * `card` renders just the card — drop it into your own layout.
213
- * `page` owns the viewport — a centered card on a full-bleed background.
223
+ * `page` owns the viewport — a split layout with a brand panel beside the form
224
+ * (the panel collapses away under 64rem, leaving the form on its own).
214
225
  */
215
226
  type SignInVariant = "card" | "page";
216
227
  interface SignInProps {
217
228
  /** Layout preset. Default `card`. */
218
229
  variant?: SignInVariant;
230
+ /**
231
+ * Content for the `page` variant's brand panel. Defaults to your `logo` plus
232
+ * `labels.tagline`; pass your own node to take the panel over completely.
233
+ * Ignored by the `card` variant.
234
+ */
235
+ panel?: ReactNode;
219
236
  /** BFF endpoint backed by `createAuthHandlers().authenticate`. Default `/api/polyx/authenticate`. */
220
237
  action?: string;
221
238
  /** Where to send the browser after success. Overrides the server's default redirect. */
@@ -224,6 +241,8 @@ interface SignInProps {
224
241
  organizationCode?: string;
225
242
  /** Rendered above the heading — your wordmark or logo. */
226
243
  logo?: ReactNode;
244
+ /** Show the "Secured by PolyX" attribution under the form. Default true. */
245
+ showSecuredBy?: boolean;
227
246
  /** Appended to the root element's class list, for escape-hatch styling. */
228
247
  className?: string;
229
248
  labels?: Partial<SignInLabels>;
package/dist/index.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
  import { AuthClient, FetchTransport, InMemoryLock, InMemoryPkceStore, InMemorySessionChannel, InMemorySessionStore, createSessionEngine, generatePkce, randomState, resolveConfig } from "@poly-x/core";
3
3
  import { createContext, useCallback, useContext, useEffect, useMemo, useState, useSyncExternalStore } from "react";
4
- import { jsx, jsxs } from "react/jsx-runtime";
4
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
5
5
  //#region src/auth-channel.ts
6
6
  var BroadcastAuthChannel = class {
7
7
  supported;
@@ -354,6 +354,7 @@ function Protected({ children, fallback = null, loading = null }) {
354
354
  const DEFAULT_LABELS = {
355
355
  heading: "Sign in",
356
356
  subheading: "Enter your credentials to continue.",
357
+ tagline: "",
357
358
  emailLabel: "Email",
358
359
  emailPlaceholder: "you@company.com",
359
360
  passwordLabel: "Password",
@@ -362,11 +363,21 @@ const DEFAULT_LABELS = {
362
363
  submitting: "Signing in…",
363
364
  chooseWorkspace: "Choose a workspace",
364
365
  chooseWorkspaceSubheading: "You have access to more than one workspace.",
366
+ securedBy: "Secured by PolyX",
367
+ setPasswordHeading: "Choose a new password",
368
+ setPasswordSubheading: "Your temporary password must be replaced before you continue.",
369
+ newPasswordLabel: "New password",
370
+ confirmPasswordLabel: "Confirm new password",
371
+ setPasswordSubmit: "Set password and sign in",
372
+ passwordMismatch: "Those passwords don't match.",
373
+ weakPassword: "Password must be at least 6 characters.",
365
374
  invalidCredentials: "Invalid email or password.",
366
375
  noAccess: "You don't have access to this application.",
367
376
  passwordChangeRequired: "You must reset your password before you can sign in.",
368
377
  genericError: "Something went wrong. Please try again."
369
378
  };
379
+ /** Mirrors poly-auth's own floor (Joi: min 6). */
380
+ const MIN_PASSWORD_LENGTH = 6;
370
381
  /**
371
382
  * The embedded, Clerk-style sign-in form (D2b). Renders email/password inside the
372
383
  * consuming app and POSTs to the SDK's BFF (`@poly-x/next` `authenticate`), which
@@ -386,10 +397,18 @@ function SignIn(props) {
386
397
  };
387
398
  const [email, setEmail] = useState("");
388
399
  const [password, setPassword] = useState("");
400
+ const [newPassword, setNewPassword] = useState("");
401
+ const [confirmPassword, setConfirmPassword] = useState("");
402
+ const [mustSetPassword, setMustSetPassword] = useState(false);
389
403
  const [tenants, setTenants] = useState(null);
390
404
  const [submitting, setSubmitting] = useState(false);
391
405
  const [error, setError] = useState(null);
392
- async function authenticate(tenantId) {
406
+ /**
407
+ * One round-trip to the BFF. `tenantId` picks a workspace; `passwords` completes
408
+ * the temp-password flow (the BFF re-verifies the temporary password, sets the
409
+ * new one, and signs the user straight in).
410
+ */
411
+ async function authenticate(options = {}) {
393
412
  setSubmitting(true);
394
413
  setError(null);
395
414
  try {
@@ -400,7 +419,8 @@ function SignIn(props) {
400
419
  email,
401
420
  password,
402
421
  organizationCode: props.organizationCode,
403
- tenantId
422
+ tenantId: options.tenantId,
423
+ ...options.passwords
404
424
  })
405
425
  })).json().catch(() => ({}));
406
426
  switch (body.status) {
@@ -410,16 +430,22 @@ function SignIn(props) {
410
430
  case "select_tenant":
411
431
  setTenants(body.tenants ?? []);
412
432
  return;
433
+ case "password_change_required":
434
+ setMustSetPassword(true);
435
+ if (body.userId) props.onPasswordChangeRequired?.(body.userId);
436
+ return;
437
+ case "password_mismatch":
438
+ setError(labels.passwordMismatch);
439
+ return;
440
+ case "weak_password":
441
+ setError(labels.weakPassword);
442
+ return;
413
443
  case "invalid_credentials":
414
444
  setError(labels.invalidCredentials);
415
445
  return;
416
446
  case "no_access":
417
447
  setError(labels.noAccess);
418
448
  return;
419
- case "password_change_required":
420
- setError(labels.passwordChangeRequired);
421
- if (body.userId) props.onPasswordChangeRequired?.(body.userId);
422
- return;
423
449
  default:
424
450
  setError(labels.genericError);
425
451
  return;
@@ -434,26 +460,55 @@ function SignIn(props) {
434
460
  event.preventDefault();
435
461
  authenticate();
436
462
  }
463
+ function onSetPasswordSubmit(event) {
464
+ event.preventDefault();
465
+ if (newPassword !== confirmPassword) {
466
+ setError(labels.passwordMismatch);
467
+ return;
468
+ }
469
+ authenticate({ passwords: {
470
+ newPassword,
471
+ confirmPassword
472
+ } });
473
+ }
474
+ const picking = tenants !== null;
475
+ const hasPanel = variant === "page" && Boolean(props.panel || props.logo || labels.tagline);
476
+ const showHeaderLogo = Boolean(props.logo) && !hasPanel;
437
477
  const rootClass = [
438
478
  "polyx-signin",
439
479
  `polyx-signin--${variant}`,
480
+ hasPanel && "polyx-signin--split",
440
481
  props.className
441
482
  ].filter(Boolean).join(" ");
442
- const picking = tenants !== null;
483
+ const step = picking ? "select_tenant" : mustSetPassword ? "set_password" : "credentials";
484
+ const HEADINGS = {
485
+ credentials: {
486
+ heading: labels.heading,
487
+ subheading: labels.subheading
488
+ },
489
+ select_tenant: {
490
+ heading: labels.chooseWorkspace,
491
+ subheading: labels.chooseWorkspaceSubheading
492
+ },
493
+ set_password: {
494
+ heading: labels.setPasswordHeading,
495
+ subheading: labels.setPasswordSubheading
496
+ }
497
+ };
443
498
  const header = /* @__PURE__ */ jsxs("div", {
444
499
  className: "polyx-signin__header",
445
500
  children: [
446
- props.logo ? /* @__PURE__ */ jsx("div", {
501
+ showHeaderLogo ? /* @__PURE__ */ jsx("div", {
447
502
  className: "polyx-signin__logo",
448
503
  children: props.logo
449
504
  }) : null,
450
505
  /* @__PURE__ */ jsx("h1", {
451
506
  className: "polyx-signin__heading",
452
- children: picking ? labels.chooseWorkspace : labels.heading
507
+ children: HEADINGS[step].heading
453
508
  }),
454
509
  /* @__PURE__ */ jsx("p", {
455
510
  className: "polyx-signin__subheading",
456
- children: picking ? labels.chooseWorkspaceSubheading : labels.subheading
511
+ children: HEADINGS[step].subheading
457
512
  })
458
513
  ]
459
514
  });
@@ -462,24 +517,84 @@ function SignIn(props) {
462
517
  className: "polyx-signin__error",
463
518
  children: error
464
519
  }) : null;
465
- return /* @__PURE__ */ jsx("div", {
466
- className: rootClass,
467
- "data-polyx-signin": picking ? "select-tenant" : "credentials",
468
- children: /* @__PURE__ */ jsxs("div", {
469
- className: "polyx-signin__card",
470
- children: [header, picking ? /* @__PURE__ */ jsxs("div", {
471
- className: "polyx-signin__form",
472
- children: [/* @__PURE__ */ jsx("ul", {
473
- className: "polyx-signin__tenants",
474
- children: tenants.map((tenant) => /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsx("button", {
475
- type: "button",
476
- className: "polyx-signin__tenant",
477
- disabled: submitting,
478
- onClick: () => void authenticate(tenant.tenantId),
479
- children: tenant.name ?? tenant.slug ?? tenant.organizationCode ?? tenant.tenantId
480
- }) }, tenant.tenantId))
481
- }), errorNode]
482
- }) : /* @__PURE__ */ jsxs("form", {
520
+ const footer = props.showSecuredBy === false ? null : /* @__PURE__ */ jsxs("p", {
521
+ className: "polyx-signin__footer",
522
+ children: [/* @__PURE__ */ jsx("svg", {
523
+ className: "polyx-signin__shield",
524
+ viewBox: "0 0 16 16",
525
+ fill: "currentColor",
526
+ "aria-hidden": "true",
527
+ focusable: "false",
528
+ children: /* @__PURE__ */ jsx("path", { d: "M8 0.5 2 3v4.2c0 3.5 2.5 6.7 6 7.8 3.5-1.1 6-4.3 6-7.8V3L8 .5Zm2.9 5.6-3.4 3.4a.7.7 0 0 1-1 0L5.1 8.1a.7.7 0 1 1 1-1l.9.9 2.9-2.9a.7.7 0 0 1 1 1Z" })
529
+ }), labels.securedBy]
530
+ });
531
+ const tenantPicker = /* @__PURE__ */ jsxs("div", {
532
+ className: "polyx-signin__form",
533
+ children: [/* @__PURE__ */ jsx("ul", {
534
+ className: "polyx-signin__tenants",
535
+ children: (tenants ?? []).map((tenant) => /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsx("button", {
536
+ type: "button",
537
+ className: "polyx-signin__tenant",
538
+ disabled: submitting,
539
+ onClick: () => void authenticate({ tenantId: tenant.tenantId }),
540
+ children: tenant.name ?? tenant.slug ?? tenant.organizationCode ?? tenant.tenantId
541
+ }) }, tenant.tenantId))
542
+ }), errorNode]
543
+ });
544
+ const setPasswordForm = /* @__PURE__ */ jsxs("form", {
545
+ className: "polyx-signin__form",
546
+ onSubmit: onSetPasswordSubmit,
547
+ children: [
548
+ /* @__PURE__ */ jsxs("label", {
549
+ className: "polyx-signin__field",
550
+ children: [/* @__PURE__ */ jsx("span", {
551
+ className: "polyx-signin__label",
552
+ children: labels.newPasswordLabel
553
+ }), /* @__PURE__ */ jsx("input", {
554
+ className: "polyx-signin__input",
555
+ type: "password",
556
+ name: "newPassword",
557
+ autoComplete: "new-password",
558
+ placeholder: labels.passwordPlaceholder,
559
+ required: true,
560
+ minLength: MIN_PASSWORD_LENGTH,
561
+ value: newPassword,
562
+ disabled: submitting,
563
+ onChange: (event) => setNewPassword(event.target.value)
564
+ })]
565
+ }),
566
+ /* @__PURE__ */ jsxs("label", {
567
+ className: "polyx-signin__field",
568
+ children: [/* @__PURE__ */ jsx("span", {
569
+ className: "polyx-signin__label",
570
+ children: labels.confirmPasswordLabel
571
+ }), /* @__PURE__ */ jsx("input", {
572
+ className: "polyx-signin__input",
573
+ type: "password",
574
+ name: "confirmPassword",
575
+ autoComplete: "new-password",
576
+ placeholder: labels.passwordPlaceholder,
577
+ required: true,
578
+ minLength: MIN_PASSWORD_LENGTH,
579
+ value: confirmPassword,
580
+ disabled: submitting,
581
+ onChange: (event) => setConfirmPassword(event.target.value)
582
+ })]
583
+ }),
584
+ errorNode,
585
+ /* @__PURE__ */ jsx("button", {
586
+ type: "submit",
587
+ className: "polyx-signin__submit",
588
+ disabled: submitting,
589
+ children: submitting ? labels.submitting : labels.setPasswordSubmit
590
+ })
591
+ ]
592
+ });
593
+ const card = /* @__PURE__ */ jsxs("div", {
594
+ className: "polyx-signin__card",
595
+ children: [
596
+ header,
597
+ step === "select_tenant" ? tenantPicker : step === "set_password" ? setPasswordForm : /* @__PURE__ */ jsxs("form", {
483
598
  className: "polyx-signin__form",
484
599
  onSubmit,
485
600
  children: [
@@ -525,8 +640,31 @@ function SignIn(props) {
525
640
  children: submitting ? labels.submitting : labels.submit
526
641
  })
527
642
  ]
528
- })]
529
- })
643
+ }),
644
+ footer
645
+ ]
646
+ });
647
+ if (variant === "page") return /* @__PURE__ */ jsxs("div", {
648
+ className: rootClass,
649
+ "data-polyx-signin": step,
650
+ children: [hasPanel ? /* @__PURE__ */ jsx("aside", {
651
+ className: "polyx-signin__panel",
652
+ children: props.panel ?? /* @__PURE__ */ jsxs(Fragment, { children: [props.logo ? /* @__PURE__ */ jsx("div", {
653
+ className: "polyx-signin__panel-logo",
654
+ children: props.logo
655
+ }) : null, labels.tagline ? /* @__PURE__ */ jsx("p", {
656
+ className: "polyx-signin__tagline",
657
+ children: labels.tagline
658
+ }) : null] })
659
+ }) : null, /* @__PURE__ */ jsx("div", {
660
+ className: "polyx-signin__main",
661
+ children: card
662
+ })]
663
+ });
664
+ return /* @__PURE__ */ jsx("div", {
665
+ className: rootClass,
666
+ "data-polyx-signin": step,
667
+ children: card
530
668
  });
531
669
  }
532
670
  //#endregion
package/dist/styles.css CHANGED
@@ -98,15 +98,6 @@
98
98
 
99
99
  /* --- Variants ------------------------------------------------------------ */
100
100
 
101
- /* `page`: owns the viewport — centered card on a full-bleed background. */
102
- .polyx-signin--page {
103
- display: grid;
104
- place-items: center;
105
- min-height: 100dvh;
106
- padding: 1.5rem;
107
- background: var(--polyx-page-bg);
108
- }
109
-
110
101
  /* `card`: just the card — drop it anywhere in your own layout. */
111
102
  .polyx-signin--card {
112
103
  display: block;
@@ -114,15 +105,110 @@
114
105
 
115
106
  .polyx-signin__card {
116
107
  width: 100%;
117
- max-width: 24rem;
108
+ max-width: 27rem;
118
109
  margin-inline: auto;
119
- padding: 2rem;
110
+ padding: 2.25rem;
120
111
  background: var(--polyx-bg);
121
112
  border: 1px solid var(--polyx-border);
122
113
  border-radius: var(--polyx-radius);
123
114
  box-shadow: var(--polyx-shadow);
124
115
  }
125
116
 
117
+ /**
118
+ * `page`: owns the viewport. With brand content (a `logo`, a `tagline`, or a
119
+ * custom `panel`) it becomes a split layout — brand panel beside the form on
120
+ * wide screens, a slim brand band above it on narrow ones. With no brand content
121
+ * there's nothing to put in a panel, so it stays a single centered column.
122
+ */
123
+ .polyx-signin--page {
124
+ display: grid;
125
+ grid-template-columns: 1fr;
126
+ min-height: 100dvh;
127
+ }
128
+
129
+ .polyx-signin--page.polyx-signin--split {
130
+ grid-template-rows: auto 1fr;
131
+ }
132
+
133
+ @media (min-width: 64rem) {
134
+ .polyx-signin--page.polyx-signin--split {
135
+ grid-template-columns: 1.1fr 1fr;
136
+ grid-template-rows: 1fr;
137
+ }
138
+ }
139
+
140
+ .polyx-signin__panel {
141
+ display: flex;
142
+ position: relative;
143
+ overflow: hidden;
144
+ align-items: center;
145
+ gap: 1rem;
146
+ padding: 1.25rem 1.5rem;
147
+ color: var(--polyx-brand-fg);
148
+ background: linear-gradient(145deg, var(--polyx-brand), color-mix(in srgb, var(--polyx-brand) 55%, #000));
149
+ }
150
+
151
+ @media (min-width: 64rem) {
152
+ .polyx-signin__panel {
153
+ flex-direction: column;
154
+ align-items: flex-start;
155
+ justify-content: space-between;
156
+ gap: 2rem;
157
+ padding: 3.5rem;
158
+ }
159
+ }
160
+
161
+ /* Soft light-bloom so a bare brand panel still reads as designed, not as a flat fill. */
162
+ .polyx-signin__panel::after {
163
+ content: "";
164
+ position: absolute;
165
+ inset: 0;
166
+ pointer-events: none;
167
+ background:
168
+ radial-gradient(circle at 82% 12%, rgb(255 255 255 / 0.18), transparent 45%),
169
+ radial-gradient(circle at 12% 88%, rgb(255 255 255 / 0.1), transparent 42%);
170
+ }
171
+
172
+ .polyx-signin__panel-logo,
173
+ .polyx-signin__tagline {
174
+ position: relative;
175
+ z-index: 1;
176
+ }
177
+
178
+ /* The tagline is the panel's headline — it only earns its space on wide screens. */
179
+ .polyx-signin__tagline {
180
+ display: none;
181
+ margin: 0;
182
+ max-width: 18ch;
183
+ font-size: 2rem;
184
+ font-weight: 600;
185
+ line-height: 1.2;
186
+ letter-spacing: -0.02em;
187
+ }
188
+
189
+ @media (min-width: 64rem) {
190
+ .polyx-signin__tagline {
191
+ display: block;
192
+ }
193
+ }
194
+
195
+ .polyx-signin__main {
196
+ display: grid;
197
+ place-items: center;
198
+ padding: 2.5rem 1.5rem;
199
+ background: var(--polyx-page-bg);
200
+ }
201
+
202
+ /* On the page variant the form isn't a floating card — it sits on the page itself. */
203
+ .polyx-signin--page .polyx-signin__card {
204
+ max-width: 25rem;
205
+ padding: 0;
206
+ background: transparent;
207
+ border: none;
208
+ box-shadow: none;
209
+ }
210
+
211
+
126
212
  /* --- Header -------------------------------------------------------------- */
127
213
 
128
214
  .polyx-signin__header {
@@ -249,6 +335,25 @@
249
335
  border-radius: var(--polyx-radius-sm);
250
336
  }
251
337
 
338
+ /* --- Footer -------------------------------------------------------------- */
339
+
340
+ .polyx-signin__footer {
341
+ display: flex;
342
+ align-items: center;
343
+ justify-content: center;
344
+ gap: 0.375rem;
345
+ margin-top: 1.5rem;
346
+ font-size: 0.75rem;
347
+ color: var(--polyx-muted-fg);
348
+ }
349
+
350
+ .polyx-signin__shield {
351
+ flex: none;
352
+ width: 0.875rem;
353
+ height: 0.875rem;
354
+ opacity: 0.85;
355
+ }
356
+
252
357
  /* --- Workspace picker ---------------------------------------------------- */
253
358
 
254
359
  .polyx-signin__tenants {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@poly-x/react",
3
- "version": "0.1.0-alpha.3",
3
+ "version": "0.1.0-alpha.4",
4
4
  "description": "PolyX SDK for React SPAs - provider, hooks, drop-in auth UI",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -27,7 +27,7 @@
27
27
  "module": "./dist/index.mjs",
28
28
  "types": "./dist/index.d.cts",
29
29
  "dependencies": {
30
- "@poly-x/core": "0.1.0-alpha.3"
30
+ "@poly-x/core": "0.1.0-alpha.4"
31
31
  },
32
32
  "peerDependencies": {
33
33
  "react": ">=18",