@poly-x/react 0.1.0-alpha.2 → 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
@@ -353,35 +353,63 @@ function Protected({ children, fallback = null, loading = null }) {
353
353
  //#endregion
354
354
  //#region src/components/sign-in.tsx
355
355
  const DEFAULT_LABELS = {
356
+ heading: "Sign in",
357
+ subheading: "Enter your credentials to continue.",
358
+ tagline: "",
356
359
  emailLabel: "Email",
360
+ emailPlaceholder: "you@company.com",
357
361
  passwordLabel: "Password",
362
+ passwordPlaceholder: "••••••••",
358
363
  submit: "Sign in",
359
364
  submitting: "Signing in…",
360
365
  chooseWorkspace: "Choose a workspace",
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.",
361
375
  invalidCredentials: "Invalid email or password.",
362
376
  noAccess: "You don't have access to this application.",
363
377
  passwordChangeRequired: "You must reset your password before you can sign in.",
364
378
  genericError: "Something went wrong. Please try again."
365
379
  };
380
+ /** Mirrors poly-auth's own floor (Joi: min 6). */
381
+ const MIN_PASSWORD_LENGTH = 6;
366
382
  /**
367
383
  * The embedded, Clerk-style sign-in form (D2b). Renders email/password inside the
368
384
  * consuming app and POSTs to the SDK's BFF (`@poly-x/next` `authenticate`), which
369
385
  * runs the credential authorize server-side and seals an httpOnly session — no
370
386
  * redirect to a hosted PolyX login page, no tokens or credentials in the browser
371
387
  * after submit. Handles the workspace picker (select_tenant) inline.
388
+ *
389
+ * Ships styled: `import "@poly-x/react/styles.css"` once, then theme it by
390
+ * overriding the `--polyx-*` custom properties on any ancestor.
372
391
  */
373
392
  function SignIn(props) {
374
393
  const action = props.action ?? "/api/polyx/authenticate";
394
+ const variant = props.variant ?? "card";
375
395
  const labels = {
376
396
  ...DEFAULT_LABELS,
377
397
  ...props.labels
378
398
  };
379
399
  const [email, setEmail] = (0, react.useState)("");
380
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);
381
404
  const [tenants, setTenants] = (0, react.useState)(null);
382
405
  const [submitting, setSubmitting] = (0, react.useState)(false);
383
406
  const [error, setError] = (0, react.useState)(null);
384
- 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 = {}) {
385
413
  setSubmitting(true);
386
414
  setError(null);
387
415
  try {
@@ -392,7 +420,8 @@ function SignIn(props) {
392
420
  email,
393
421
  password,
394
422
  organizationCode: props.organizationCode,
395
- tenantId
423
+ tenantId: options.tenantId,
424
+ ...options.passwords
396
425
  })
397
426
  })).json().catch(() => ({}));
398
427
  switch (body.status) {
@@ -402,16 +431,22 @@ function SignIn(props) {
402
431
  case "select_tenant":
403
432
  setTenants(body.tenants ?? []);
404
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;
405
444
  case "invalid_credentials":
406
445
  setError(labels.invalidCredentials);
407
446
  return;
408
447
  case "no_access":
409
448
  setError(labels.noAccess);
410
449
  return;
411
- case "password_change_required":
412
- setError(labels.passwordChangeRequired);
413
- if (body.userId) props.onPasswordChangeRequired?.(body.userId);
414
- return;
415
450
  default:
416
451
  setError(labels.genericError);
417
452
  return;
@@ -426,73 +461,212 @@ function SignIn(props) {
426
461
  event.preventDefault();
427
462
  authenticate();
428
463
  }
429
- if (tenants) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
430
- className: props.className,
431
- "data-polyx-signin": "select-tenant",
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;
478
+ const rootClass = [
479
+ "polyx-signin",
480
+ `polyx-signin--${variant}`,
481
+ hasPanel && "polyx-signin--split",
482
+ props.className
483
+ ].filter(Boolean).join(" ");
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
+ };
499
+ const header = /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
500
+ className: "polyx-signin__header",
432
501
  children: [
433
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
502
+ showHeaderLogo ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
503
+ className: "polyx-signin__logo",
504
+ children: props.logo
505
+ }) : null,
506
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h1", {
434
507
  className: "polyx-signin__heading",
435
- children: labels.chooseWorkspace
508
+ children: HEADINGS[step].heading
436
509
  }),
437
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
438
- className: "polyx-signin__tenants",
439
- children: tenants.map((tenant) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
440
- type: "button",
441
- className: "polyx-signin__tenant",
442
- disabled: submitting,
443
- onClick: () => void authenticate(tenant.tenantId),
444
- children: tenant.name ?? tenant.slug ?? tenant.organizationCode ?? tenant.tenantId
445
- }) }, tenant.tenantId))
446
- }),
447
- error ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
448
- role: "alert",
449
- className: "polyx-signin__error",
450
- children: error
451
- }) : null
510
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
511
+ className: "polyx-signin__subheading",
512
+ children: HEADINGS[step].subheading
513
+ })
452
514
  ]
453
515
  });
454
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("form", {
455
- className: props.className,
456
- "data-polyx-signin": "credentials",
457
- onSubmit,
516
+ const errorNode = error ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
517
+ role: "alert",
518
+ className: "polyx-signin__error",
519
+ children: error
520
+ }) : null;
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,
458
548
  children: [
459
549
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
460
550
  className: "polyx-signin__field",
461
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: labels.emailLabel }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
462
- type: "email",
463
- name: "email",
464
- autoComplete: "username",
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,
465
560
  required: true,
466
- value: email,
561
+ minLength: MIN_PASSWORD_LENGTH,
562
+ value: newPassword,
467
563
  disabled: submitting,
468
- onChange: (event) => setEmail(event.target.value)
564
+ onChange: (event) => setNewPassword(event.target.value)
469
565
  })]
470
566
  }),
471
567
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
472
568
  className: "polyx-signin__field",
473
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: labels.passwordLabel }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
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",
474
574
  type: "password",
475
- name: "password",
476
- autoComplete: "current-password",
575
+ name: "confirmPassword",
576
+ autoComplete: "new-password",
577
+ placeholder: labels.passwordPlaceholder,
477
578
  required: true,
478
- value: password,
579
+ minLength: MIN_PASSWORD_LENGTH,
580
+ value: confirmPassword,
479
581
  disabled: submitting,
480
- onChange: (event) => setPassword(event.target.value)
582
+ onChange: (event) => setConfirmPassword(event.target.value)
481
583
  })]
482
584
  }),
483
- error ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
484
- role: "alert",
485
- className: "polyx-signin__error",
486
- children: error
487
- }) : null,
585
+ errorNode,
488
586
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
489
587
  type: "submit",
490
588
  className: "polyx-signin__submit",
491
589
  disabled: submitting,
492
- children: submitting ? labels.submitting : labels.submit
590
+ children: submitting ? labels.submitting : labels.setPasswordSubmit
493
591
  })
494
592
  ]
495
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", {
599
+ className: "polyx-signin__form",
600
+ onSubmit,
601
+ children: [
602
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
603
+ className: "polyx-signin__field",
604
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
605
+ className: "polyx-signin__label",
606
+ children: labels.emailLabel
607
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
608
+ className: "polyx-signin__input",
609
+ type: "email",
610
+ name: "email",
611
+ autoComplete: "username",
612
+ placeholder: labels.emailPlaceholder,
613
+ required: true,
614
+ value: email,
615
+ disabled: submitting,
616
+ onChange: (event) => setEmail(event.target.value)
617
+ })]
618
+ }),
619
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
620
+ className: "polyx-signin__field",
621
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
622
+ className: "polyx-signin__label",
623
+ children: labels.passwordLabel
624
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
625
+ className: "polyx-signin__input",
626
+ type: "password",
627
+ name: "password",
628
+ autoComplete: "current-password",
629
+ placeholder: labels.passwordPlaceholder,
630
+ required: true,
631
+ value: password,
632
+ disabled: submitting,
633
+ onChange: (event) => setPassword(event.target.value)
634
+ })]
635
+ }),
636
+ errorNode,
637
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
638
+ type: "submit",
639
+ className: "polyx-signin__submit",
640
+ disabled: submitting,
641
+ children: submitting ? labels.submitting : labels.submit
642
+ })
643
+ ]
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
669
+ });
496
670
  }
497
671
  //#endregion
498
672
  //#region src/components/auth-callback.tsx
package/dist/index.d.cts CHANGED
@@ -193,24 +193,57 @@ declare function Protected({
193
193
  //#region src/components/sign-in.d.ts
194
194
  /** Every user-facing string, overridable for i18n / white-labelling. */
195
195
  interface SignInLabels {
196
+ heading: string;
197
+ subheading: string;
198
+ /** Headline on the `page` variant's brand panel. Empty renders a clean brand panel. */
199
+ tagline: string;
196
200
  emailLabel: string;
201
+ emailPlaceholder: string;
197
202
  passwordLabel: string;
203
+ passwordPlaceholder: string;
198
204
  submit: string;
199
205
  submitting: string;
200
206
  chooseWorkspace: string;
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;
201
216
  invalidCredentials: string;
202
217
  noAccess: string;
203
218
  passwordChangeRequired: string;
204
219
  genericError: string;
205
220
  }
221
+ /**
222
+ * `card` renders just the card — drop it into your own layout.
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).
225
+ */
226
+ type SignInVariant = "card" | "page";
206
227
  interface SignInProps {
228
+ /** Layout preset. Default `card`. */
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;
207
236
  /** BFF endpoint backed by `createAuthHandlers().authenticate`. Default `/api/polyx/authenticate`. */
208
237
  action?: string;
209
238
  /** Where to send the browser after success. Overrides the server's default redirect. */
210
239
  afterSignInPath?: string;
211
240
  /** Pre-scope the login to a single organization (skips org discovery). */
212
241
  organizationCode?: string;
213
- /** Applied to the root `<form>` / picker container style via your own CSS. */
242
+ /** Rendered above the headingyour wordmark or logo. */
243
+ logo?: ReactNode;
244
+ /** Show the "Secured by PolyX" attribution under the form. Default true. */
245
+ showSecuredBy?: boolean;
246
+ /** Appended to the root element's class list, for escape-hatch styling. */
214
247
  className?: string;
215
248
  labels?: Partial<SignInLabels>;
216
249
  /** Fired when the backend requires the user to set a new password before continuing. */
@@ -222,6 +255,9 @@ interface SignInProps {
222
255
  * runs the credential authorize server-side and seals an httpOnly session — no
223
256
  * redirect to a hosted PolyX login page, no tokens or credentials in the browser
224
257
  * after submit. Handles the workspace picker (select_tenant) inline.
258
+ *
259
+ * Ships styled: `import "@poly-x/react/styles.css"` once, then theme it by
260
+ * overriding the `--polyx-*` custom properties on any ancestor.
225
261
  */
226
262
  declare function SignIn(props: SignInProps): ReactNode;
227
263
  //#endregion
@@ -243,4 +279,4 @@ declare function AuthCallback({
243
279
  declare const PACKAGE_NAME = "@poly-x/react";
244
280
  declare const version = "0.0.0";
245
281
  //#endregion
246
- export { AuthCallback, type AuthCallbackMessage, type AuthCallbackProps, type AuthChannel, BroadcastAuthChannel, BroadcastChannelSessionChannel, type BrowserBridge, type CallbackParams, type LoginController, type LoginControllerDeps, PACKAGE_NAME, PolyXContext, type PolyXContextValue, PolyXProvider, type PolyXProviderProps, PopupCancelledError, type PopupHandle, Protected, type ProtectedProps, SessionStoragePkceStore, SignIn, type SignInLabels, type SignInOptions, type SignInProps, type UseAuth, WebLocksLock, WindowBrowserBridge, createLoginController, useAuth, useAuthCallback, useSession, useUser, version };
282
+ export { AuthCallback, type AuthCallbackMessage, type AuthCallbackProps, type AuthChannel, BroadcastAuthChannel, BroadcastChannelSessionChannel, type BrowserBridge, type CallbackParams, type LoginController, type LoginControllerDeps, PACKAGE_NAME, PolyXContext, type PolyXContextValue, PolyXProvider, type PolyXProviderProps, PopupCancelledError, type PopupHandle, Protected, type ProtectedProps, SessionStoragePkceStore, SignIn, type SignInLabels, type SignInOptions, type SignInProps, type SignInVariant, type UseAuth, WebLocksLock, WindowBrowserBridge, createLoginController, useAuth, useAuthCallback, useSession, useUser, version };
package/dist/index.d.mts CHANGED
@@ -193,24 +193,57 @@ declare function Protected({
193
193
  //#region src/components/sign-in.d.ts
194
194
  /** Every user-facing string, overridable for i18n / white-labelling. */
195
195
  interface SignInLabels {
196
+ heading: string;
197
+ subheading: string;
198
+ /** Headline on the `page` variant's brand panel. Empty renders a clean brand panel. */
199
+ tagline: string;
196
200
  emailLabel: string;
201
+ emailPlaceholder: string;
197
202
  passwordLabel: string;
203
+ passwordPlaceholder: string;
198
204
  submit: string;
199
205
  submitting: string;
200
206
  chooseWorkspace: string;
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;
201
216
  invalidCredentials: string;
202
217
  noAccess: string;
203
218
  passwordChangeRequired: string;
204
219
  genericError: string;
205
220
  }
221
+ /**
222
+ * `card` renders just the card — drop it into your own layout.
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).
225
+ */
226
+ type SignInVariant = "card" | "page";
206
227
  interface SignInProps {
228
+ /** Layout preset. Default `card`. */
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;
207
236
  /** BFF endpoint backed by `createAuthHandlers().authenticate`. Default `/api/polyx/authenticate`. */
208
237
  action?: string;
209
238
  /** Where to send the browser after success. Overrides the server's default redirect. */
210
239
  afterSignInPath?: string;
211
240
  /** Pre-scope the login to a single organization (skips org discovery). */
212
241
  organizationCode?: string;
213
- /** Applied to the root `<form>` / picker container style via your own CSS. */
242
+ /** Rendered above the headingyour wordmark or logo. */
243
+ logo?: ReactNode;
244
+ /** Show the "Secured by PolyX" attribution under the form. Default true. */
245
+ showSecuredBy?: boolean;
246
+ /** Appended to the root element's class list, for escape-hatch styling. */
214
247
  className?: string;
215
248
  labels?: Partial<SignInLabels>;
216
249
  /** Fired when the backend requires the user to set a new password before continuing. */
@@ -222,6 +255,9 @@ interface SignInProps {
222
255
  * runs the credential authorize server-side and seals an httpOnly session — no
223
256
  * redirect to a hosted PolyX login page, no tokens or credentials in the browser
224
257
  * after submit. Handles the workspace picker (select_tenant) inline.
258
+ *
259
+ * Ships styled: `import "@poly-x/react/styles.css"` once, then theme it by
260
+ * overriding the `--polyx-*` custom properties on any ancestor.
225
261
  */
226
262
  declare function SignIn(props: SignInProps): ReactNode;
227
263
  //#endregion
@@ -243,4 +279,4 @@ declare function AuthCallback({
243
279
  declare const PACKAGE_NAME = "@poly-x/react";
244
280
  declare const version = "0.0.0";
245
281
  //#endregion
246
- export { AuthCallback, type AuthCallbackMessage, type AuthCallbackProps, type AuthChannel, BroadcastAuthChannel, BroadcastChannelSessionChannel, type BrowserBridge, type CallbackParams, type LoginController, type LoginControllerDeps, PACKAGE_NAME, PolyXContext, type PolyXContextValue, PolyXProvider, type PolyXProviderProps, PopupCancelledError, type PopupHandle, Protected, type ProtectedProps, SessionStoragePkceStore, SignIn, type SignInLabels, type SignInOptions, type SignInProps, type UseAuth, WebLocksLock, WindowBrowserBridge, createLoginController, useAuth, useAuthCallback, useSession, useUser, version };
282
+ export { AuthCallback, type AuthCallbackMessage, type AuthCallbackProps, type AuthChannel, BroadcastAuthChannel, BroadcastChannelSessionChannel, type BrowserBridge, type CallbackParams, type LoginController, type LoginControllerDeps, PACKAGE_NAME, PolyXContext, type PolyXContextValue, PolyXProvider, type PolyXProviderProps, PopupCancelledError, type PopupHandle, Protected, type ProtectedProps, SessionStoragePkceStore, SignIn, type SignInLabels, type SignInOptions, type SignInProps, type SignInVariant, type UseAuth, WebLocksLock, WindowBrowserBridge, createLoginController, useAuth, useAuthCallback, useSession, useUser, version };
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;
@@ -352,35 +352,63 @@ function Protected({ children, fallback = null, loading = null }) {
352
352
  //#endregion
353
353
  //#region src/components/sign-in.tsx
354
354
  const DEFAULT_LABELS = {
355
+ heading: "Sign in",
356
+ subheading: "Enter your credentials to continue.",
357
+ tagline: "",
355
358
  emailLabel: "Email",
359
+ emailPlaceholder: "you@company.com",
356
360
  passwordLabel: "Password",
361
+ passwordPlaceholder: "••••••••",
357
362
  submit: "Sign in",
358
363
  submitting: "Signing in…",
359
364
  chooseWorkspace: "Choose a workspace",
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.",
360
374
  invalidCredentials: "Invalid email or password.",
361
375
  noAccess: "You don't have access to this application.",
362
376
  passwordChangeRequired: "You must reset your password before you can sign in.",
363
377
  genericError: "Something went wrong. Please try again."
364
378
  };
379
+ /** Mirrors poly-auth's own floor (Joi: min 6). */
380
+ const MIN_PASSWORD_LENGTH = 6;
365
381
  /**
366
382
  * The embedded, Clerk-style sign-in form (D2b). Renders email/password inside the
367
383
  * consuming app and POSTs to the SDK's BFF (`@poly-x/next` `authenticate`), which
368
384
  * runs the credential authorize server-side and seals an httpOnly session — no
369
385
  * redirect to a hosted PolyX login page, no tokens or credentials in the browser
370
386
  * after submit. Handles the workspace picker (select_tenant) inline.
387
+ *
388
+ * Ships styled: `import "@poly-x/react/styles.css"` once, then theme it by
389
+ * overriding the `--polyx-*` custom properties on any ancestor.
371
390
  */
372
391
  function SignIn(props) {
373
392
  const action = props.action ?? "/api/polyx/authenticate";
393
+ const variant = props.variant ?? "card";
374
394
  const labels = {
375
395
  ...DEFAULT_LABELS,
376
396
  ...props.labels
377
397
  };
378
398
  const [email, setEmail] = useState("");
379
399
  const [password, setPassword] = useState("");
400
+ const [newPassword, setNewPassword] = useState("");
401
+ const [confirmPassword, setConfirmPassword] = useState("");
402
+ const [mustSetPassword, setMustSetPassword] = useState(false);
380
403
  const [tenants, setTenants] = useState(null);
381
404
  const [submitting, setSubmitting] = useState(false);
382
405
  const [error, setError] = useState(null);
383
- 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 = {}) {
384
412
  setSubmitting(true);
385
413
  setError(null);
386
414
  try {
@@ -391,7 +419,8 @@ function SignIn(props) {
391
419
  email,
392
420
  password,
393
421
  organizationCode: props.organizationCode,
394
- tenantId
422
+ tenantId: options.tenantId,
423
+ ...options.passwords
395
424
  })
396
425
  })).json().catch(() => ({}));
397
426
  switch (body.status) {
@@ -401,16 +430,22 @@ function SignIn(props) {
401
430
  case "select_tenant":
402
431
  setTenants(body.tenants ?? []);
403
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;
404
443
  case "invalid_credentials":
405
444
  setError(labels.invalidCredentials);
406
445
  return;
407
446
  case "no_access":
408
447
  setError(labels.noAccess);
409
448
  return;
410
- case "password_change_required":
411
- setError(labels.passwordChangeRequired);
412
- if (body.userId) props.onPasswordChangeRequired?.(body.userId);
413
- return;
414
449
  default:
415
450
  setError(labels.genericError);
416
451
  return;
@@ -425,73 +460,212 @@ function SignIn(props) {
425
460
  event.preventDefault();
426
461
  authenticate();
427
462
  }
428
- if (tenants) return /* @__PURE__ */ jsxs("div", {
429
- className: props.className,
430
- "data-polyx-signin": "select-tenant",
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;
477
+ const rootClass = [
478
+ "polyx-signin",
479
+ `polyx-signin--${variant}`,
480
+ hasPanel && "polyx-signin--split",
481
+ props.className
482
+ ].filter(Boolean).join(" ");
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
+ };
498
+ const header = /* @__PURE__ */ jsxs("div", {
499
+ className: "polyx-signin__header",
431
500
  children: [
432
- /* @__PURE__ */ jsx("p", {
501
+ showHeaderLogo ? /* @__PURE__ */ jsx("div", {
502
+ className: "polyx-signin__logo",
503
+ children: props.logo
504
+ }) : null,
505
+ /* @__PURE__ */ jsx("h1", {
433
506
  className: "polyx-signin__heading",
434
- children: labels.chooseWorkspace
507
+ children: HEADINGS[step].heading
435
508
  }),
436
- /* @__PURE__ */ jsx("ul", {
437
- className: "polyx-signin__tenants",
438
- children: tenants.map((tenant) => /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsx("button", {
439
- type: "button",
440
- className: "polyx-signin__tenant",
441
- disabled: submitting,
442
- onClick: () => void authenticate(tenant.tenantId),
443
- children: tenant.name ?? tenant.slug ?? tenant.organizationCode ?? tenant.tenantId
444
- }) }, tenant.tenantId))
445
- }),
446
- error ? /* @__PURE__ */ jsx("p", {
447
- role: "alert",
448
- className: "polyx-signin__error",
449
- children: error
450
- }) : null
509
+ /* @__PURE__ */ jsx("p", {
510
+ className: "polyx-signin__subheading",
511
+ children: HEADINGS[step].subheading
512
+ })
451
513
  ]
452
514
  });
453
- return /* @__PURE__ */ jsxs("form", {
454
- className: props.className,
455
- "data-polyx-signin": "credentials",
456
- onSubmit,
515
+ const errorNode = error ? /* @__PURE__ */ jsx("p", {
516
+ role: "alert",
517
+ className: "polyx-signin__error",
518
+ children: error
519
+ }) : null;
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,
457
547
  children: [
458
548
  /* @__PURE__ */ jsxs("label", {
459
549
  className: "polyx-signin__field",
460
- children: [/* @__PURE__ */ jsx("span", { children: labels.emailLabel }), /* @__PURE__ */ jsx("input", {
461
- type: "email",
462
- name: "email",
463
- autoComplete: "username",
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,
464
559
  required: true,
465
- value: email,
560
+ minLength: MIN_PASSWORD_LENGTH,
561
+ value: newPassword,
466
562
  disabled: submitting,
467
- onChange: (event) => setEmail(event.target.value)
563
+ onChange: (event) => setNewPassword(event.target.value)
468
564
  })]
469
565
  }),
470
566
  /* @__PURE__ */ jsxs("label", {
471
567
  className: "polyx-signin__field",
472
- children: [/* @__PURE__ */ jsx("span", { children: labels.passwordLabel }), /* @__PURE__ */ jsx("input", {
568
+ children: [/* @__PURE__ */ jsx("span", {
569
+ className: "polyx-signin__label",
570
+ children: labels.confirmPasswordLabel
571
+ }), /* @__PURE__ */ jsx("input", {
572
+ className: "polyx-signin__input",
473
573
  type: "password",
474
- name: "password",
475
- autoComplete: "current-password",
574
+ name: "confirmPassword",
575
+ autoComplete: "new-password",
576
+ placeholder: labels.passwordPlaceholder,
476
577
  required: true,
477
- value: password,
578
+ minLength: MIN_PASSWORD_LENGTH,
579
+ value: confirmPassword,
478
580
  disabled: submitting,
479
- onChange: (event) => setPassword(event.target.value)
581
+ onChange: (event) => setConfirmPassword(event.target.value)
480
582
  })]
481
583
  }),
482
- error ? /* @__PURE__ */ jsx("p", {
483
- role: "alert",
484
- className: "polyx-signin__error",
485
- children: error
486
- }) : null,
584
+ errorNode,
487
585
  /* @__PURE__ */ jsx("button", {
488
586
  type: "submit",
489
587
  className: "polyx-signin__submit",
490
588
  disabled: submitting,
491
- children: submitting ? labels.submitting : labels.submit
589
+ children: submitting ? labels.submitting : labels.setPasswordSubmit
492
590
  })
493
591
  ]
494
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", {
598
+ className: "polyx-signin__form",
599
+ onSubmit,
600
+ children: [
601
+ /* @__PURE__ */ jsxs("label", {
602
+ className: "polyx-signin__field",
603
+ children: [/* @__PURE__ */ jsx("span", {
604
+ className: "polyx-signin__label",
605
+ children: labels.emailLabel
606
+ }), /* @__PURE__ */ jsx("input", {
607
+ className: "polyx-signin__input",
608
+ type: "email",
609
+ name: "email",
610
+ autoComplete: "username",
611
+ placeholder: labels.emailPlaceholder,
612
+ required: true,
613
+ value: email,
614
+ disabled: submitting,
615
+ onChange: (event) => setEmail(event.target.value)
616
+ })]
617
+ }),
618
+ /* @__PURE__ */ jsxs("label", {
619
+ className: "polyx-signin__field",
620
+ children: [/* @__PURE__ */ jsx("span", {
621
+ className: "polyx-signin__label",
622
+ children: labels.passwordLabel
623
+ }), /* @__PURE__ */ jsx("input", {
624
+ className: "polyx-signin__input",
625
+ type: "password",
626
+ name: "password",
627
+ autoComplete: "current-password",
628
+ placeholder: labels.passwordPlaceholder,
629
+ required: true,
630
+ value: password,
631
+ disabled: submitting,
632
+ onChange: (event) => setPassword(event.target.value)
633
+ })]
634
+ }),
635
+ errorNode,
636
+ /* @__PURE__ */ jsx("button", {
637
+ type: "submit",
638
+ className: "polyx-signin__submit",
639
+ disabled: submitting,
640
+ children: submitting ? labels.submitting : labels.submit
641
+ })
642
+ ]
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
668
+ });
495
669
  }
496
670
  //#endregion
497
671
  //#region src/components/auth-callback.tsx
@@ -0,0 +1,379 @@
1
+ /**
2
+ * @poly-x/react — default component styles.
3
+ *
4
+ * Import once, near the root of your app:
5
+ * import "@poly-x/react/styles.css"; // or "@poly-x/next/styles.css"
6
+ *
7
+ * The SDK is a guest in your app: every rule is scoped under a `.polyx-*` class,
8
+ * there are no global element selectors, and nothing here leaks into your own
9
+ * markup. To theme it, override the custom properties below on any ancestor
10
+ * (`:root`, a wrapper div, …) — that is the supported theming surface. Dark mode
11
+ * follows the OS by default and defers to an explicit `.dark` / `[data-theme]`
12
+ * on a host ancestor when one is present.
13
+ */
14
+
15
+ .polyx-signin {
16
+ /* Colors */
17
+ --polyx-brand: #4f46e5;
18
+ --polyx-brand-hover: #4338ca;
19
+ --polyx-brand-fg: #ffffff;
20
+ --polyx-bg: #ffffff;
21
+ --polyx-fg: #18181b;
22
+ --polyx-muted-fg: #71717a;
23
+ --polyx-border: #e4e4e7;
24
+ --polyx-input-bg: #ffffff;
25
+ --polyx-ring: #a5b4fc;
26
+ --polyx-danger: #dc2626;
27
+ --polyx-danger-bg: #fef2f2;
28
+ --polyx-page-bg: #fafafa;
29
+
30
+ /* Shape + type */
31
+ --polyx-radius: 0.75rem;
32
+ --polyx-radius-sm: 0.5rem;
33
+ --polyx-font: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
34
+ --polyx-shadow: 0 1px 3px rgb(0 0 0 / 0.06), 0 10px 30px rgb(0 0 0 / 0.07);
35
+
36
+ font-family: var(--polyx-font);
37
+ color: var(--polyx-fg);
38
+ -webkit-font-smoothing: antialiased;
39
+ }
40
+
41
+ /* Scoped box model — applies only inside our own subtree. */
42
+ .polyx-signin,
43
+ .polyx-signin *,
44
+ .polyx-signin *::before,
45
+ .polyx-signin *::after {
46
+ box-sizing: border-box;
47
+ }
48
+
49
+ /* Dark: follow the OS… */
50
+ @media (prefers-color-scheme: dark) {
51
+ .polyx-signin {
52
+ --polyx-brand: #6366f1;
53
+ --polyx-brand-hover: #818cf8;
54
+ --polyx-bg: #18181b;
55
+ --polyx-fg: #fafafa;
56
+ --polyx-muted-fg: #a1a1aa;
57
+ --polyx-border: #3f3f46;
58
+ --polyx-input-bg: #27272a;
59
+ --polyx-ring: #4f46e5;
60
+ --polyx-danger: #f87171;
61
+ --polyx-danger-bg: #2a1616;
62
+ --polyx-page-bg: #09090b;
63
+ --polyx-shadow: 0 1px 3px rgb(0 0 0 / 0.4), 0 10px 30px rgb(0 0 0 / 0.35);
64
+ }
65
+ }
66
+
67
+ /* …but an explicit host theme wins over the OS preference (`:where` keeps
68
+ specificity at 0 so these stay as easy to override as the base rule). */
69
+ :where(.dark, [data-theme="dark"]) .polyx-signin {
70
+ --polyx-brand: #6366f1;
71
+ --polyx-brand-hover: #818cf8;
72
+ --polyx-bg: #18181b;
73
+ --polyx-fg: #fafafa;
74
+ --polyx-muted-fg: #a1a1aa;
75
+ --polyx-border: #3f3f46;
76
+ --polyx-input-bg: #27272a;
77
+ --polyx-ring: #4f46e5;
78
+ --polyx-danger: #f87171;
79
+ --polyx-danger-bg: #2a1616;
80
+ --polyx-page-bg: #09090b;
81
+ --polyx-shadow: 0 1px 3px rgb(0 0 0 / 0.4), 0 10px 30px rgb(0 0 0 / 0.35);
82
+ }
83
+
84
+ :where(.light, [data-theme="light"]) .polyx-signin {
85
+ --polyx-brand: #4f46e5;
86
+ --polyx-brand-hover: #4338ca;
87
+ --polyx-bg: #ffffff;
88
+ --polyx-fg: #18181b;
89
+ --polyx-muted-fg: #71717a;
90
+ --polyx-border: #e4e4e7;
91
+ --polyx-input-bg: #ffffff;
92
+ --polyx-ring: #a5b4fc;
93
+ --polyx-danger: #dc2626;
94
+ --polyx-danger-bg: #fef2f2;
95
+ --polyx-page-bg: #fafafa;
96
+ --polyx-shadow: 0 1px 3px rgb(0 0 0 / 0.06), 0 10px 30px rgb(0 0 0 / 0.07);
97
+ }
98
+
99
+ /* --- Variants ------------------------------------------------------------ */
100
+
101
+ /* `card`: just the card — drop it anywhere in your own layout. */
102
+ .polyx-signin--card {
103
+ display: block;
104
+ }
105
+
106
+ .polyx-signin__card {
107
+ width: 100%;
108
+ max-width: 27rem;
109
+ margin-inline: auto;
110
+ padding: 2.25rem;
111
+ background: var(--polyx-bg);
112
+ border: 1px solid var(--polyx-border);
113
+ border-radius: var(--polyx-radius);
114
+ box-shadow: var(--polyx-shadow);
115
+ }
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
+
212
+ /* --- Header -------------------------------------------------------------- */
213
+
214
+ .polyx-signin__header {
215
+ display: flex;
216
+ flex-direction: column;
217
+ gap: 0.375rem;
218
+ margin-bottom: 1.75rem;
219
+ }
220
+
221
+ .polyx-signin__logo {
222
+ display: flex;
223
+ margin-bottom: 0.75rem;
224
+ }
225
+
226
+ .polyx-signin__heading {
227
+ margin: 0;
228
+ font-size: 1.375rem;
229
+ font-weight: 600;
230
+ letter-spacing: -0.01em;
231
+ color: var(--polyx-fg);
232
+ }
233
+
234
+ .polyx-signin__subheading {
235
+ margin: 0;
236
+ font-size: 0.875rem;
237
+ color: var(--polyx-muted-fg);
238
+ }
239
+
240
+ /* --- Form ---------------------------------------------------------------- */
241
+
242
+ .polyx-signin__form {
243
+ display: flex;
244
+ flex-direction: column;
245
+ gap: 1.125rem;
246
+ }
247
+
248
+ .polyx-signin__field {
249
+ display: flex;
250
+ flex-direction: column;
251
+ gap: 0.4375rem;
252
+ }
253
+
254
+ .polyx-signin__label {
255
+ font-size: 0.875rem;
256
+ font-weight: 500;
257
+ color: var(--polyx-fg);
258
+ }
259
+
260
+ .polyx-signin__input {
261
+ width: 100%;
262
+ height: 2.5rem;
263
+ padding: 0 0.75rem;
264
+ font: inherit;
265
+ font-size: 0.875rem;
266
+ color: var(--polyx-fg);
267
+ background: var(--polyx-input-bg);
268
+ border: 1px solid var(--polyx-border);
269
+ border-radius: var(--polyx-radius-sm);
270
+ outline: none;
271
+ transition: border-color 0.15s ease, box-shadow 0.15s ease;
272
+ }
273
+
274
+ .polyx-signin__input::placeholder {
275
+ color: var(--polyx-muted-fg);
276
+ }
277
+
278
+ .polyx-signin__input:focus-visible {
279
+ border-color: var(--polyx-brand);
280
+ box-shadow: 0 0 0 3px var(--polyx-ring);
281
+ }
282
+
283
+ .polyx-signin__input:disabled {
284
+ opacity: 0.6;
285
+ cursor: not-allowed;
286
+ }
287
+
288
+ /* --- Buttons ------------------------------------------------------------- */
289
+
290
+ .polyx-signin__submit,
291
+ .polyx-signin__tenant {
292
+ display: inline-flex;
293
+ align-items: center;
294
+ justify-content: center;
295
+ width: 100%;
296
+ min-height: 2.5rem;
297
+ padding: 0 1rem;
298
+ font: inherit;
299
+ font-size: 0.875rem;
300
+ font-weight: 500;
301
+ color: var(--polyx-brand-fg);
302
+ background: var(--polyx-brand);
303
+ border: 1px solid transparent;
304
+ border-radius: var(--polyx-radius-sm);
305
+ cursor: pointer;
306
+ transition: background-color 0.15s ease;
307
+ }
308
+
309
+ .polyx-signin__submit:hover:not(:disabled),
310
+ .polyx-signin__tenant:hover:not(:disabled) {
311
+ background: var(--polyx-brand-hover);
312
+ }
313
+
314
+ .polyx-signin__submit:focus-visible,
315
+ .polyx-signin__tenant:focus-visible {
316
+ outline: none;
317
+ box-shadow: 0 0 0 3px var(--polyx-ring);
318
+ }
319
+
320
+ .polyx-signin__submit:disabled,
321
+ .polyx-signin__tenant:disabled {
322
+ opacity: 0.6;
323
+ cursor: not-allowed;
324
+ }
325
+
326
+ /* --- Error --------------------------------------------------------------- */
327
+
328
+ .polyx-signin__error {
329
+ margin: 0;
330
+ padding: 0.625rem 0.75rem;
331
+ font-size: 0.8125rem;
332
+ color: var(--polyx-danger);
333
+ background: var(--polyx-danger-bg);
334
+ border: 1px solid color-mix(in srgb, var(--polyx-danger) 30%, transparent);
335
+ border-radius: var(--polyx-radius-sm);
336
+ }
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
+
357
+ /* --- Workspace picker ---------------------------------------------------- */
358
+
359
+ .polyx-signin__tenants {
360
+ display: flex;
361
+ flex-direction: column;
362
+ gap: 0.625rem;
363
+ margin: 0;
364
+ padding: 0;
365
+ list-style: none;
366
+ }
367
+
368
+ /* The picker's buttons read as choices, not as the primary CTA. */
369
+ .polyx-signin__tenant {
370
+ justify-content: flex-start;
371
+ color: var(--polyx-fg);
372
+ background: var(--polyx-input-bg);
373
+ border-color: var(--polyx-border);
374
+ }
375
+
376
+ .polyx-signin__tenant:hover:not(:disabled) {
377
+ background: var(--polyx-input-bg);
378
+ border-color: var(--polyx-brand);
379
+ }
package/package.json CHANGED
@@ -1,10 +1,12 @@
1
1
  {
2
2
  "name": "@poly-x/react",
3
- "version": "0.1.0-alpha.2",
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",
7
- "sideEffects": false,
7
+ "sideEffects": [
8
+ "**/*.css"
9
+ ],
8
10
  "files": [
9
11
  "dist"
10
12
  ],
@@ -18,13 +20,14 @@
18
20
  "types": "./dist/index.d.cts",
19
21
  "default": "./dist/index.cjs"
20
22
  }
21
- }
23
+ },
24
+ "./styles.css": "./dist/styles.css"
22
25
  },
23
26
  "main": "./dist/index.cjs",
24
27
  "module": "./dist/index.mjs",
25
28
  "types": "./dist/index.d.cts",
26
29
  "dependencies": {
27
- "@poly-x/core": "0.1.0-alpha.2"
30
+ "@poly-x/core": "0.1.0-alpha.4"
28
31
  },
29
32
  "peerDependencies": {
30
33
  "react": ">=18",
@@ -51,7 +54,7 @@
51
54
  "react-dom": "^19.2.7"
52
55
  },
53
56
  "scripts": {
54
- "build": "tsdown",
57
+ "build": "tsdown && node scripts/copy-styles.mjs",
55
58
  "test": "vitest run",
56
59
  "lint": "eslint src"
57
60
  }