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

@lithium-ts/core

v0.1.0

Published

A lightweight, modern web framework built on top of [Lit](https://lit.dev) with reactive state management, dependency injection, and powerful routing.

Downloads

280

Readme

🔋 LithiumJS

A lightweight, modern web framework built on top of Lit with reactive state management, dependency injection, and powerful routing.

Table of Contents


Installation

npm install @lithium-ts/core

Core Concepts

LithiumElement

The base class for all components. Extends Lit's LitElement with reactive signals and EventBus integration.

import { LithiumElement, defineElement, html } from "@lithium-ts/core";

@defineElement({ tag: "my-button" })
export class MyButton extends LithiumElement {
  render() {
    return html`<button><slot></slot></button>`;
  }
}

Key Methods

| Method | Description | | --------------------------- | ------------------------------------ | | channel(name, config) | Create/access a reactive channel | | publish(name, data) | Publish data to a channel | | subscribe(name, callback) | Subscribe to channel changes | | emit(event, data) | Emit instant event (fire-and-forget) | | on(event, callback) | Listen to instant events | | navigate(path, options) | Navigate to a route | | output(event, detail) | Emit a DOM CustomEvent | | clear(name) | Remove a channel | | clearAll(storage?) | Remove all channels |


Pages

Pages are components with automatic title management. The tag name is auto-generated from the class name.

import { LithiumElement, definePage, html } from "@lithium-ts/core";
import styles from "./home.page.css?inline";

@definePage({
  title: "Home - My App",
  styles: [styles],
})
export class HomePage extends LithiumElement {
  render() {
    return html`
      <h1>Welcome!</h1>
      <button @click=${() => this.navigate("/about")}>Go to About</button>
    `;
  }
}
// Auto-registered as <home-page>

Modules

Modules group routes with optional layouts. Perfect for feature modules (admin, auth, etc.).

import { LithiumModule, defineModule, html } from "@lithium-ts/core";

const adminRoutes = [
  { path: "/", render: () => html`<admin-dashboard></admin-dashboard>` },
  { path: "/users", render: () => html`<admin-users></admin-users>` },
];

@defineModule({ routes: adminRoutes })
export class AdminModule extends LithiumModule {
  // Optional: Custom layout
  render() {
    return html`
      <nav>Admin Navigation</nav>
      <main>${this._routes.outlet()}</main>
    `;
  }
}
// Auto-registered as <admin-module>

LithiumApp

The root application component with main routing.

import { LithiumApp, defineApp, html } from "@lithium-ts/core";

const routes = [
  { path: "/", render: () => html`<home-page></home-page>` },
  { path: "/about", render: () => html`<about-page></about-page>` },
  { path: "/admin/*", render: () => html`<admin-module></admin-module>` },
];

@defineApp({
  tag: "main-app",
  routes,
})
export class MainApp extends LithiumApp {
  render() {
    return html`
      <header>My App</header>
      ${this._router.outlet()}
      <footer>© 2026</footer>
    `;
  }
}

State Management (EventBus)

LithiumJS uses a reactive EventBus with Signals for state management.

Channels

Channels are reactive state containers that automatically update components.

// In any component
export class MyComponent extends LithiumElement {
  // Create a channel (reactive!)
  private user = this.channel<User>("user:current", {
    initialValue: null,
    storage: "session",
  });

  render() {
    const user = this.user.get(); // Reactive - auto re-renders on change
    return html`<p>Hello, ${user?.name ?? "Guest"}</p>`;
  }

  login(userData: User) {
    this.publish("user:current", userData); // All subscribers update
  }
}

Channel Configuration

this.channel<T>("channel-name", {
  // Value
  initialValue: null,

  // Storage type
  storage: "memory" | "session" | "local" | "page",
  storageKey: "custom-key", // Default: lithium:channel-name

  // Auto-cleanup
  ttl: 60000, // Time-to-live in ms
  autoCleanup: true, // Delete when no subscribers

  // For 'page' storage only
  persist: true, // Survive page refresh

  // Lifecycle hooks
  onChange: (newVal, oldVal) => console.log("Changed"),
  onInit: (value) => console.log("First value set"),
  onClear: () => console.log("Channel cleared"),

  // Transformation
  validate: (value) => value.id > 0, // Return false to cancel update
  transform: (value) => normalize(value),

  // Rate limiting
  debounce: 500, // Debounce onChange
  throttle: 1000, // Throttle onChange
});

Storage Types

| Type | Persistence | Use Case | | --------- | -------------------- | ----------------------- | | memory | Lost on refresh | Temporary UI state | | session | Browser tab lifetime | User session data | | local | Forever | User preferences | | page | Until route change | Form drafts, page state |

Page Storage Example

// Form data that clears when navigating away
private formDraft = this.channel('form:draft', {
  initialValue: { name: '', email: '' },
  storage: 'page',
  persist: false  // Clears on navigation
});

// Notes that survive navigation but still page-scoped
private notes = this.channel('page:notes', {
  initialValue: '',
  storage: 'page',
  persist: true  // Survives navigation, stored in sessionStorage
});

Instant Events

Fire-and-forget events without persistence.

// Emit (sender)
this.emit('toast:show', {
  message: 'Saved!',
  type: 'success'
});

// Listen (receiver)
connectedCallback() {
  super.connectedCallback();
  this.on('toast:show', (data) => {
    this.showToast(data.message, data.type);
  });
}

Middleware

Intercept all EventBus actions for logging, validation, etc.

import { EventBus } from "@lithium-ts/core";

// Add middleware
EventBus.use((action, channelOrEvent, data, next) => {
  console.log(`[${action}] ${channelOrEvent}:`, data);
  next(); // Must call next() to continue
});

// Enable dev mode for detailed logging
EventBus.setDevMode(true);

Routing

LithiumRouter

A wrapper component that intercepts navigation for guards.

import { LithiumApp, defineApp, html } from "@lithium-ts/core";

@defineApp({ tag: "main-app", routes })
export class MainApp extends LithiumApp {
  render() {
    return html`
      <lithium-router
        .beforeRoute=${this.authGuard}
        .afterRoute=${this.analytics}
      >
        ${this._router.outlet()}
      </lithium-router>
    `;
  }
}

Route Guards

Guards control access to routes.

beforeRoute

Runs before navigation. Can block or redirect.

import { RouteLocation, RouteGuardResult } from "@lithium-ts/core";

const authGuard = async (
  to: RouteLocation,
  from: RouteLocation,
): Promise<boolean | RouteGuardResult> => {
  const isAuthenticated = !!localStorage.getItem("token");

  // Protected routes
  if (to.path.startsWith("/admin") && !isAuthenticated) {
    return { continue: false, redirect: "/login" };
  }

  // Allow navigation
  return true;
};

afterRoute

Runs after successful navigation. For analytics, logging, etc.

const analyticsGuard = (to: RouteLocation, from: RouteLocation) => {
  analytics.track("page_view", {
    path: to.path,
    referrer: from.path,
  });
};

RouteLocation Object

interface RouteLocation {
  path: string; // /users/123
  query: Record<string, string>; // { tab: 'settings' }
  hash: string; // 'section-1'
  fullPath: string; // /users/123?tab=settings#section-1
}

Programmatic Navigation

Navigate from anywhere using static methods or component methods.

import { LithiumRouter } from "@lithium-ts/core";

// From anywhere (static)
LithiumRouter.navigate("/dashboard");
LithiumRouter.navigate("/search", { query: { q: "hello" } });
LithiumRouter.navigate("/docs", { hash: "installation" });
LithiumRouter.navigate("/login", { replace: true }); // No history entry
LithiumRouter.back();
LithiumRouter.forward();
LithiumRouter.go(-2);

// From a component
class MyComponent extends LithiumElement {
  goToDashboard() {
    this.navigate("/dashboard");
    this.navigate("/profile", { state: { from: "home" } });
  }
}

Decorators

@defer

Loads content after initial render. Perfect for non-critical UI.

import { defer, html } from "@lithium-ts/core";

class MyPage extends LithiumElement {
  // Basic - loads on next microtask
  @defer()
  get analytics() {
    return html`<analytics-widget></analytics-widget>`;
  }

  // With placeholder
  @defer({ placeholder: html`<loading-spinner></loading-spinner>` })
  get charts() {
    return html`<heavy-charts></heavy-charts>`;
  }

  // With delay
  @defer({ delay: 2000 })
  get lowPriority() {
    return html`<recommendations></recommendations>`;
  }

  // With dynamic import (real code-splitting!)
  @defer({
    loader: () => import("./heavy-component.js"),
    placeholder: html`<skeleton-loader></skeleton-loader>`,
  })
  get heavyContent() {
    return html`<heavy-component></heavy-component>`;
  }

  render() {
    return html`
      <main>Important content first</main>
      ${this.analytics} ${this.charts}
    `;
  }
}

@lazy

Loads content when it enters the viewport using IntersectionObserver.

import { lazy, html } from "@lithium-ts/core";

class MyPage extends LithiumElement {
  // Basic lazy loading
  @lazy()
  get comments() {
    return html`<comments-section></comments-section>`;
  }

  // With options
  @lazy({
    threshold: 0.5, // 50% visible
    rootMargin: "100px", // Load 100px before visible
    placeholder: html`<skeleton></skeleton>`,
    minHeight: "200px", // Prevent layout shift
  })
  get heavySection() {
    return html`<data-visualization></data-visualization>`;
  }

  // With dynamic import
  @lazy({
    loader: () => import("./gallery.js"),
    placeholder: html`<gallery-skeleton></gallery-skeleton>`,
  })
  get gallery() {
    return html`<photo-gallery></photo-gallery>`;
  }

  render() {
    return html`
      <main>Above the fold</main>
      <div style="height: 200vh;">Scroll down...</div>
      ${this.comments}
      <!-- Loads when scrolled into view -->
      ${this.gallery}
    `;
  }
}

@conditional

Renders content only when a condition is true. Supports lazy loading.

import { conditional, html } from "@lithium-ts/core";
import { state } from "lit/decorators.js";

class MyPage extends LithiumElement {
  @state() isAdmin = false;
  @state() showAdvanced = false;

  // Basic conditional
  @conditional("isAdmin")
  get adminPanel() {
    return html`<admin-panel></admin-panel>`;
  }

  // With placeholder
  @conditional("showAdvanced", {
    placeholder: html`<p>Enable advanced mode to see more options</p>`,
  })
  get advancedOptions() {
    return html`<advanced-settings></advanced-settings>`;
  }

  // With lazy loading (only imports when condition is true!)
  @conditional("isAdmin", {
    loader: () => import("./admin-dashboard.js"),
    placeholder: html`<p>Admin only</p>`,
  })
  get adminDashboard() {
    return html`<admin-dashboard></admin-dashboard>`;
  }

  render() {
    return html`
      <main>${this.adminPanel}</main>
      <div>${this.advancedOptions}</div>
      <button @click=${() => (this.isAdmin = !this.isAdmin)}>
        Toggle Admin
      </button>
    `;
  }
}

Services & Dependency Injection

Creating Services

Services are singletons with HTTP helpers and EventBus integration.

import {
  LithiumService,
  service,
  inject,
  ServiceInterceptor,
} from "@lithium-ts/core";

// Optional: Service-specific interceptor
const cacheInterceptor: ServiceInterceptor = {
  name: "cache",
  response: (res, data) => {
    localStorage.setItem(`cache:${res.url}`, JSON.stringify(data));
    return data;
  },
};

@service({
  baseUrl: "https://api.example.com",
  headers: { "X-App-Version": "1.0.0" },
  interceptors: [cacheInterceptor],
})
export class UserService extends LithiumService {
  async getUsers(): Promise<User[]> {
    return this.get<User[]>("/users", {
      onSuccess: "users:loaded",
      onError: "users:error",
    });
  }

  async createUser(user: User): Promise<User> {
    return this.post<User>("/users", user);
  }

  async updateUser(id: number, data: Partial<User>): Promise<User> {
    return this.put<User>(`/users/${id}`, data);
  }

  async deleteUser(id: number): Promise<void> {
    return this.delete(`/users/${id}`);
  }
}

Using Services in Components

import { inject } from "@lithium-ts/core";

@definePage({ title: "Users" })
export class UsersPage extends LithiumElement {
  @inject(UserService) private userService!: UserService;

  async connectedCallback() {
    super.connectedCallback();
    const users = await this.userService.getUsers();
    this.publish("users:list", users);
  }
}

HTTP Methods

class MyService extends LithiumService {
  // GET
  getItems() {
    return this.get<Item[]>("/items");
  }

  // POST
  createItem(data: Item) {
    return this.post<Item>("/items", data);
  }

  // PUT
  updateItem(id: number, data: Item) {
    return this.put<Item>(`/items/${id}`, data);
  }

  // PATCH
  patchItem(id: number, data: Partial<Item>) {
    return this.patch<Item>(`/items/${id}`, data);
  }

  // DELETE
  deleteItem(id: number) {
    return this.delete(`/items/${id}`);
  }

  // Custom fetch with options
  customRequest() {
    return this.fetch<Data>("/endpoint", {
      method: "POST",
      body: JSON.stringify(data),
      onSuccess: "data:loaded",
      onError: "data:error",
      transform: async (res) => res.text(), // Custom response transform
      skipGlobalInterceptors: true,
    });
  }
}

Interceptors

Interceptors modify requests/responses globally or per-service.

Global Interceptors

Applied to ALL services.

import { LithiumService } from "@lithium-ts/core";

// Auth interceptor
LithiumService.addInterceptor({
  name: "auth",
  request: (ctx) => {
    const token = localStorage.getItem("token");
    if (token) {
      ctx.options.headers = {
        ...ctx.options.headers,
        Authorization: `Bearer ${token}`,
      };
    }
    return ctx;
  },
  error: (error, ctx) => {
    if (error.status === 401) {
      EventBus.emit("auth:expired");
      LithiumRouter.navigate("/login");
    }
    return error;
  },
});

// Logging interceptor
LithiumService.addInterceptor({
  name: "logging",
  request: (ctx) => {
    console.log(`[API] ${ctx.method} ${ctx.url}`);
    return ctx;
  },
  response: (res, data, ctx) => {
    console.log(`[API] Response:`, data);
    return data;
  },
});

Service-Specific Interceptors

Applied only to one service.

@service({
  baseUrl: "https://api.example.com",
  interceptors: [
    {
      name: "retry",
      error: async (error, ctx) => {
        if (error.status === 503) {
          // Retry logic
          await delay(1000);
          return fetch(ctx.url, ctx.options);
        }
        return error;
      },
    },
  ],
})
export class MyService extends LithiumService {}

Interceptor Context

interface InterceptorContext {
  url: string; // Full URL
  options: RequestInit; // Fetch options
  endpoint: string; // Just the endpoint path
  method: string; // HTTP method
}

WebSocket Services

For real-time features like chat, notifications, etc.

import { LithiumSocketService, socketService, inject } from "@lithium-ts/core";

@socketService()
export class ChatService extends LithiumSocketService {
  constructor() {
    super({
      url: "wss://api.example.com/chat",
      reconnect: true,
      reconnectInterval: 3000,
      maxReconnectAttempts: 5,
      autoConnect: false,
      protocols: ["v1.json"],
      auth: () => localStorage.getItem("token"),
      heartbeatInterval: 30000,
      heartbeatMessage: "ping",
    });

    // Register message handlers
    this.on("message:new", (data) => {
      this.publishToChannel("chat:messages", data);
    });

    this.on("user:typing", (data) => {
      this.emit("chat:typing", data); // Instant event
    });
  }

  // Public methods
  sendMessage(content: string) {
    this.send("message:send", { content });
  }

  joinRoom(roomId: string) {
    this.send("room:join", { roomId });
  }

  // Lifecycle hooks (override as needed)
  protected onConnected() {
    console.log("Connected to chat");
    this.emit("chat:connected", {});
  }

  protected onDisconnected() {
    console.log("Disconnected from chat");
    this.emit("chat:disconnected", {});
  }

  protected onReconnectFailed() {
    this.emit("chat:error", { message: "Connection failed" });
  }
}

Using in Components

@definePage({ title: "Chat" })
export class ChatPage extends LithiumElement {
  @inject(ChatService) private chat!: ChatService;

  private messages = this.channel<Message[]>("chat:messages", {
    initialValue: [],
    storage: "page",
  });

  connectedCallback() {
    super.connectedCallback();
    this.chat.connect();

    this.on("chat:typing", (data) => {
      this.showTypingIndicator(data.username);
    });
  }

  disconnectedCallback() {
    super.disconnectedCallback();
    this.chat.disconnect();
  }

  render() {
    return html`
      <div class="messages">
        ${this.messages
          .get()
          .map((msg) => html` <div class="message">${msg.content}</div> `)}
      </div>
      <input @keyup=${this.handleSend} />
    `;
  }
}

SocketServiceConfig

| Option | Type | Default | Description | | ---------------------- | ------------------ | -------- | ---------------------------- | | url | string | required | WebSocket server URL | | reconnect | boolean | true | Auto-reconnect on disconnect | | reconnectInterval | number | 3000 | Base reconnect delay (ms) | | maxReconnectAttempts | number | 5 | Max attempts (0 = infinite) | | autoConnect | boolean | false | Connect on instantiation | | protocols | string[] | [] | WebSocket sub-protocols | | auth | () => string | - | Auth token provider | | heartbeatInterval | number | 30000 | Heartbeat interval (ms) | | heartbeatMessage | string \| object | 'ping' | Heartbeat payload |


API Reference

Exports

// Core
export { LithiumElement, defineElement, StorageType } from "@lithium-ts/core";
export { definePage } from "@lithium-ts/core";
export { LithiumModule, defineModule } from "@lithium-ts/core";
export { LithiumApp, defineApp } from "@lithium-ts/core";
export { html, unsafeCSS, property } from "@lithium-ts/core";

// EventBus
export {
  EventBus,
  ChannelConfig,
  ListenerOptions,
  ChannelInfo,
  EventBusMiddleware,
} from "@lithium-ts/core";

// Router
export {
  LithiumRouter,
  NAVIGATE_EVENT,
  RouteGuardResult,
  RouteLocation,
  NavigateEventData,
  NavigateOptions,
  BeforeRouteCallback,
  AfterRouteCallback,
} from "@lithium-ts/core";

// Decorators
export { defer, DeferOptions } from "@lithium-ts/core";
export { lazy, LazyOptions } from "@lithium-ts/core";
export { conditional } from "@lithium-ts/core";

// Services
export {
  LithiumService,
  ServiceContainer,
  service,
  inject,
  ServiceRequestOptions,
  ServiceConfig,
  ServiceInterceptor,
  InterceptorContext,
} from "@lithium-ts/core";

// WebSocket Services
export {
  LithiumSocketService,
  socketService,
  SocketServiceConfig,
  SocketState,
  SocketMessage,
} from "@lithium-ts/core";

License

MIT © 2026