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

@shifl-inc/ui

v0.9.17

Published

Reusable Vue 3 UI components for Shifl.

Readme

Shifl Grid (Vue 3)

JSON-driven data grid for Shifl UI, built with Vue 3 + TypeScript. Library-first output via Vite (library mode) with Tailwind + CSS variables for theming.

Status

  • ✅ Minimal grid (column visibility toggle, client-side sorting, global filter)
  • ✅ API integration with server-side data fetching
  • ✅ Pagination, search, filters, and sorting via API
  • 🚧 Everything else scaffolded/stubbed for incremental delivery (editing, virtual scroll, tours, views)

Requirements

  • Node >=20.11.0
  • Peer deps: vue@^3.4.0

Install

npm install @shifl-inc/ui

Usage

Setup

// main.ts
import { createApp } from 'vue';
import App from './App.vue';
import { ShiflGridPlugin } from '@shifl-inc/ui';
import '@shifl-inc/ui/style.css';

const app = createApp(App);
app.use(ShiflGridPlugin);
app.mount('#app');

Client-Side Mode (Static Data)

For static data that doesn't require API calls:

<script setup lang="ts">
import { ShiflGrid } from '@shifl-inc/ui';
import type { GridConfig } from '@shifl-inc/ui';

const config: GridConfig = {
  name: 'My Preferred Shipments',
  columns: [
    { key: 'shifl_ref', label: 'Shifl Ref #', sortable: true, freeze: true },
    { key: 'company_name', label: 'Customer', sortable: true }
  ],
  search: ['shifl_ref', 'company_name'],
  sort: { key: 'shifl_ref', order: 'asc' },
  rows: [
    { shifl_ref: 'SF-1001', company_name: 'GMA Inc' },
    { shifl_ref: 'SF-1002', company_name: 'Walads Autowash' }
  ],
  paginationMeta: {
    show: true,
    currentPage: 1,
    perPage: 20,
    total: 2
  }
};
</script>

<template>
  <ShiflGrid :config="config" />
</template>

Server-Side Mode (API Integration)

For dynamic data fetched from an API:

<script setup lang="ts">
import { ShiflGrid } from '@shifl-inc/ui';
import type { GridConfig } from '@shifl-inc/ui';

const config: GridConfig = {
  name: 'My Preferred Shipments',
  apiConfig: {
    apiUrl: 'https://api.example.com/shipments',
    method: 'GET', // Optional: 'GET' | 'POST' | 'PUT' (default: 'GET')
    getAuthToken: () => localStorage.getItem('token'), // Optional: function to get auth token
    headers: {
      // Optional: additional headers
      'X-Custom-Header': 'value'
    },
    additionalParams: {
      // Optional: extra params to send with requests
      module: 'shipments',
      customerId: 123
    },
    debounceMs: 300, // Optional: debounce search queries (default: 300ms)
    responseTransformer: <T,>(response: unknown) => {
      // Optional: transform API response to expected format
      const res = response as {
        data?: T[];
        meta?: {
          current_page?: number;
          per_page?: number;
          total?: number;
        };
      };
      return {
        data: res.data || [],
        meta: res.meta
          ? {
              currentPage: res.meta.current_page,
              perPage: res.meta.per_page,
              total: res.meta.total,
              show: true
            }
          : undefined
      };
    }
  },
  columns: [
    { key: 'shifl_ref', label: 'Shifl Ref #', sortable: true, freeze: true },
    { key: 'company_name', label: 'Customer', sortable: true }
  ],
  search: ['shifl_ref', 'company_name'],
  paginationMeta: {
    show: true,
    currentPage: 1,
    perPage: 20,
    total: 0 // Will be updated from API response
  }
};
</script>

<template>
  <ShiflGrid :config="config" />
</template>

Configuration Options

GridConfig

interface GridConfig<T = Record<string, unknown>> {
  id?: string;
  name?: string;
  customerId?: number;
  module?: string;
  resourceId?: string | number | null;
  creatorId?: number;
  pageLocation?: string;
  view?: 'regular' | 'compact' | 'comfortable';

  // API Configuration (for server-side mode)
  apiConfig?: {
    apiUrl: string; // Required: API endpoint URL
    method?: 'GET' | 'POST' | 'PUT'; // Optional: HTTP method (default: 'GET')
    getAuthToken?: () => string | undefined; // Optional: function to get auth token
    headers?: Record<string, string>; // Optional: additional headers
    additionalParams?: Record<string, unknown> | ((context) => Record<string, unknown>); // Optional: extra params
    paramsBuilder?: (context) => URLSearchParams | Record<string, unknown>; // Optional: custom query params
    bodyBuilder?: (context) => unknown; // Optional: custom request body
    responseTransformer?: <T>(response: unknown) => ApiResponse<T>; // Optional: transform API response
    debounceMs?: number; // Optional: search debounce delay (default: 300ms)
  };

  columns: GridColumn[];
  search?: string[]; // Searchable column keys (for client-side filtering)
  searchMode?: 'client' | 'server'; // Auto-set to 'server' if apiConfig is provided
  filters?: GridFilter[];
  sort?: GridSort;
  paginationMeta?: PaginationMeta;
  rows?: T[]; // Static data (for client-side mode)
}

Client-Side Mode Features

  • Sorting: Client-side only; click column headers to toggle asc/desc
  • Global filter: Simple case-insensitive substring search across listed search fields
  • Visibility: Toggle columns via pills above the grid
  • Pagination: Client-side pagination of static data

Server-Side Mode Features

  • Sorting: Server-side; click column headers to sort via API
  • Search: Debounced search queries sent to API (default: 300ms delay)
  • Filters: Filter values sent as query parameters or in request body
  • Pagination: Handled by server; pagination metadata from API response
  • Authentication: Optional bearer token via getAuthToken function
  • Custom params: Use additionalParams to send extra parameters
  • Pagination state: User's selected page is the source of truth; API's currentPage is informational only

API Response Formats

Default Response Format (Recommended)

The grid works out-of-the-box with this format:

{
  "data": [
    { "id": 1, "name": "Item 1", "status": "active" },
    { "id": 2, "name": "Item 2", "status": "inactive" }
  ],
  "meta": {
    "currentPage": 1,
    "perPage": 20,
    "total": 100,
    "lastPage": 5,
    "from": 1,
    "to": 20,
    "show": true
  }
}

Alternative Default Formats

The default transformer also supports these formats:

Format 1: Using rows instead of data

{
  "rows": [
    { "id": 1, "name": "Item 1" },
    { "id": 2, "name": "Item 2" }
  ],
  "meta": {
    "currentPage": 1,
    "perPage": 20,
    "total": 100
  }
}

Format 2: Data only (no pagination)

{
  "data": [
    { "id": 1, "name": "Item 1" },
    { "id": 2, "name": "Item 2" }
  ]
}

Required Fields

The grid requires these fields in meta for proper pagination:

  • Required for pagination to work:

    • perPage (or per_page in Laravel format) - number of items per page
    • total (total number of records) - used to calculate total pages
  • Optional but recommended:

    • currentPage (or current_page in Laravel format) - informational only, does not override user's selected page
    • lastPage (or last_page) - for better UX
    • from - starting record number
    • to - ending record number
    • show - whether to show pagination (default: true)

Important: Pagination State Behavior

The grid uses the user's pagination state as the source of truth. When a user clicks page 2, the grid will:

  • Display page 2 as active in the UI
  • Send page=2 in the API request
  • Show the correct active page even if the API response returns currentPage: 1

This behavior ensures the pagination UI remains correct even when using mock/dummy APIs that always return currentPage: 1. The API's currentPage value is only used for informational purposes and does not override the user's selection.

Custom Response Transformers

If your API uses a different response format, use the responseTransformer option:

Laravel-Style Pagination Format

const config: GridConfig = {
  apiConfig: {
    apiUrl: 'https://api.example.com/data',
    getAuthToken: () => localStorage.getItem('token'),
    responseTransformer: <T>(response: unknown) => {
      const res = response as {
        data?: T[];
        meta?: {
          current_page?: number;
          per_page?: number;
          total?: number;
          last_page?: number;
          from?: number;
          to?: number;
        };
      };

      return {
        data: res.data || [],
        meta: res.meta
          ? {
              currentPage: res.meta.current_page,
              perPage: res.meta.per_page,
              total: res.meta.total,
              lastPage: res.meta.last_page,
              from: res.meta.from,
              to: res.meta.to,
              show: true
            }
          : undefined
      };
    }
  }
  // ... rest of config
};

Laravel Response Example:

{
  "data": [{ "id": 1, "name": "Item 1" }],
  "meta": {
    "current_page": 1,
    "per_page": 20,
    "total": 100,
    "last_page": 5,
    "from": 1,
    "to": 20
  }
}

REST API Standard Format

const config: GridConfig = {
  apiConfig: {
    apiUrl: 'https://api.example.com/data',
    responseTransformer: <T>(response: unknown) => {
      const res = response as {
        results?: T[];
        items?: T[];
        count?: number;
        page?: number;
        pageSize?: number;
      };

      return {
        data: res.results || res.items || [],
        meta: {
          currentPage: res.page || 1,
          perPage: res.pageSize || 20,
          total: res.count || 0,
          show: true
        }
      };
    }
  }
  // ... rest of config
};

REST Response Example:

{
  "results": [{ "id": 1, "name": "Item 1" }],
  "count": 100,
  "page": 1,
  "pageSize": 20
}

GraphQL-Style Format

const config: GridConfig = {
  apiConfig: {
    apiUrl: 'https://api.example.com/graphql',
    responseTransformer: <T>(response: unknown) => {
      const res = response as {
        data?: {
          items?: T[];
          pagination?: {
            page: number;
            pageSize: number;
            totalCount: number;
          };
        };
      };

      return {
        data: res.data?.items || [],
        meta: res.data?.pagination
          ? {
              currentPage: res.data.pagination.page,
              perPage: res.data.pagination.pageSize,
              total: res.data.pagination.totalCount,
              show: true
            }
          : undefined
      };
    }
  }
  // ... rest of config
};

GraphQL Response Example:

{
  "data": {
    "items": [{ "id": 1, "name": "Item 1" }],
    "pagination": {
      "page": 1,
      "pageSize": 20,
      "totalCount": 100
    }
  }
}

Request Parameters Sent by Grid

The grid automatically sends these parameters:

GET Request (Query Parameters)

GET /api/data?search=query&page=1&per_page=20&sort=name&order=asc&filters[status]=active&filters[type][]=type1&filters[type][]=type2

POST/PUT Request (Body)

{
  "search": "query",
  "page": 1,
  "per_page": 20,
  "sort": "name",
  "order": "asc",
  "filters": {
    "status": "active",
    "type": ["type1", "type2"]
  },
  "module": "shipments",
  "customerId": 123
}

Error Handling

The grid handles errors gracefully. Your API can return standard HTTP error responses:

{
  "error": "Unauthorized",
  "message": "Invalid authentication token"
}

Or with status codes:

  • 400 - Bad Request
  • 401 - Unauthorized
  • 403 - Forbidden
  • 404 - Not Found
  • 500 - Internal Server Error

The grid will display the error and stop loading.

Scripts

  • npm run dev — Vite dev server
  • npm run build — Library build + d.ts via vite-plugin-dts
  • npm run lint — ESLint (Vue + TS) flat config
  • npm run test — Vitest (jsdom)
  • npm run storybook — Storybook dev
  • npm run storybook:build — Storybook static build

Project structure (high level)

  • src/components/gridShiflGrid.vue (minimal implementation)
  • src/composables — Core behaviors (sort/filter/columns implemented, others stubbed/TODO)
  • src/types — Grid + theme contracts
  • src/theme — CSS tokens
  • src/styles — Tailwind entry + component styles
  • src/utils — Helpers (config normalization)
  • src/plugins — Vue install plugin
  • stories/ — Storybook stories
  • .storybook/ — Storybook configuration

API Integration

The grid supports server-side data fetching with automatic API calls for search, filters, sorting, and pagination. When apiConfig is provided, the grid automatically:

  • Fetches data from your API endpoint
  • Sends search, filter, sort, and pagination parameters
  • Handles loading states and errors
  • Updates the grid when parameters change
  • Supports bearer token authentication
  • Debounces search queries to reduce API calls
  • Uses user's pagination state as source of truth - the active page displayed is based on user selection, not the API's currentPage response

See API Response Formats for detailed information about:

  • Expected API response formats
  • Custom response transformers
  • Request parameters sent by the grid
  • Error handling
  • Pagination behavior and requirements

Notes

  • Virtual scroll, editing, tours, saved/recommended views, telemetry are intentionally stubbed for later milestones.