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

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,54 @@ 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";
477
+ const showHeaderLogo = Boolean(props.logo) && !hasPanel;
438
478
  const rootClass = [
439
479
  "polyx-signin",
440
480
  `polyx-signin--${variant}`,
441
481
  props.className
442
482
  ].filter(Boolean).join(" ");
443
- 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
+ };
444
498
  const header = /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
445
499
  className: "polyx-signin__header",
446
500
  children: [
447
- props.logo ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
501
+ showHeaderLogo ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
448
502
  className: "polyx-signin__logo",
449
503
  children: props.logo
450
504
  }) : null,
451
505
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h1", {
452
506
  className: "polyx-signin__heading",
453
- children: picking ? labels.chooseWorkspace : labels.heading
507
+ children: HEADINGS[step].heading
454
508
  }),
455
509
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
456
510
  className: "polyx-signin__subheading",
457
- children: picking ? labels.chooseWorkspaceSubheading : labels.subheading
511
+ children: HEADINGS[step].subheading
458
512
  })
459
513
  ]
460
514
  });
@@ -463,24 +517,84 @@ function SignIn(props) {
463
517
  className: "polyx-signin__error",
464
518
  children: error
465
519
  }) : 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", {
520
+ const footer = props.showSecuredBy === false ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
521
+ className: "polyx-signin__footer",
522
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.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__ */ (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" })
529
+ }), labels.securedBy]
530
+ });
531
+ const tenantPicker = /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
532
+ className: "polyx-signin__form",
533
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
534
+ className: "polyx-signin__tenants",
535
+ children: (tenants ?? []).map((tenant) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: /* @__PURE__ */ (0, react_jsx_runtime.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__ */ (0, react_jsx_runtime.jsxs)("form", {
545
+ className: "polyx-signin__form",
546
+ onSubmit: onSetPasswordSubmit,
547
+ children: [
548
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
549
+ className: "polyx-signin__field",
550
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
551
+ className: "polyx-signin__label",
552
+ children: labels.newPasswordLabel
553
+ }), /* @__PURE__ */ (0, react_jsx_runtime.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__ */ (0, react_jsx_runtime.jsxs)("label", {
567
+ className: "polyx-signin__field",
568
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
569
+ className: "polyx-signin__label",
570
+ children: labels.confirmPasswordLabel
571
+ }), /* @__PURE__ */ (0, react_jsx_runtime.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__ */ (0, react_jsx_runtime.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__ */ (0, react_jsx_runtime.jsxs)("div", {
594
+ className: "polyx-signin__card",
595
+ children: [
596
+ header,
597
+ step === "select_tenant" ? tenantPicker : step === "set_password" ? setPasswordForm : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("form", {
484
598
  className: "polyx-signin__form",
485
599
  onSubmit,
486
600
  children: [
@@ -526,8 +640,31 @@ function SignIn(props) {
526
640
  children: submitting ? labels.submitting : labels.submit
527
641
  })
528
642
  ]
529
- })]
530
- })
643
+ }),
644
+ footer
645
+ ]
646
+ });
647
+ if (variant === "page") return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
648
+ className: rootClass,
649
+ "data-polyx-signin": step,
650
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("aside", {
651
+ className: "polyx-signin__panel",
652
+ children: props.panel ?? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [props.logo ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
653
+ className: "polyx-signin__panel-logo",
654
+ children: props.logo
655
+ }) : null, labels.tagline ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
656
+ className: "polyx-signin__tagline",
657
+ children: labels.tagline
658
+ }) : null] })
659
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
660
+ className: "polyx-signin__main",
661
+ children: card
662
+ })]
663
+ });
664
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
665
+ className: rootClass,
666
+ "data-polyx-signin": step,
667
+ children: card
531
668
  });
532
669
  }
533
670
  //#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,54 @@ 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";
476
+ const showHeaderLogo = Boolean(props.logo) && !hasPanel;
437
477
  const rootClass = [
438
478
  "polyx-signin",
439
479
  `polyx-signin--${variant}`,
440
480
  props.className
441
481
  ].filter(Boolean).join(" ");
442
- const picking = tenants !== null;
482
+ const step = picking ? "select_tenant" : mustSetPassword ? "set_password" : "credentials";
483
+ const HEADINGS = {
484
+ credentials: {
485
+ heading: labels.heading,
486
+ subheading: labels.subheading
487
+ },
488
+ select_tenant: {
489
+ heading: labels.chooseWorkspace,
490
+ subheading: labels.chooseWorkspaceSubheading
491
+ },
492
+ set_password: {
493
+ heading: labels.setPasswordHeading,
494
+ subheading: labels.setPasswordSubheading
495
+ }
496
+ };
443
497
  const header = /* @__PURE__ */ jsxs("div", {
444
498
  className: "polyx-signin__header",
445
499
  children: [
446
- props.logo ? /* @__PURE__ */ jsx("div", {
500
+ showHeaderLogo ? /* @__PURE__ */ jsx("div", {
447
501
  className: "polyx-signin__logo",
448
502
  children: props.logo
449
503
  }) : null,
450
504
  /* @__PURE__ */ jsx("h1", {
451
505
  className: "polyx-signin__heading",
452
- children: picking ? labels.chooseWorkspace : labels.heading
506
+ children: HEADINGS[step].heading
453
507
  }),
454
508
  /* @__PURE__ */ jsx("p", {
455
509
  className: "polyx-signin__subheading",
456
- children: picking ? labels.chooseWorkspaceSubheading : labels.subheading
510
+ children: HEADINGS[step].subheading
457
511
  })
458
512
  ]
459
513
  });
@@ -462,24 +516,84 @@ function SignIn(props) {
462
516
  className: "polyx-signin__error",
463
517
  children: error
464
518
  }) : 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", {
519
+ const footer = props.showSecuredBy === false ? null : /* @__PURE__ */ jsxs("p", {
520
+ className: "polyx-signin__footer",
521
+ children: [/* @__PURE__ */ jsx("svg", {
522
+ className: "polyx-signin__shield",
523
+ viewBox: "0 0 16 16",
524
+ fill: "currentColor",
525
+ "aria-hidden": "true",
526
+ focusable: "false",
527
+ 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" })
528
+ }), labels.securedBy]
529
+ });
530
+ const tenantPicker = /* @__PURE__ */ jsxs("div", {
531
+ className: "polyx-signin__form",
532
+ children: [/* @__PURE__ */ jsx("ul", {
533
+ className: "polyx-signin__tenants",
534
+ children: (tenants ?? []).map((tenant) => /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsx("button", {
535
+ type: "button",
536
+ className: "polyx-signin__tenant",
537
+ disabled: submitting,
538
+ onClick: () => void authenticate({ tenantId: tenant.tenantId }),
539
+ children: tenant.name ?? tenant.slug ?? tenant.organizationCode ?? tenant.tenantId
540
+ }) }, tenant.tenantId))
541
+ }), errorNode]
542
+ });
543
+ const setPasswordForm = /* @__PURE__ */ jsxs("form", {
544
+ className: "polyx-signin__form",
545
+ onSubmit: onSetPasswordSubmit,
546
+ children: [
547
+ /* @__PURE__ */ jsxs("label", {
548
+ className: "polyx-signin__field",
549
+ children: [/* @__PURE__ */ jsx("span", {
550
+ className: "polyx-signin__label",
551
+ children: labels.newPasswordLabel
552
+ }), /* @__PURE__ */ jsx("input", {
553
+ className: "polyx-signin__input",
554
+ type: "password",
555
+ name: "newPassword",
556
+ autoComplete: "new-password",
557
+ placeholder: labels.passwordPlaceholder,
558
+ required: true,
559
+ minLength: MIN_PASSWORD_LENGTH,
560
+ value: newPassword,
561
+ disabled: submitting,
562
+ onChange: (event) => setNewPassword(event.target.value)
563
+ })]
564
+ }),
565
+ /* @__PURE__ */ jsxs("label", {
566
+ className: "polyx-signin__field",
567
+ children: [/* @__PURE__ */ jsx("span", {
568
+ className: "polyx-signin__label",
569
+ children: labels.confirmPasswordLabel
570
+ }), /* @__PURE__ */ jsx("input", {
571
+ className: "polyx-signin__input",
572
+ type: "password",
573
+ name: "confirmPassword",
574
+ autoComplete: "new-password",
575
+ placeholder: labels.passwordPlaceholder,
576
+ required: true,
577
+ minLength: MIN_PASSWORD_LENGTH,
578
+ value: confirmPassword,
579
+ disabled: submitting,
580
+ onChange: (event) => setConfirmPassword(event.target.value)
581
+ })]
582
+ }),
583
+ errorNode,
584
+ /* @__PURE__ */ jsx("button", {
585
+ type: "submit",
586
+ className: "polyx-signin__submit",
587
+ disabled: submitting,
588
+ children: submitting ? labels.submitting : labels.setPasswordSubmit
589
+ })
590
+ ]
591
+ });
592
+ const card = /* @__PURE__ */ jsxs("div", {
593
+ className: "polyx-signin__card",
594
+ children: [
595
+ header,
596
+ step === "select_tenant" ? tenantPicker : step === "set_password" ? setPasswordForm : /* @__PURE__ */ jsxs("form", {
483
597
  className: "polyx-signin__form",
484
598
  onSubmit,
485
599
  children: [
@@ -525,8 +639,31 @@ function SignIn(props) {
525
639
  children: submitting ? labels.submitting : labels.submit
526
640
  })
527
641
  ]
528
- })]
529
- })
642
+ }),
643
+ footer
644
+ ]
645
+ });
646
+ if (variant === "page") return /* @__PURE__ */ jsxs("div", {
647
+ className: rootClass,
648
+ "data-polyx-signin": step,
649
+ children: [/* @__PURE__ */ jsx("aside", {
650
+ className: "polyx-signin__panel",
651
+ children: props.panel ?? /* @__PURE__ */ jsxs(Fragment, { children: [props.logo ? /* @__PURE__ */ jsx("div", {
652
+ className: "polyx-signin__panel-logo",
653
+ children: props.logo
654
+ }) : null, labels.tagline ? /* @__PURE__ */ jsx("p", {
655
+ className: "polyx-signin__tagline",
656
+ children: labels.tagline
657
+ }) : null] })
658
+ }), /* @__PURE__ */ jsx("div", {
659
+ className: "polyx-signin__main",
660
+ children: card
661
+ })]
662
+ });
663
+ return /* @__PURE__ */ jsx("div", {
664
+ className: rootClass,
665
+ "data-polyx-signin": step,
666
+ children: card
530
667
  });
531
668
  }
532
669
  //#endregion
package/dist/styles.css CHANGED
@@ -25,7 +25,10 @@
25
25
  --polyx-ring: #a5b4fc;
26
26
  --polyx-danger: #dc2626;
27
27
  --polyx-danger-bg: #fef2f2;
28
- --polyx-page-bg: #fafafa;
28
+ --polyx-page-bg: #ffffff;
29
+ /* The `page` variant's left panel — a quiet surface, a shade off the form side. */
30
+ --polyx-panel-bg: #fafafa;
31
+ --polyx-panel-fg: #18181b;
29
32
 
30
33
  /* Shape + type */
31
34
  --polyx-radius: 0.75rem;
@@ -59,7 +62,9 @@
59
62
  --polyx-ring: #4f46e5;
60
63
  --polyx-danger: #f87171;
61
64
  --polyx-danger-bg: #2a1616;
62
- --polyx-page-bg: #09090b;
65
+ --polyx-page-bg: #18181b;
66
+ --polyx-panel-bg: #09090b;
67
+ --polyx-panel-fg: #fafafa;
63
68
  --polyx-shadow: 0 1px 3px rgb(0 0 0 / 0.4), 0 10px 30px rgb(0 0 0 / 0.35);
64
69
  }
65
70
  }
@@ -77,7 +82,9 @@
77
82
  --polyx-ring: #4f46e5;
78
83
  --polyx-danger: #f87171;
79
84
  --polyx-danger-bg: #2a1616;
80
- --polyx-page-bg: #09090b;
85
+ --polyx-page-bg: #18181b;
86
+ --polyx-panel-bg: #09090b;
87
+ --polyx-panel-fg: #fafafa;
81
88
  --polyx-shadow: 0 1px 3px rgb(0 0 0 / 0.4), 0 10px 30px rgb(0 0 0 / 0.35);
82
89
  }
83
90
 
@@ -92,21 +99,14 @@
92
99
  --polyx-ring: #a5b4fc;
93
100
  --polyx-danger: #dc2626;
94
101
  --polyx-danger-bg: #fef2f2;
95
- --polyx-page-bg: #fafafa;
102
+ --polyx-page-bg: #ffffff;
103
+ --polyx-panel-bg: #fafafa;
104
+ --polyx-panel-fg: #18181b;
96
105
  --polyx-shadow: 0 1px 3px rgb(0 0 0 / 0.06), 0 10px 30px rgb(0 0 0 / 0.07);
97
106
  }
98
107
 
99
108
  /* --- Variants ------------------------------------------------------------ */
100
109
 
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
110
  /* `card`: just the card — drop it anywhere in your own layout. */
111
111
  .polyx-signin--card {
112
112
  display: block;
@@ -114,15 +114,86 @@
114
114
 
115
115
  .polyx-signin__card {
116
116
  width: 100%;
117
- max-width: 24rem;
117
+ max-width: 27rem;
118
118
  margin-inline: auto;
119
- padding: 2rem;
119
+ padding: 2.25rem;
120
120
  background: var(--polyx-bg);
121
121
  border: 1px solid var(--polyx-border);
122
122
  border-radius: var(--polyx-radius);
123
123
  box-shadow: var(--polyx-shadow);
124
124
  }
125
125
 
126
+ /**
127
+ * `page`: owns the viewport — a left/right split. The form sits on the right; the
128
+ * left is a quiet surface holding whatever the consumer gives it (a `logo`, a
129
+ * `tagline`, or a custom `panel`). Deliberately minimal: no gradient, no invented
130
+ * copy. An empty panel reads as intentional negative space, and never fights the
131
+ * host app's brand. Below 64rem the panel drops away entirely and the form takes
132
+ * the screen on its own.
133
+ */
134
+ .polyx-signin--page {
135
+ display: grid;
136
+ grid-template-columns: 1fr;
137
+ min-height: 100dvh;
138
+ }
139
+
140
+ @media (min-width: 64rem) {
141
+ .polyx-signin--page {
142
+ grid-template-columns: 1fr 1fr;
143
+ }
144
+ }
145
+
146
+ .polyx-signin__panel {
147
+ display: none;
148
+ flex-direction: column;
149
+ justify-content: space-between;
150
+ gap: 2rem;
151
+ padding: 3.5rem;
152
+ color: var(--polyx-panel-fg);
153
+ background: var(--polyx-panel-bg);
154
+ border-right: 1px solid var(--polyx-border);
155
+ }
156
+
157
+ @media (min-width: 64rem) {
158
+ .polyx-signin__panel {
159
+ display: flex;
160
+ }
161
+ }
162
+
163
+ /* The tagline is the panel's headline — it only earns its space on wide screens. */
164
+ .polyx-signin__tagline {
165
+ display: none;
166
+ margin: 0;
167
+ max-width: 18ch;
168
+ font-size: 2rem;
169
+ font-weight: 600;
170
+ line-height: 1.2;
171
+ letter-spacing: -0.02em;
172
+ }
173
+
174
+ @media (min-width: 64rem) {
175
+ .polyx-signin__tagline {
176
+ display: block;
177
+ }
178
+ }
179
+
180
+ .polyx-signin__main {
181
+ display: grid;
182
+ place-items: center;
183
+ padding: 2.5rem 1.5rem;
184
+ background: var(--polyx-page-bg);
185
+ }
186
+
187
+ /* On the page variant the form isn't a floating card — it sits on the page itself. */
188
+ .polyx-signin--page .polyx-signin__card {
189
+ max-width: 25rem;
190
+ padding: 0;
191
+ background: transparent;
192
+ border: none;
193
+ box-shadow: none;
194
+ }
195
+
196
+
126
197
  /* --- Header -------------------------------------------------------------- */
127
198
 
128
199
  .polyx-signin__header {
@@ -249,6 +320,25 @@
249
320
  border-radius: var(--polyx-radius-sm);
250
321
  }
251
322
 
323
+ /* --- Footer -------------------------------------------------------------- */
324
+
325
+ .polyx-signin__footer {
326
+ display: flex;
327
+ align-items: center;
328
+ justify-content: center;
329
+ gap: 0.375rem;
330
+ margin-top: 1.5rem;
331
+ font-size: 0.75rem;
332
+ color: var(--polyx-muted-fg);
333
+ }
334
+
335
+ .polyx-signin__shield {
336
+ flex: none;
337
+ width: 0.875rem;
338
+ height: 0.875rem;
339
+ opacity: 0.85;
340
+ }
341
+
252
342
  /* --- Workspace picker ---------------------------------------------------- */
253
343
 
254
344
  .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.5",
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.5"
31
31
  },
32
32
  "peerDependencies": {
33
33
  "react": ">=18",