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 🙏

© 2024 – Pkg Stats / Ryan Hefner

kui-framework

v1.0.9

Published

A lightweight framework for building web applications.

Downloads

37

Readme

UI Framework

License Version TypeScript

A lightweight and flexible UI framework for building modern web applications.

Features

You can create all kinds of interfaces quickly with this framework.

Email Client Example

  • 🚀 Lightweight and fast
  • 🧩 Modular component system
  • 🎨 Customizable styling
  • 🔌 Extensible plugin architecture
  • 📱 Responsive design support
  • ✨ Animation support for UI elements
  • 📝 Templating system for rendering dynamic content

How it Works

The code in src/lib is implementing a lightweight UI framework for building web applications. It provides a set of reusable components and utilities to create interactive user interfaces. Here's a brief overview of its functionality:

  1. Core components: View, Container, Button, Form, List, and Modal.
  2. Templating system for rendering dynamic content.
  3. Animation support for UI elements.
  4. Styling utilities for consistent appearance.
  5. Plugin system for extensibility.

Simple Example:

import { Container, Button, Form, Animation } from "../index";

export function loginexample()
{
    const app = new Container();

    const login = new Button({
    text: "Login",
    hoverAnimation: Animation("grow", 300),
    });
    login.setOnClick(() => alert("Hello!"));

    const loginForm = new Form({
    fields: [
        { type: "text", name: "username", label: "Username" },
        { type: "password", name: "password", label: "Password" },
    ],
    onSubmit: (data) => console.log("Login:", data),
    });

    app.addChild(loginForm);
    app.addChild(login);
    app.mount(document.body);
}

This example creates a simple app with a greeting button and a login form.

In-depth explanation

The framework is built around the concept of Views, which are the basic building blocks of the UI. The View class provides core functionality for creating and managing UI elements.

// view.ts

import { AnimationOptions } from "./animations";

export interface ViewOptions {
  id?: string;
  className?: string;
  template?: string;
}

export class View {
  protected element: HTMLElement;
  protected children: View[] = [];
  private mountedPromise: Promise<void>;
  private mountedResolve!: () => void;

  constructor(options: ViewOptions = {}) {
    this.element = document.createElement("div");
    if (options.id) this.element.id = options.id;
    if (options.className) this.element.className = options.className;
    if (options.template) this.setTemplate(options.template);

    this.mountedPromise = new Promise((resolve) => {
      this.mountedResolve = resolve;
    });
  }

  setTemplate(template: string): void {
    this.element.innerHTML = template;
  }

  appendChild(child: View): void {
    this.children.push(child);
    this.element.appendChild(child.getElement());
  }

  getElement(): HTMLElement {
    return this.element;
  }

  async onMount(): Promise<void> {
    // To be overridden by subclasses
  }

  async onUnmount(): Promise<void> {
    // To be overridden by subclasses
  }

  mounted(): Promise<void> {
    return this.mountedPromise;
  }

  mount(parent: HTMLElement): void {
    parent.appendChild(this.element);
    this.mountedResolve();
    this.onMount();
  }

  unmount(): void {
    this.element.remove();
    this.onUnmount();
  }

  centerInRow(): this {
    this.element.style.justifySelf = "center";
    return this;
  }

  centerInColumn(): this {
    this.element.style.alignSelf = "center";
    return this;
  }
}

The Container class extends View and allows for more complex layouts, including grid systems and flexible positioning of child elements.

// container.ts

import { View, ViewOptions } from "./view";
import { Button, ButtonOptions } from "./button";
import { Form, FormOptions } from "./form";
import { framework } from "./framework";

// Initialize the framework
framework.init();

export interface GridOptions {
  columns: number | string;
  rows?: number | string;
  gap?: string;
}

export type RowAlignment =
  | "left"
  | "center"
  | "right"
  | "space-between"
  | "space-around"
  | "space-evenly";

export interface ContainerOptions extends ViewOptions {
  gap?: string;
  padding?: string;
  grid?: GridOptions;
  scrollable?: boolean;
  rowAlignment?: RowAlignment;
  equalWidthChildren?: boolean;
  columns?: number | string;
  rows?: number | string;
}

Other components like Button, Form, and List build upon these base classes to provide specific functionality. For example, the Button class adds click handling and hover effects.

// button.ts

import { View, ViewOptions } from "./view";
import { renderTemplate } from "./templating";
import { AnimationOptions, applyAnimation } from "./animations";

export interface ButtonOptions extends ViewOptions {
  text: string;
  onClick?: () => void;
  color?: string;
  hoverColor?: string;
  hoverAnimation?: AnimationOptions;
  clickAnimation?: AnimationOptions;
}

const buttonTemplate = `
  <button class="ui-button" style="background-color: {{color}}">
    {{text}}
  </button>
`;

export class Button extends View {
  private buttonElement: HTMLButtonElement;

  constructor(options: ButtonOptions) {
    super({
      ...options,
      template: renderTemplate(buttonTemplate, {
        text: options.text,
        color: options.color || "#007bff",
      }),
    });

    this.buttonElement = this.element.querySelector(
      "button"
    ) as HTMLButtonElement;

    if (options.onClick) {
      this.buttonElement.addEventListener("click", () => {
        if (options.clickAnimation) {
          applyAnimation(this.buttonElement, options.clickAnimation);
        }
        // Add null check before invoking onClick
        options.onClick?.();
      });
    }

    if (options.hoverAnimation || options.hoverColor) {
      this.buttonElement.addEventListener("mouseenter", () => {
        if (options.hoverAnimation) {
          applyAnimation(this.buttonElement, options.hoverAnimation);
        }
        if (options.hoverColor) {
          this.buttonElement.style.backgroundColor = options.hoverColor;
        }
      });

      this.buttonElement.addEventListener("mouseleave", () => {
        if (options.hoverAnimation) {
          this.buttonElement.style.animation = "none";
        }
        if (options.hoverColor) {
          this.buttonElement.style.backgroundColor = options.color || "#007bff";
        }
      });
    }
  }

  setText(text: string): void {
    this.buttonElement.textContent = text;
  }

  setColor(color: string): void {
    this.buttonElement.style.backgroundColor = color;
  }
}

The framework also includes a templating system for rendering dynamic content, and an animation system (referenced in button.ts) for adding visual effects to UI elements.

// templating.ts

export function renderTemplate(
  template: string,
  data: { [key: string]: any }
): string {
  return template.replace(/\{\{(\w+)\}\}/g, (_, key) => data[key] || "");
}

Complex Example: Email Client Example

// emailClientExample.ts

import { Container, View, Button, Form, List } from "../index";
import { initStyles, appendStyles } from "../styles";

interface Email {
  id: number;
  from: string;
  subject: string;
  body: string;
  read: boolean;
}

class EmailItem extends View {
  constructor(email: Email, onSelect: (id: number) => void) {
    super({
      className: `email-item ${email.read ? "read" : "unread"}`,
      template: `
        <div class="email-sender">${email.from}</div>
        <div class="email-subject">${email.subject}</div>
      `,
    });

    this.element.addEventListener("click", () => onSelect(email.id));
  }
}

class EmailView extends View {
  constructor(email: Email | null) {
    super({
      className: "email-view",
      template: email
        ? `
        <h2>${email.subject}</h2>
        <p><strong>From:</strong> ${email.from}</p>
        <div class="email-body">${email.body}</div>
      `
        : "<p>Select an email to view</p>",
    });
  }
}

export function createEmailClientExample(): View {
  initStyles();
  appendStyles(emailClientCSS);

  const mainContainer = new Container({
    className: "email-client",
    gap: "20px",
  });

  // Header
  const header = new View({
    className: "header",
    template: "<h1>Simple Email Client</h1>",
  });
  mainContainer.addChild(header);

  // Main content area
  const content = new Container({
    className: "content",
    gap: "20px",
    columns: "150px 300px 1fr", // Define three columns
  });
  mainContainer.addChild(content);

  // Sidebar (Folders)
  const sidebar = new Container({
    className: "sidebar",
    gap: "10px",
    equalWidthChildren: true,
  });
  ["Inbox", "Sent", "Drafts", "Trash"].forEach((folder) => {
    sidebar.addChild(
      new Button({
        text: folder,
        hoverColor: "#0056b3",
        hoverAnimation: { type: "grow", duration: 300 },
        clickAnimation: { type: "push", duration: 200 },
        onClick: () => console.log(`Switched to ${folder}`),
      })
    );
  });
  content.addChild(sidebar);

  // Email list
  const emailListContainer = new Container({
    className: "email-list-container",
    gap: "10px",
  });
  content.addChild(emailListContainer);

  const emailList = new List({ className: "email-list" });
  emailListContainer.addChild(emailList);

  // Compose button
  const composeButton = new Button({
    text: "Compose",
    onClick: () => showComposeForm(),
  });
  emailListContainer.addChild(composeButton);

  // Email view
  const emailViewContainer = new Container({
    className: "email-view-container",
    gap: "10px",
  });
  content.addChild(emailViewContainer);

  let currentEmailView = new EmailView(null);
  emailViewContainer.addChild(currentEmailView);

  // Sample emails
  const emails: Email[] = [
    {
      id: 1,
      from: "[email protected]",
      subject: "Hello",
      body: "Hi there!",
      read: false,
    },
    {
      id: 2,
      from: "[email protected]",
      subject: "Meeting tomorrow",
      body: "Don't forget our meeting.",
      read: true,
    },
    {
      id: 3,
      from: "[email protected]",
      subject: "Urgent: Report needed",
      body: "Please send the report ASAP.",
      read: false,
    },
  ];

  function renderEmails() {
    emailList.clear();
    emails.forEach((email) => {
      emailList.addItem(new EmailItem(email, selectEmail));
    });
  }

  function selectEmail(id: number) {
    const email = emails.find((e) => e.id === id);
    if (email) {
      email.read = true;
      currentEmailView = new EmailView(email);
      emailViewContainer.clear();
      emailViewContainer.addChild(currentEmailView);
      renderEmails(); // Re-render to update read/unread status
    }
  }

  function showComposeForm() {
    const composeForm = new Form({
      fields: [
        { type: "text", name: "to", label: "To:", required: true },
        { type: "text", name: "subject", label: "Subject:", required: true },
        { type: "textarea", name: "body", label: "Message:", required: true },
      ],
      onSubmit: (data) => {
        console.log("Sending email:", data);
        // Here you would typically send the email and update the sent folder
        emailViewContainer.clear();
        emailViewContainer.addChild(new EmailView(null));
      },
    });
    emailViewContainer.clear();
    emailViewContainer.addChild(composeForm);
  }

  renderEmails();

  return mainContainer;
}

const emailClientCSS = `
  .email-client {
    max-width: 1200px;
    margin: 0 auto;
    padding: 20px;
  }

  .content {
    display: grid;
    grid-template-columns: 150px 300px 1fr;
    gap: 20px;
  }

  .sidebar .ui-button button {
    width: 100%;
    text-align: left;
    padding: 10px;
  }

  .email-list-container {
    overflow-y: auto;
    max-height: 80vh;
  }

  .email-item {
    padding: 10px;
    border-bottom: 1px solid #40444B;
    cursor: pointer;
  }

  .email-item:hover {
    background-color: #40444B;
  }

  .email-item.unread {
    font-weight: bold;
  }

  .email-sender {
    font-size: 0.9em;
  }

  .email-subject {
    font-size: 1em;
  }

  .email-view {
    background-color: #40444B;
    padding: 20px;
    border-radius: 4px;
  }

  .email-body {
    margin-top: 20px;
    white-space: pre-wrap;
  }
`;

This example creates a more complex task management application using various components and demonstrating their interactions.

Adding and Using Plugins

The framework includes a plugin system that allows for extending its functionality.

// plugin-system.ts

/**
 * Interface for plugins that can be registered with the PluginManager.
 */
interface Plugin {
  readonly name: string;
  install(framework: any): void;
}

To add a plugin:

  1. Create a plugin object that implements the Plugin interface:
const myPlugin: Plugin = {
  name: 'myPlugin',
  install(framework: any) {
    // Add new functionality to the framework
    framework.newMethod = () => console.log('New method added by plugin');
  }
};
  1. Register the plugin with the framework:
import { framework } from './lib/framework';

framework.registerPlugin(myPlugin);
  1. Use the new functionality provided by the plugin:
framework.newMethod(); // Outputs: "New method added by plugin"

This plugin system allows for modular extension of the framework's capabilities without modifying its core code. Here is a state management plugin:

import { Plugin } from '../plugin-system';

type Reducer<S, A> = (state: S, action: A) => S;
type Listener = () => void;
type Unsubscribe = () => void;

interface Store<S, A> {
  getState: () => S;
  dispatch: (action: A) => void;
  subscribe: (listener: Listener) => Unsubscribe;
}

const stateManagementPlugin: Plugin = {
  name: 'StateManagementPlugin',
  install(framework) {
    framework.createStore = function<S, A>(reducer: Reducer<S, A>, initialState: S): Store<S, A> {
      let state = initialState;
      const listeners: Listener[] = [];

      const store: Store<S, A> = {
        getState: () => state,
        dispatch: (action: A) => {
          state = reducer(state, action);
          listeners.forEach(listener => listener());
        },
        subscribe: (listener: Listener): Unsubscribe => {
          listeners.push(listener);
          return () => {
            const index = listeners.indexOf(listener);
            if (index > -1) listeners.splice(index, 1);
          };
        }
      };

      return store;
    };

    framework.View.prototype.connectToStore = function<S, P>(store: Store<S, any>, mapStateToProps: (state: S) => P) {
      const updateView = () => {
        const stateProps = mapStateToProps(store.getState());
        Object.assign(this, stateProps);
        this.render();
      };
      const unsubscribe = store.subscribe(updateView);
      updateView();

      // Clean up subscription when view is unmounted
      const originalUnmount = this.unmount;
      this.unmount = () => {
        unsubscribe();
        originalUnmount.call(this);
      };
    };
  }
};

export default stateManagementPlugin;

This plugin adds a Redux-like state management system to the framework:

  1. It defines a createStore function that creates a store with getState, dispatch, and subscribe methods.
  2. The connectToStore method is added to the View prototype, allowing views to easily connect to the store and update when the state changes.
  3. It uses the reducer pattern to update the state immutably.
  4. The plugin handles subscription cleanup when a view is unmounted.