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

alpine-components-tombras

v1.0.4

Published

Agnostic, reusable scripts that handle the behavior of the UI. When needed, data is provided to a component through a service.

Readme

Shared Front-End Components: Build & Maintenance Guide

1. Key Concepts

Components

Agnostic, reusable scripts that handle the behavior of the UI. When needed, data is provided to a component through a service.

Services

Brand specific, implementation oriented scripts that provide data to the DOM.

Principles

  • Separation of Concerns: Components remain agnostic, services handle implementation.
  • Event-Driven Communication: Components and services interact via custom events.
  • Zero Dependencies: Components should work standalone (except Alpine.js/Tailwind)

Component Structure

Every component should have its name as a folder inside the scripts folder, with the actual js file as index.js.

File Organization

frontend/
├── scripts/
│   ├── ExampleComponent/
│   │   ├── index.js
│   │   └── README.md
│   └── AnotherComponent/
│       ├── index.js
│       └── README.md

Component Template

// @param {string} id unique identifier for the component. Mostly used when multiple instances of a component are listening to similar events. Must be equal to service's id it's listening to.
// @param {string} initializationEvent the event the component might need to listen for a certain action or initialization.
// @param {object} props the component specific properties that might be needed.

export function ExampleComponent(
  props = { id: "", initializationEvent: "", prop1, prop2, prop3 }
) {
  return {
    // Component state.
    isActive: false,

    // Initialization
    init() {
      // If id is declared, the component will only listen for the unique event, otherwise it will listen to the generic one. If no event is passed through, initialize normally.
      const { id, initializationEvent } = props;
      if (initializationEvent) {
        const uniqueInitializationEvent = id
          ? initializationEvent + "-" + id
          : initializationEvent;
        window.addEventListener(uniqueInitializationEvent, () => {
          this.initExampleComponent();
        });
      } else {
        this.initExampleComponent();
      }
    },

    initExampleComponent() {
      // Your initialization logic...
    },

    // Methods
    toggle() {
      this.isActive = !this.isActive;
    },

    // Cleanup
    destroy() {
      // Remove event listeners.
    },
  };
}

Service Template

Services should handle API requests, loading states and API error handling.

// @param {string} id unique identifier for the service. Will transform a generic event to a unique one. The components listening for this service's events must be assigned the same id.
// @param {string} eventName the name of the event to dispatch, if an id is provided, the event will dispatch as unique, with the format eventName + '-' + id
function ExampleService(id = "", eventName = "") {
  return {
    // Service state
    isLoading: false,
    error: null,
    data: null,

    // Initialization
    async init() {
      // Your initialization logic
      await this.loadData();
    },

    async loadData() {
      try {
        this.data = await this.fetchApi();
        this.$nextTick(() => {
          const uniqueEventName = id ? eventName + "-" + id : eventName;
          window.dispatchEvent(
            new CustomEvent(uniqueEventName, {
              detail: {
                data: this.data,
              },
            })
          );
        });
      } catch (error) {
        console.error("Error loading data:", error);
        this.error = error.message;
        this.data = null;
      }
    },

    // Api fetching logic.
    async fetchApi() {
      this.isLoading = true;
      this.error = null;

      try {
        const response = await fetch();

        if (!response.ok) {
          throw new Error(`HTTP error! status: ${response.status}`);
        }

        const json = await response.json();
        return json;
      } catch (error) {
        console.error("Error fetching data:", error);
        throw error;
      } finally {
        this.isLoading = false;
      }
    },

    // Utility logic.
    getCurrency(number) {
      return number.toLocaleString();
    },
  };
}

2. Component Documentation

Each component must include a README.md with the following structure:

Template

# Component Name

## Description

Brief description of what the component does.

## Requirements

Necessary requirements for the component to work.

## Example usage

```html
<div x-data="componentName.data()">
  <!-- Component HTML structure -->
</div>
```

```

## Properties

- `propertyName` (type): Description

## State

- `stateName` (type): Description

## Methods

- `methodName()`: Description

## Notes
```

3. Distribution Logic

Npm registry

We publish one version (ESM) to be used in modules like follows (Keep in mind this would need to be rebuilt for usage.):

import { Select } from "alpine-components-tombras";
export function data(props = {}) {
  return {
    ...Select(props),
  };
}

We publish another version for browser compatibility, which we can then include directly in our HTML. (CDN approach might be more suitable.)

<!DOCTYPE html>
<html>
  <head>
    <script
      defer
      src="https://unpkg.com/[email protected]/dist/cdn.min.js"
    ></script>
    <script src="<node-modules>/dist/alpine-components-tombras.js"></script>
  </head>
  <body>
    <div
      x-data="TombrasComponents.Select({ options: [{value: '1', label: 'Opt 1'},{value:'1', label:'Opt 1'}] })"
    >
      <p>Your select component should work here</p>
    </div>
  </body>
</html>

Private registry

  • Paid
  • Must create an organization in npm
  • Will be linked to our repository

Public registry

  • Free
  • Everybody can use it and fork it

4. Implementation

Direct usage

Create your html file, include the installed .js file in a script tag(node-modules/alpine-components-tombras/dist/alpine-components-tombras.js). You can freely use your components registered under the TombrasComponents namespace. Example:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
    <script
      defer
      src="https://cdn.jsdelivr.net/npm/[email protected]/dist/cdn.min.js"
    ></script>
    <script src="../../node_modules/alpine-components-tombras/dist/alpine-components-tombras.js"></script>
  </head>

  <body>
    <!-- Your component example -->
    <main class="py-20 flex items-center flex-col justify-center gap-10">
      <h1 class="font-bold text-2xl">TabsComponent</h1>
      <!-- Tab Component -->
      <div
        x-data="TombrasComponents.Tabs()"
        class="bg-white rounded-lg shadow-lg overflow-hidden"
      >
        <!-- Tab List -->
        <div class="flex border-b border-gray-200" role="tablist">
          <button
            role="tab"
            @click="activateTab(0)"
            @keydown="handleKeydown($event)"
            :class="activeTab === 0 ? 'bg-blue-500 text-white' : 'bg-gray-50 text-gray-700 hover:bg-gray-100'"
            class="flex-1 cursor-pointer px-6 py-3 font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-blue-500"
          >
            Dashboard
          </button>
          <button
            role="tab"
            @click="activateTab(1)"
            @keydown="handleKeydown($event)"
            :class="activeTab === 1 ? 'bg-blue-500 text-white' : 'bg-gray-50 text-gray-700 hover:bg-gray-100'"
            class="flex-1 cursor-pointer px-6 py-3 font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-blue-500"
          >
            Profile
          </button>
          <button
            role="tab"
            @click="activateTab(2)"
            @keydown="handleKeydown($event)"
            :class="activeTab === 2 ? 'bg-blue-500 text-white' : 'bg-gray-50 text-gray-700 hover:bg-gray-100'"
            class="flex-1 cursor-pointer px-6 py-3 font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-blue-500"
          >
            Settings
          </button>
        </div>

        <!-- Tab Panels -->
        <div role="tabpanel" class="p-6">
          <h2 class="text-xl font-semibold mb-3 text-gray-800">Dashboard</h2>
          <p class="text-gray-600 mb-4">
            Welcome to your dashboard! Here you can see an overview of your
            account activity.
          </p>
          <div class="bg-blue-50 p-4 rounded-lg">
            <p class="text-blue-800">
              📊 Your stats: 42 projects, 128 tasks completed
            </p>
          </div>
        </div>

        <div role="tabpanel" class="p-6">
          <h2 class="text-xl font-semibold mb-3 text-gray-800">Profile</h2>
          <p class="text-gray-600 mb-4">
            Manage your personal information and preferences.
          </p>
          <div class="bg-green-50 p-4 rounded-lg">
            <p class="text-green-800">👤 Profile completion: 85%</p>
          </div>
        </div>

        <div role="tabpanel" class="p-6">
          <h2 class="text-xl font-semibold mb-3 text-gray-800">Settings</h2>
          <p class="text-gray-600 mb-4">
            Configure your application settings and preferences.
          </p>
          <div class="bg-purple-50 p-4 rounded-lg">
            <p class="text-purple-800">⚙️ 12 settings available to customize</p>
          </div>
        </div>
      </div>
    </main>
  </body>
</html>

Component extension

If you want to extend the component's functionality, you need to create a new javascript file, import the component/s you'd like to inherit and add/overwrite your desired props. Then you need to build them using esbuild and include them using the load-script tag, doc reference: https://tombras.atlassian.net/wiki/spaces/DEV/pages/2929000450/.NET+Frontend+Development+Process

Example:

import { Select } from "alpine-components-tombras";
export function data(props = {}) {
  return {
    ...Select(props),
  };
}
  • Run build process with namespace selectDropdown
<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
    <script
      defer
      src="https://cdn.jsdelivr.net/npm/[email protected]/dist/cdn.min.js"
    ></script>
  </head>

  <body>
    <main class="py-20 flex flex-col items-center justify-center gap-10">
      <h1 class="font-bold text-2xl">SelectDropdown</h1>

      <div
        x-data="selectDropdown.data({initialValue: 'default', options: [
            { value: 'option1', label: 'Option 1' },
            { value: 'option2', label: 'Option 2' },
            { value: 'option3', label: 'Option 3' }
        ]})"
        class="relative"
      >
        <!-- Hidden native select for form submission -->
        <select name="dropdown" class="sr-only hidden" x-model="selectedValue">
          <template x-for="option in options" :key="option.value">
            <option :value="option.value" x-text="option.label"></option>
          </template>
        </select>

        <!-- Custom dropdown trigger -->
        <button
          @keydown.enter.prevent="toggleOpen()"
          @keydown.space.prevent="toggleOpen()"
          @keydown.arrow-down.prevent="navigateOptions(1)"
          @click="toggleOpen()"
          :aria-expanded="open"
          class="w-full gap-5 p-4 border-2 border-gray-200 rounded-xl bg-white hover:bg-gray-50 hover:border-blue-300 focus:outline-none focus:ring-4 focus:ring-blue-100 focus:border-blue-400 flex items-center cursor-pointer justify-between transition-all duration-200 shadow-sm"
        >
          <span x-text="selectedValue" class="text-gray-700 font-medium"></span>
          <svg
            class="w-5 h-5 transition-transform duration-200 text-gray-400"
            :class="{'rotate-180': open}"
            viewBox="0 0 20 20"
            fill="currentColor"
          >
            <path
              fill-rule="evenodd"
              d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z"
              clip-rule="evenodd"
            />
          </svg>
        </button>

        <!-- Dropdown options -->
        <div
          x-show="open"
          x-transition:enter="transition ease-out duration-200"
          @click.away="open = false"
          x-transition:enter-start="opacity-0 transform scale-95"
          x-transition:enter-end="opacity-100 transform scale-100"
          x-transition:leave="transition ease-in duration-150"
          x-transition:leave-start="opacity-100 transform scale-100"
          x-transition:leave-end="opacity-0 transform scale-95"
          x-cloak
          @click.away="open = false"
          class="absolute z-10 w-full mt-2 bg-white border border-gray-200 rounded-xl shadow-lg overflow-hidden"
          role="listbox"
        >
          <template x-for="(option, index) in options" :key="index">
            <button
              @click="selectOption(option.value)"
              @keydown.enter.prevent="selectOption(option.value)"
              @keydown.space.prevent="selectOption(option.value)"
              @keydown.arrow-down.prevent="navigateOptions(1)"
              @keydown.arrow-up.prevent="navigateOptions(-1)"
              :class="{
                            'bg-blue-50 border-blue-200': highlightedIndex === index,
                            'bg-blue-500 text-white': selectedValue === option.value
                        }"
              :aria-selected="selectedValue === option.value"
              role="option"
              :tabindex="highlightedIndex === index ? 0 : -1"
              class="w-full cursor-pointer px-4 py-3 text-left hover:bg-gray-50 focus:bg-blue-50 focus:outline-none transition-colors duration-150 border-b border-gray-100 last:border-b-0"
            >
              <span
                x-text="option.label"
                class="font-medium text-gray-700"
              ></span>
            </button>
          </template>
        </div>
      </div>
    </main>
    <script src="./dist/select.js"></script>
  </body>
</html>

5. Testing