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

@planetlogin/planetlogin

v0.4.7

Published

Sign in with a planet — interactive globe locale picker that detects timezone, language and location. Framework-agnostic (Web Component).

Readme

PlanetLogin

Sign in with a planet. A framework-agnostic globe component that detects a visitor's timezone, language and location — so your app greets them in their tongue, in their hours, from the first second.

npm downloads license CI bundle stars

Live demo → planetlogin.org · npm i @planetlogin/planetlogin


Spin the globe, drag it (with inertia), zoom with the wheel, click a country, or search a postal code / city / country. PlanetLogin flies there and emits a locale. Works in React, Vue, Svelte, Angular or plain HTML — it ships as a standard Web Component, a class, and a factory.

  • 🌍 Real orthographic globe (d3-geo) — correct hemisphere clipping, drag, inertia, zoom.
  • 🗣️ Detects timezone + language + country from a single pick.
  • 🔑 No API key, no tracking — geocoding via Open-Meteo + OSM Nominatim.
  • 📦 ~17 kB gzip, zero peer deps, TypeScript types included.
  • 🧩 Web Component → drops into any framework or plain HTML.

More than a globe? PlanetLogin is also a stateless auth service (@planetlogin/core) — password, OAuth, magic links, passkeys, TOTP, guest sessions. Jump to it ↓

Install

npm i @planetlogin/planetlogin

…or drop it in with a <script> (no build step):

<planet-login style="display:block;width:100%;height:480px"></planet-login>
<script type="module" src="https://cdn.jsdelivr.net/npm/@planetlogin/planetlogin"></script>

Quickstart

Plain HTML / any framework (Web Component)

<planet-login accent="#f6a13c" resolution="110m"></planet-login>
<script type="module">
  import '@planetlogin/planetlogin';
  document.querySelector('planet-login').addEventListener('locale', (e) => {
    const { language, timezone, country, lat, lon, label } = e.detail;
    setAppLanguage(language);
    setAppTimezone(timezone);
  });
</script>

Function API (full control, typed)

import { createPlanetLogin, type PlanetLocale } from '@planetlogin/planetlogin';

const globe = createPlanetLogin(document.getElementById('globe')!, {
  accent: '#f6a13c',
  onLocale: (loc: PlanetLocale) => console.log(loc),
});

globe.flyTo(2.17, 41.39);   // fly to Barcelona and pick it
globe.search('Tokyo');      // or search by name / postal code
// globe.destroy();         // tear down when done

React

import { useEffect, useRef } from 'react';
import { createPlanetLogin, type PlanetLocale } from '@planetlogin/planetlogin';

export function GlobeLogin({ onLocale }: { onLocale: (l: PlanetLocale) => void }) {
  const ref = useRef<HTMLDivElement>(null);
  useEffect(() => {
    const globe = createPlanetLogin(ref.current!, { onLocale });
    return () => globe.destroy();
  }, [onLocale]);
  return <div ref={ref} style={{ width: '100%', height: 480 }} />;
}

Vue 3

<script setup lang="ts">
import { onMounted, onBeforeUnmount, ref } from 'vue';
import { createPlanetLogin, type PlanetLogin } from '@planetlogin/planetlogin';

const el = ref<HTMLElement>();
let globe: PlanetLogin;
onMounted(() => { globe = createPlanetLogin(el.value!, { onLocale: (l) => emit('locale', l) }); });
onBeforeUnmount(() => globe?.destroy());
const emit = defineEmits<{ locale: [unknown] }>();
</script>

<template><div ref="el" style="width:100%;height:480px" /></template>

Svelte

<script lang="ts">
  import { onDestroy } from 'svelte';
  import { createPlanetLogin, type PlanetLogin, type PlanetLocale } from '@planetlogin/planetlogin';
  let el: HTMLElement;
  let globe: PlanetLogin;
  export let onlocale: (l: PlanetLocale) => void = () => {};
  $: if (el && !globe) globe = createPlanetLogin(el, { onLocale: onlocale });
  onDestroy(() => globe?.destroy());
</script>

<div bind:this={el} style="width:100%;height:480px"></div>

Options

| Option | Type | Default | | |---|---|---|---| | accent | string | #f6a13c | Brand color (highlight, button). | | search | boolean | true | Show the built-in search box. | | placeholder | string | Postal code, city or country… | Search box placeholder. | | autoSpin | boolean | true | Gentle rotation until the first pick. | | resolution | '110m' \| '50m' | '110m' | Border detail (50m = sharper, heavier). | | dataUrl | string | world-atlas CDN | Override the country TopoJSON URL. | | onLocale | (l: PlanetLocale) => void | — | Callback on every pick. | | remember | boolean | false | Persist the picked locale to browser storage (see Locale memory). | | flyToSaved | boolean | false | On mount, fly to the remembered locale and re-emit it. | | storageKey | string | planetlogin:locale | Storage key for the remembered locale. | | storage | 'local' \| 'session' \| 'none' | 'local' | Where to persist (or disable). |

The Web Component mirrors these as attributes: accent, resolution, search, placeholder, autospin, remember, fly-to-saved, storage-key, storage.

Methods

| Method | | |---|---| | on('locale', cb) | Add a listener fired on every pick. Returns this. | | flyTo(lon, lat) | Animate to coordinates and pick them. | | search(query) | Geocode a string and fly to the result (async). | | getSavedLocale() | The locale remembered on this device, or null. | | clearSavedLocale() | Forget the remembered locale on this device. | | destroy() | Stop animation and remove all DOM it created. |

The locale payload

Delivered three ways — the locale DOM CustomEvent (bubbles), the on('locale', …) listener, and the onLocale option:

interface PlanetLocale {
  lat: number; lon: number;
  country: string;        // ISO 3166-1 alpha-2, uppercase
  timezone: string;       // IANA ("Europe/Madrid") or approximate "UTC±N"
  language: string;       // BCP-47-ish, e.g. "es"
  label: string;          // "Barcelona, Spain"
  approxTimezone?: boolean; // true when tz is a longitude estimate, not IANA
}

Locale memory

Opt-in, device-local, zero backend: the globe can remember where the user picked and fly back to it next time. Both gates are off by default (privacy-first).

<planet-login remember fly-to-saved></planet-login>
// Or with the function API:
const planet = createPlanetLogin(el, { remember: true, flyToSaved: true });
planet.getSavedLocale();   // → PlanetLocale | null
planet.clearSavedLocale(); // forget it

Read or write the saved value without an instance (e.g. to pre-fill a form on a static page) — these are pure helpers over localStorage, never throw:

import { readSavedLocale, writeSavedLocale, clearSavedLocale } from '@planetlogin/planetlogin';
const saved = readSavedLocale(localStorage); // PlanetLocale | null

Per-account memory (survives devices, flies to the user's place on login) needs a backend: run a PlanetLogin auth flavor with a downstream store and turn on config.locale.persist / flyToOnLogin. See the SPEC.

How it works

A real orthographic globe via d3-geo (proper hemisphere clipping, not a flat map). Geocoding with no API key — Open-Meteo gives a precise IANA timezone, with an OSM Nominatim fallback for postal codes and reverse lookups. Country borders come from world-atlas, fetched at runtime from a CDN.

More than a globe — a full auth service

The globe is the face. The project behind it is @planetlogin/core: a framework-agnostic, stateless login service — password, OAuth/OIDC, magic links, passkeys, TOTP and anonymous guest sessions. It signs an asymmetric JWT (EdDSA) and publishes a JWKS, and it keeps no database of its own — persistence is your store over REST (or in-process), or none at all for guest sessions. Your app stays the source of truth.

import { passwordLogin, verifyPassword, signSession } from '@planetlogin/core';

// Stateless login — no session table inside PlanetLogin:
const result = await passwordLogin(
  { downstream, verifyPassword, signSession },
  { identifier: email, password },
);
// → { ok: true, token } — a signed JWT your app verifies via JWKS, offline.

Ship it as a flavor — the same app in every runtime, kept honest by a black-box conformance suite that any flavor must pass. Need real accounts with zero downstream code? Drop in a batteries-included store.

| Package | | |---|---| | @planetlogin/core | the framework-agnostic auth core — flows, JWT/JWKS, downstream contract, pluggable session store | | @planetlogin/planetlogin | the globe Web Component (this package) | | @planetlogin/store-sqlite | batteries-included accounts on node:sqlite — one file, zero backend code | | @planetlogin/store-postgres | the same, over any pg-compatible client |

Flavors: flavors/svelte (SvelteKit — reference) · flavors/vanilla (plain Node). More welcome.

Run your own in one container from the prebuilt image — design it in the white-label studio (it exports the config

docker run -p 3000:3000 --env-file .env ghcr.io/planetlogin/portal:latest

The whole idea: whitepaper · SPEC · integration guide.

Develop

npm i
npm run dev        # demo at localhost:5173
npm run build      # dist/ (ESM + UMD + .d.ts)
npm run typecheck
npm test           # unit tests

This is a monorepo (npm workspaces): the globe component at the root, plus packages/* (core + store adapters) and flavors/*. Branches: main = the monorepo · simple = a zero-build, single-file globe to copy-paste (what serves planetlogin.org).

See CONTRIBUTING.md — issues and PRs welcome.

License & attribution

AGPL-3.0 with an attribution term (AGPLv3 §7b).

  • Free for any use, including commercial.
  • Modify / host a modified version → share your source under the same license (copyleft).
  • Keep the visible PlanetLogin · by Ricajos credit (→ ricajos.com). Removing it needs written permission — a commercial / white-label license is available, open an issue.

Credits

Created by Ricard (Ricajos) · @rricajos. Born as the onboarding for a booking product of his own, released standalone as a community project.