npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@just-apps/auth

v0.4.2

Published

Just Apps shared auth UI components (login, terms agreement, my page, user menu, account delete) — framework-agnostic, props-only, Tailwind token driven.

Readme

@just-apps/auth

Just Apps shared authentication UI component library. A pure presentational library that the Next.js homepage and Tauri desktop apps use to share auth screens — login, terms agreement, my page, account deletion, user menu, and so on.

This package contains no authentication logic. The Supabase client, session store, OAuth flow, cookie/storage handling, and so on are all implemented by the consuming app (Next.js, Tauri) and injected as props. That means SSO-style session-sharing issues can't occur, and the package has no framework dependencies.


1. Overview

What's included

  • 8 React components — login, terms agreement (view + card), my page, account deletion, OAuth callback, user menu, login button
  • 4 UI primitivesButton, Spinner, GoogleIcon, MarbleAvatar
  • TypesLocale, Theme, AuthUser, TermItem, TranslationOverrides
  • i18n — built-in t() function + ko-KR / en-US translation dictionaries (overridable)
  • Utilitiescn (Tailwind class merge)

What's not included

  • ❌ Supabase client / @supabase/supabase-js calls
  • ❌ Session management (Zustand store, localStorage, cookies)
  • ❌ OAuth redirect / callback handling
  • ❌ DB schema-dependent queries such as user_agreements
  • ❌ Next.js server utilities / route handlers
  • next/* module imports (for Tauri/Vite compatibility)

Design principles

  1. Props-only — all state and behavior is injected via props (user, onGoogleLogin, onSignOut, etc.)
  2. Framework-agnostic — no next/link, next/navigation, next/image, etc. When needed, delegate via a callback prop (onRoute(destination), logoHref, etc.)
  3. DI first — never directly import the Supabase client, router, or translation function
  4. Slot pattern — app-specific areas such as TermsAgreementView.headerSlot are received as ReactNode
  5. "use client" — every component has the "use client" directive. Next recognizes it, Vite treats it as a no-op → compatible with both

2. Installation

pnpm workspace (current monorepo)

Root package.json:

{
  "dependencies": {
    "@just-apps/auth": "workspace:*"
  }
}

For a Next.js app, in next.config.ts:

const nextConfig: NextConfig = {
  transpilePackages: ["@just-apps/auth"],
  // ...
};

Peer Dependencies

The consuming app must have the following installed:

  • react ^19
  • react-dom ^19
  • lucide-react ^1
  • boring-avatars (when using MarbleAvatar)
  • class-variance-authority, clsx, tailwind-merge (for Button / cn)

Tailwind configuration

⚠️ This step is mandatory. If you skip it the components will render but look broken — padding, background colors, borders, and margins will silently disappear.

Tailwind v4 (and v3) does not scan node_modules by default, so any utility class that is only used inside this package (e.g. bg-accent, rounded-lg, mt-8, border-t) is purged from the final CSS unless you explicitly point Tailwind at the package.

Pick the snippet that matches how you consume the package:

Tailwind v4 — installed from npm (the usual case):

@import "tailwindcss";
@source "../node_modules/@just-apps/auth/dist/**/*.{js,mjs}";

The dist output is produced by tsup and preserves className strings as plain literals, so Tailwind's static scanner can pick them up. Adjust the relative path so it resolves from your CSS file to your project's node_modules.

Tailwind v4 — pnpm workspace (this monorepo):

@import "tailwindcss";
@source "../../packages/auth/src/**/*.{ts,tsx}";

Tailwind v3 — installed from npm:

export default {
  content: [
    "./src/**/*.{ts,tsx}",
    "./node_modules/@just-apps/auth/dist/**/*.{js,mjs}",
  ],
  // ...
};

Tailwind v3 — pnpm workspace:

export default {
  content: [
    "./src/**/*.{ts,tsx}",
    "../../packages/auth/src/**/*.{ts,tsx}",
  ],
  // ...
};

How to tell if you forgot this step: open the rendered auth screen and check whether the "Agree to all" row in TermsAgreementView has a visible rounded accent background and whether there's a horizontal divider between it and the individual terms. If both are missing and everything feels crammed together, you're missing the @source / content entry.

Required Tailwind tokens

The package components use the following CSS variable-based colors. The consuming app's design system must define these tokens:

  • --background, --foreground
  • --card, --card-foreground
  • --primary, --primary-foreground
  • --secondary, --secondary-foreground
  • --accent, --accent-foreground
  • --muted, --muted-foreground
  • --destructive, --destructive-foreground
  • --border, --input, --ring

⚠️ Note: if --secondary and --card are too close in value, region separation disappears in dark mode. Keep a sufficient brightness delta (at least ~3%).


3. Quick Start

import { LoginView } from "@just-apps/auth";
import { useAuth } from "@/stores/useAuth"; // auth store implemented by the app
import { useLocale } from "@/stores/useLocale"; // locale store implemented by the app

export function LoginPage() {
  const signInWithGoogle = useAuth((s) => s.signInWithGoogle);
  const locale = useLocale((s) => s.locale);

  return (
    <LoginView
      locale={locale}
      onGoogleLogin={signInWithGoogle}
    />
  );
}

The core pattern: the package draws the UI, the app injects state and behavior. No reverse dependency.


4. Components API

<LoginView />

Google OAuth login card.

interface LoginViewProps {
  locale: Locale;
  onGoogleLogin: () => void;
  translations?: TranslationOverrides;
}

| Prop | Description | |---|---| | locale | Display language ("ko-KR" / "en-US") | | onGoogleLogin | Called when the Google button is clicked. Inject the app's OAuth flow starter | | translations | (optional) App-specific translation overrides |

Example:

<LoginView
  locale="ko-KR"
  onGoogleLogin={() => supabase.auth.signInWithOAuth({
    provider: "google",
    options: { redirectTo: window.location.origin + "/auth/callback" },
  })}
/>

<LoginButton />

Small "Login" button used in headers and such.

interface LoginButtonProps {
  locale: Locale;
  onClick: () => void;
  translations?: TranslationOverrides;
}

<UserMenu />

Avatar + dropdown menu (my page / admin / sign out).

interface UserMenuProps {
  locale: Locale;
  user: AuthUser;
  role: "admin" | "user";
  onMyPage: () => void;
  onAdmin: () => void;
  onSignOut: () => void;
  translations?: TranslationOverrides;
}

| Prop | Description | |---|---| | user | { id, email?, created_at? } — the app's session user object | | role | "admin" shows the admin menu, otherwise shows the my-page menu | | onMyPage / onAdmin / onSignOut | Click handlers for each menu item |

Behavior:

  • Closes on outside click (mousedown listener)
  • The avatar is generated automatically by MarbleAvatar (hashed from email/ID)

<MyPageView />

My page profile card + sign-out / delete-account buttons.

interface MyPageViewProps {
  locale: Locale;
  user: AuthUser;
  onSignOut: () => void;
  onDeleteAccount: () => void;
  translations?: TranslationOverrides;
}

Displays: email, join date (derived from user.created_at via toLocaleDateString), sign-out button, delete-account link.


<TermsAgreementView />

Terms agreement screen. Rendered as a full-page layout (including header and footer) and customizable via slots.

Internally composes <TermsAgreementCard /> (see below) inside a min-h-screen + header + <main> + footer shell. If you don't need the page shell, import TermsAgreementCard directly instead.

interface TermsAgreementViewProps {
  locale: Locale;
  theme: Theme;
  terms: TermItem[];
  onToggleLocale: () => void;
  onToggleTheme?: () => void;
  onSubmit: (agreed: Record<string, boolean>) => Promise<void>;
  onCancel?: () => void;
  termsViewUrl?: (type: string, locale: Locale) => string;
  logoText?: string;
  logoHref?: string;
  headerSlot?: React.ReactNode;
  footerSlot?: React.ReactNode;
  hideHeader?: boolean;
  hideFooter?: boolean;
  translations?: TranslationOverrides;
}

| Prop | Description | |---|---| | terms | Array of term items. Each item is { id, type, title, required } | | onSubmit | Called on submit. The argument is a { [type]: boolean } map (e.g. { terms_of_service: true, privacy_policy: true }) | | onCancel | (optional) When provided, a "decline and leave" link is shown below the submit button. Wire it to sign-out / navigate-home / whatever the app needs | | termsViewUrl | (optional) URL generator for each term's "view" link | | logoText / logoHref | (optional) Logo in the default header (defaults to "Just Apps" / "/") | | headerSlot / footerSlot | (optional) Replaces the default header/footer | | hideHeader | (optional) If true, the default header (logo + locale/theme toggles) is not rendered at all. Useful for desktop apps that have their own title bar. Default false | | hideFooter | (optional) If true, the default © footer is not rendered. Default false | | onToggleTheme | (optional) Omit to hide the theme toggle button (for forced-theme scenarios) |

Default behavior:

  • "Agree to all" checkbox → toggles every item
  • The submit button activates only when all required terms are checked
  • A loading spinner is shown during submission

<TermsAgreementCard />

Just the terms agreement card — no page layout, no header, no footer. All the checkbox state, "agree to all" logic, required-terms validation, and submission flow lives here; TermsAgreementView is a thin wrapper that composes this card inside a full-page shell.

Use this directly when you need the card in your own layout — e.g. a desktop app with its own title bar, a modal dialog, or a custom multi-column page.

interface TermsAgreementCardProps {
  locale: Locale;
  terms: TermItem[];
  onSubmit: (agreed: Record<string, boolean>) => Promise<void>;
  onCancel?: () => void;
  termsViewUrl?: (type: string, locale: Locale) => string;
  translations?: TranslationOverrides;
  className?: string;
}

| Prop | Description | |---|---| | terms / onSubmit / onCancel / termsViewUrl / translations | Same semantics as TermsAgreementView | | className | (optional) Extra classes merged onto the card's outer <div>. Use this to override width, margin, background, etc. Default base classes are w-full max-w-md rounded-xl border border-border bg-card p-8 |

Example (desktop app, custom layout):

import { TermsAgreementCard } from "@just-apps/auth";

export function DesktopTermsScreen() {
  return (
    <div className="flex items-center justify-center p-6">
      <TermsAgreementCard
        locale="ko-KR"
        terms={terms}
        onSubmit={handleSubmit}
        onCancel={handleDecline}
      />
    </div>
  );
}

<AccountDeleteView />

Account deletion warning + two-step confirmation UI.

interface AccountDeleteViewProps {
  locale: Locale;
  user: AuthUser | null;
  onGoogleLogin: () => void;
  onDelete: () => Promise<void>;
  onGoHome: () => void;
  translations?: TranslationOverrides;
}

State machine:

  1. user === null → login prompt + Google login button
  2. Signed in → "Delete account" button
  3. Click → "Are you sure you want to delete?" confirmation dialog
  4. Confirm → calls onDelete(), shows loading state, logs errors on failure
  5. Success → "Account deleted" + "Go home" button

<AuthCallbackView />

OAuth callback waiting screen + automatic route trigger.

interface AuthCallbackViewProps {
  loading: boolean;
  user?: AuthUser | null;
  isNewUser?: boolean;
  onRoute: (destination: "login" | "terms" | "home") => void;
}

Logic:

  • Shows a spinner while loading
  • Once loading finishes, a useEffect calls onRoute(destination):
    • !user"login"
    • isNewUser"terms"
    • otherwise → "home"

Inside onRoute, the app performs the actual navigation with its own router (Next's useRouter, react-router, etc.).


5. UI Primitives

<Button />

shadcn-style button. 5 variants × 4 sizes.

import { Button, buttonVariants, type ButtonProps } from "@just-apps/auth";

<Button variant="outline" size="lg" onClick={...}>...</Button>

Variants: default | destructive | outline | ghost | link Sizes: default | sm | lg | icon

The buttonVariants cva function is also exported so other components can reuse the styles.

<Spinner />

Loading spinner (a wrapper around lucide-react's Loader2).

<Spinner size="lg" className="text-primary" />

Sizes: sm (16px) | md (24px, default) | lg (32px)

<GoogleIcon />

Google logo SVG (the official 4 colors). Size is controlled via the className prop (defaults to h-5 w-5).

<MarbleAvatar />

Hash avatar based on boring-avatars.

<MarbleAvatar name={`justapps:${user.email ?? user.id}`} size={32} />
  • Same name → same avatar every time (deterministic)
  • Fixed 5-color palette: #818CF8 #C084FC #F472B6 #34D399 #60A5FA
  • The justapps: prefix is recommended (prevents collisions with other services that share the same email)

6. Types

export type Locale = "ko-KR" | "en-US";
export type Theme = "light" | "dark";

export interface AuthUser {
  id: string;
  email?: string | null;
  created_at?: string;
}

export interface TermItem {
  id: string;
  type: string;       // e.g. "terms_of_service", "privacy_policy", "marketing"
  title: string;
  required: boolean;
  content?: string;
}

export type TranslationOverrides = Partial<
  Record<string, Record<Locale, string>>
>;

AuthUser is intentionally narrowed to be structurally compatible with Supabase's User type. You can pass a user object pulled straight from a Supabase session.


7. i18n

The t() function

import { t } from "@just-apps/auth";

t("login.title", "ko-KR");        // "로그인"
t("login.title", "en-US");        // "Login"
t("missing.key", "ko-KR");        // "missing.key" (fallback: the key itself)

Translation overrides

When you want to change only a specific key's copy for one app:

<LoginView
  locale="ko-KR"
  onGoogleLogin={...}
  translations={{
    "login.title": {
      "ko-KR": "Just Cut에 오신 것을 환영합니다",
      "en-US": "Welcome to Just Cut",
    },
  }}
/>

Overridden keys apply only within that component tree (not globally).

Full list of translation keys

| Namespace | Keys | |---|---| | login.* | title, subtitle, google | | terms.* | title, subtitle, agree_all, required, optional, agree_suffix, view, submit, decline | | mypage.* | title, email, joined, delete_account, logout | | delete.* | title, subtitle, login_required, login, data_title, data_email, data_agreements, data_activity, warning, confirm_button, confirm_title, confirm_message, confirm_yes, confirm_cancel, processing, success | | usermenu.* | admin, admin_badge, mypage, logout | | common.* | loading, auth_processing, login, go_home | | footer.* | copyright |

For type-safe keys:

import type { TranslationKey } from "@just-apps/auth";

8. App integration guide

Next.js (this repo)

The homepage's current adapter pattern:

src/components/common/UserMenu.tsx — a thin wrapper that injects app state:

'use client';

import { useRouter } from "@/lib/navigation";
import { UserMenu as UserMenuView } from "@just-apps/auth";
import { useAuth } from "@/stores/useAuth";
import { useLocale } from "@/stores/useLocale";

export function UserMenu() {
  const router = useRouter();
  const { user, role, signOut } = useAuth();
  const locale = useLocale((s) => s.locale);

  if (!user) return null;

  return (
    <UserMenuView
      locale={locale}
      user={user}
      role={role === "admin" ? "admin" : "user"}
      onMyPage={() => router.push("/mypage")}
      onAdmin={() => router.push("/admin")}
      onSignOut={() => signOut()}
    />
  );
}

Principle: the page shell (routing, session access, header/footer assembly) lives in the app; the presentational shell lives in the package.

Tauri / Vite apps

Fundamentally the same pattern as Next:

// packages/xxx-tauri-app/src/pages/Login.tsx
import { LoginView } from "@just-apps/auth";
import { supabase } from "./lib/supabase"; // the Tauri app's own Supabase client

export function LoginPage() {
  return (
    <LoginView
      locale="ko-KR"
      onGoogleLogin={async () => {
        // Loopback OAuth flow for Tauri
        await startLoopbackOAuth();
      }}
    />
  );
}

Caveats:

  1. The Tauri app's Tailwind config must include the package path
  2. OAuth redirect can't use window.location.origin + "/auth/callback" like Next does → use a loopback server / deep link
  3. The package has zero next/* imports, so Tauri builds work fine

9. FAQ

Q. Can the package hold the Supabase client? A. No. The moment the package imports @supabase/supabase-js, each app gets a duplicate instance, the session singleton breaks, and framework dependencies like an SSR cookie adapter creep in. An earlier attempt had to be fully rolled back because of this (9c1f98e). Each app should create its own client and inject it via props/callbacks.

Q. Can hooks like useAuth live in the package? A. Auth stores have different schemas per app (user_agreements table, role column, terms rules, etc.). Generalizing them would require DI for everything, which isn't worth it. Implement them per app.

Q. My dark mode colors look wrong. A. This package depends on the consuming app's design system for Tailwind CSS tokens (--card, --secondary, etc.). In particular, if --secondary and --card are close in value, regions become indistinguishable. Adjust your app's globals.css dark tokens or tweak the Tailwind classes on the affected component.

Q. Do I need the "use client" directive in Tauri too? A. It isn't needed but it's harmless. Vite ignores the directive and Next requires it, so it stays for dual compatibility.

Q. What about adding a new language beyond i18n overrides? A. The Locale type is currently hardcoded to "ko-KR" | "en-US". Adding a new language requires changes inside the package itself.


10. Versioning / changelog

Currently 0.0.0 (private workspace only). External npm publishing is not yet supported. Versioning policy / changelog will be introduced later in Phase 7.


11. Related docs

  • docs/ROADMAP_PACKAGE_MIGRATION.md — packaging roadmap & design decisions
  • packages/subscription/README.md — subscription UI package (sibling package)
  • README.md (root) — overall monorepo structure