jengacore.shadcn registry · Tailwind v4 · Radix and Base UI
Components
Blocks · @jengacore/sign-in

Sign-in

The shared sign-in: navy brand panel with the four products, environment choice, email and password, single sign-on.

jengacore.
Digital public infrastructure

Building digital public infrastructure for an inclusive future.

  • Monitoring
  • Core banking
  • Finance
  • Disputes
finance portal.

Sign in

Settlement, positions and participant finance for the hub.

Environment
Forgot?
or

Access is logged and reviewed. Ask your administrator for an account.

v2.4.0 · PROD · jengacore

Install

terminal
npx shadcn@latest add @jengacore/sign-in

Without the namespace configured: npx shadcn@latest add https://ui.jengacore.com/r/<new-york-v4|base-nova>/sign-in.json

Source

components/blocks/sign-in.tsx
"use client"

import * as React from "react"
import { Info, Lock, LogIn } from "lucide-react"

import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { FormField } from "@/components/jc/form-field"
import { Segmented } from "@/components/jc/segmented"
import { PRODUCT_NAME, PRODUCT_TILE, type Product } from "@/components/jc/theme"
import { cn } from "@/lib/utils"

const PRODUCTS: { key: Product; label: string; file: string }[] = [
  { key: "mulisa", label: "Monitoring", file: "mulisa" },
  { key: "msingi", label: "Core banking", file: "msingi" },
  { key: "finance", label: "Finance", file: "finance-portal" },
  { key: "disputes", label: "Disputes", file: "disputes" },
]

const ENV_LABEL = { prod: "Production", uat: "UAT", sandbox: "Sandbox", dev: "Dev" } as const
type Env = keyof typeof ENV_LABEL

/**
 * The shared sign-in: the brand panel on navy (diagonal hairlines, the jengacore. wordmark, the four products
 * with the current one marked) and the form on white. Wire `onSubmit` to the app's credentials provider and
 * `onSso` to its OIDC provider; leave either out to hide it.
 */
export function SignIn({
  product,
  description,
  environments,
  defaultEnvironment,
  onSubmit,
  onSso,
  ssoLabel = "Continue with single sign-on",
  forgotHref,
  error,
  notice,
  version,
}: {
  product: Product
  /** One sentence: what this product is for. */
  description: string
  environments?: Env[]
  defaultEnvironment?: Env
  onSubmit?: (values: { email: string; password: string; environment?: Env }) => void | Promise<void>
  /** May return a promise: the button shows "Redirecting…" and stays disabled until it settles. */
  onSso?: () => void | Promise<void>
  ssoLabel?: string
  /** Informational line above the form ("Invitation accepted. Sign in to continue."). Errors go in `error`. */
  notice?: string
  forgotHref?: string
  /** Shown under the form: say how to fix it ("Check the email and password, then try again."). */
  error?: string
  /** Footer line, e.g. "v2.4.0 · PROD". */
  version?: string
}) {
  const [env, setEnv] = React.useState<Env | undefined>(defaultEnvironment ?? environments?.[0])
  const [pending, setPending] = React.useState(false)
  const [ssoPending, setSsoPending] = React.useState(false)

  async function sso() {
    if (!onSso) return
    setSsoPending(true)
    try {
      await onSso()
    } finally {
      setSsoPending(false)
    }
  }
  const file = PRODUCTS.find((p) => p.key === product)!.file

  async function submit(e: React.FormEvent<HTMLFormElement>) {
    e.preventDefault()
    if (!onSubmit) return
    const data = new FormData(e.currentTarget)
    setPending(true)
    try {
      await onSubmit({ email: String(data.get("email") ?? ""), password: String(data.get("password") ?? ""), environment: env })
    } finally {
      setPending(false)
    }
  }

  return (
    <div data-slot="sign-in" className="grid min-h-svh lg:grid-cols-[minmax(0,760px)_1fr]">
      <section className="relative flex flex-col justify-between overflow-hidden bg-navy-950 px-14 py-12 text-white max-lg:hidden">
        <svg className="pointer-events-none absolute inset-0 h-full w-full" aria-hidden preserveAspectRatio="none" viewBox="0 0 760 900">
          {[0, 1, 2, 3, 4].map((i) => (
            <line key={i} x1={340 + i * 90} y1={0} x2={760} y2={420 - i * 90 * (420 / 420)} stroke="white" strokeOpacity={0.55} strokeWidth={0.8} />
          ))}
        </svg>
        <img src="/brand/jengacore-wordmark-rule-white.svg" alt="jengacore." className="relative h-10 w-auto self-start" />
        <div className="relative max-w-[520px]">
          <div className="font-mono text-[11px] tracking-[0.16em] text-emerald-400 uppercase">Digital public infrastructure</div>
          <p className="mt-4 text-[52px] leading-[1.02] font-extrabold tracking-[-0.01em]">
            Building digital public infrastructure for an <span className="text-emerald-400">inclusive</span> future.
          </p>
        </div>
        <ul className="relative flex flex-wrap gap-3" aria-label="jengacore products">
          {PRODUCTS.map((p) => (
            <li
              key={p.key}
              aria-current={p.key === product ? "true" : undefined}
              className={cn(
                "flex items-center gap-2.5 rounded-md border px-2.5 py-2 text-[13px] text-grey-200",
                p.key === product ? "border-emerald-400/70 bg-white/[0.04]" : "border-white/10"
              )}
            >
              <img src={PRODUCT_TILE[p.key]} alt="" className="size-6 rounded-xs" />
              {p.label}
            </li>
          ))}
        </ul>
      </section>

      <section className="flex items-center justify-center bg-background px-6 py-12">
        <div className="w-full max-w-[380px]">
          <img src={`/brand/${file}-wordmark.svg`} alt={PRODUCT_NAME[product]} className="h-7 w-auto dark:hidden" />
          <img src={`/brand/${file}-wordmark-white.svg`} alt={PRODUCT_NAME[product]} className="hidden h-7 w-auto dark:block" />
          <h1 className="mt-10 text-[26px] leading-[34px] font-semibold">Sign in</h1>
          <p className="mt-1 text-[13.5px] text-muted-foreground">{description}</p>
          {notice ? (
            <p role="status" className="mt-5 flex items-start gap-2 rounded-md bg-info-subtle px-3 py-2.5 text-[13px] text-foreground">
              <Info className="mt-px size-4 shrink-0 stroke-[1.75] text-info" aria-hidden />
              {notice}
            </p>
          ) : null}

          <form className="mt-6 grid gap-4" onSubmit={submit}>
            {environments?.length && env ? (
              <div className="grid gap-1.5">
                <span className="text-xs font-medium" id="jc-env-label">
                  Environment
                </span>
                <Segmented
                  label="Environment"
                  value={env}
                  onValueChange={setEnv}
                  size="lg"
                  className="w-full"
                  options={environments.map((e) => ({ value: e, label: ENV_LABEL[e] }))}
                />
              </div>
            ) : null}
            {onSubmit ? (
              <>
                <FormField label="Work email" htmlFor="jc-email">
                  <Input id="jc-email" name="email" type="email" autoComplete="username" required />
                </FormField>
                <div className="grid gap-1.5">
                  <div className="flex items-baseline justify-between">
                    <label htmlFor="jc-password" className="text-xs font-medium">
                      Password
                    </label>
                    {forgotHref ? (
                      <a href={forgotHref} className="text-xs text-primary hover:underline">
                        Forgot?
                      </a>
                    ) : null}
                  </div>
                  <Input id="jc-password" name="password" type="password" autoComplete="current-password" required aria-invalid={error ? true : undefined} />
                </div>
                <Button type="submit" size="lg" disabled={pending} className="w-full">
                  <LogIn />
                  {pending ? "Signing in…" : env ? `Sign in to ${ENV_LABEL[env]}` : "Sign in"}
                </Button>
              </>
            ) : null}
            {error ? (
              <p role="alert" className="text-[13px] text-destructive">
                {error}
              </p>
            ) : null}
          </form>

          {onSso ? (
            <>
              {onSubmit ? (
                <div className="my-6 flex items-center gap-3 font-mono text-[10.5px] tracking-[0.14em] text-muted-foreground uppercase before:h-px before:flex-1 before:bg-border after:h-px after:flex-1 after:bg-border">
                  or
                </div>
              ) : (
                <div className="mt-6" />
              )}
              <Button type="button" variant={onSubmit ? "outline" : "default"} size="lg" className="w-full" onClick={sso} loading={ssoPending}>
                {ssoPending ? null : <Lock />}
                {ssoPending ? "Redirecting…" : ssoLabel}
              </Button>
            </>
          ) : null}

          <p className="mt-6 text-[12.5px] text-muted-foreground">Access is logged and reviewed. Ask your administrator for an account.</p>
          {version ? <p className="mt-4 font-mono text-[11px] tracking-[0.08em] text-muted-foreground">{version} · jengacore</p> : null}
        </div>
      </section>
    </div>
  )
}