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

@vention/machine-apps-components

v0.4.0

Published

Reusable components for machine applications.

Readme

Machine Apps Components

Reusable components for machine applications.

Components

NavigationBar

A navigation bar component with support for:

  • Multiple navigation items with icons
  • Control Center button
  • Support button
  • Optional timer display

Props

interface NavigationItem {
  label: string
  path: string
  icon: ReactNode
  onClick?: () => void // Optional: Custom click handler (overrides default navigation)
}

interface NavigationBarProps {
  navigationItems: NavigationItem[]
  showTimer?: boolean // Default: true
}

Example Usage

Basic Usage
import { NavigationBar } from "@vention/machine-apps-components"
import { VentionIcon } from "@ventionco/machine-ui"

const NAV_ITEMS = [
  {
    label: "Operation",
    path: "/operation",
    icon: <VentionIcon size={32} type="category-filled" color="white" />,
  },
  {
    label: "Settings",
    path: "/settings",
    icon: <VentionIcon size={32} type="tool-1" color="white" />,
  },
]

// With timer (default)
<NavigationBar navigationItems={NAV_ITEMS} />

// Without timer
<NavigationBar navigationItems={NAV_ITEMS} showTimer={false} />
Custom Click Handlers

You can override the default navigation behavior by providing custom click handlers:

// Custom handler for navigation items
const NAV_ITEMS = [
  {
    label: "Settings",
    path: "/settings",
    icon: <VentionIcon size={32} type="tool-1" color="white" />,
    onClick: () => {
      // Show confirmation before navigating
      if (confirm("You have unsaved changes. Continue?")) {
        navigate("/settings")
      }
    },
  },
  {
    label: "Logs",
    path: "/logs",
    icon: <VentionIcon size={32} type="alarm-bell-filled" color="white" />,
    onClick: () => {
      // Do something before navigating
      localStorage.setItem("lastView", "operation")
      navigate("/logs")
    },
  },
]

Note: When a custom onClick handler is provided, it completely overrides the default navigation behavior. The handler is responsible for any navigation logic if needed.

StatusTopBar

A status bar component positioned at the top of the screen that displays:

  • Status indicator with label and colored dot
  • Configurable action buttons with visibility and disabled states
  • Custom styling for buttons

Key Features:

  • ✅ Declarative, config-based API
  • ✅ Fully controlled component (no internal state)
  • ✅ Type-safe button configurations
  • ✅ Easy to test and reason about

Props

interface ButtonConfig {
  id: string // Unique identifier for the button
  label: string // Button text
  onClick?: () => void // Click handler
  visible?: boolean // Show/hide button (default: true)
  disabled?: boolean // Enable/disable button (default: false)
  backgroundColor?: string // Button background color (e.g., "#4CAF50")
  backgroundColorHover?: string // Hover background color
  borderColor?: string // Border color
  textColor?: string // Text color
  width?: number // Button width in pixels (default: 208)
  height?: number // Button height in pixels (default: 80)
  icon?: ReactNode // Icon to display when enabled
  iconDisabled?: ReactNode // Icon to display when disabled
}

interface StatusTopBarProps {
  status?: {
    label: string // Status label text
    color: string // Color of the status dot
  }
  buttonConfigs?: ButtonConfig[] // Array of button configurations
}

Example Usage

Basic Usage
import { StatusTopBar, ButtonConfig } from "@vention/machine-apps-components"
import { VentionIcon } from "@ventionco/machine-ui"

function App() {
  const buttonConfigs: ButtonConfig[] = [
    {
      id: "start",
      label: "Start",
      onClick: () => console.log("Start clicked"),
      backgroundColor: "#2196F3",
      textColor: "white",
      icon: <VentionIcon size={32} type="player-play-filled" color="white" />,
      visible: true,
      disabled: false,
    },
    {
      id: "stop",
      label: "Stop",
      onClick: () => console.log("Stop clicked"),
      backgroundColor: "#F44336",
      textColor: "white",
      icon: <VentionIcon size={32} type="player-stop-filled" color="white" />,
      visible: false, // Hidden by default
      disabled: false,
    },
  ]

  return (
    <StatusTopBar
      status={{
        label: "Machine state: Ready",
        color: "#4CAF50",
      }}
      buttonConfigs={buttonConfigs}
    />
  )
}
Dynamic State Management

The component is fully controlled - all button states are derived from your application state:

import { StatusTopBar, ButtonConfig } from "@vention/machine-apps-components"
import { useMemo } from "react"

function MachineApp() {
  const { machineState, isRunning, canStart } = useMachineState()

  const buttonConfigs: ButtonConfig[] = useMemo(() => {
    const configs: ButtonConfig[] = []

    // Start button - visible when not running
    if (!isRunning) {
      configs.push({
        id: "start",
        label: "Start",
        onClick: handleStart,
        backgroundColor: "#2196F3",
        textColor: "white",
        visible: true,
        disabled: !canStart, // Disabled if prerequisites not met
      })
    }

    // Stop button - visible when running
    if (isRunning) {
      configs.push({
        id: "stop",
        label: "Stop",
        onClick: handleStop,
        backgroundColor: "#F44336",
        textColor: "white",
        visible: true,
        disabled: false,
      })
    }

    // Home button - always visible
    configs.push({
      id: "home",
      label: "Home",
      onClick: handleHome,
      borderColor: "#E2E8F0",
      textColor: "#1A202C",
      visible: true,
      disabled: isRunning, // Can't home while running
    })

    return configs
  }, [isRunning, canStart])

  return (
    <StatusTopBar
      status={{
        label: `Machine state: ${machineState}`,
        color: getStatusColor(machineState),
      }}
      buttonConfigs={buttonConfigs}
    />
  )
}
Conditional Buttons

Easily show/hide buttons based on application state:

const buttonConfigs: ButtonConfig[] = useMemo(() => {
  const configs: ButtonConfig[] = []

  // Operation page buttons
  if (currentPage === "operation") {
    configs.push({
      id: "start",
      label: isRunning ? "Stop" : "Start",
      onClick: isRunning ? handleStop : handleStart,
      backgroundColor: isRunning ? "#F44336" : "#2196F3",
      textColor: "white",
      visible: true,
      disabled: false,
    })
  }

  // Calibration page buttons
  if (currentPage === "calibration") {
    configs.push({
      id: "freedrive",
      label: isFreeDriveEnabled ? "Disable Free Drive" : "Enable Free Drive",
      onClick: toggleFreeDrive,
      backgroundColor: isFreeDriveEnabled ? "#CBD5E0" : undefined,
      borderColor: !isFreeDriveEnabled ? "#E2E8F0" : undefined,
      textColor: "black",
      visible: true,
      disabled: false,
    })
  }

  return configs
}, [currentPage, isRunning, isFreeDriveEnabled])

Migration from Imperative API

If you're migrating from the old compound component pattern:

Before (compound components):

<StatusTopBar statusLabel="Ready" dotColor="#4CAF50" visibleButtons={["start"]} disabledButtons={["stop"]}>
  <StatusTopBar.Button id="start" label="Start" onClick={handleStart} backgroundColor="#2196F3" textColor="white" />
  <StatusTopBar.Button id="stop" label="Stop" onClick={handleStop} backgroundColor="#F44336" textColor="white" />
</StatusTopBar>

After (config-based):

<StatusTopBar
  status={{ label: "Ready", color: "#4CAF50" }}
  buttonConfigs={[
    { id: "start", label: "Start", onClick: handleStart, backgroundColor: "#2196F3", textColor: "white", visible: true },
    { id: "stop", label: "Stop", onClick: handleStop, backgroundColor: "#F44336", textColor: "white", visible: false },
  ]}
/>

Benefits of the new API:

  • Single source of truth for button state
  • No need for refs or imperative methods
  • Easier to test (just pass different configs)
  • Better TypeScript support
  • More predictable behavior

Logs

Reusable logs components for filtering, sorting, and displaying machine logs.

Exports

import { LogsPanel, LogsTable, LogFilterForm, LogsPagination } from "@vention/machine-apps-components"
import type {
  LogEntry,
  LogType,
  LogFilterFormValues,
  SortOrder,
  LogsPanelHandle,
  PaginationConfig,
  PaginationMode,
  FetchParams,
  FetchResult,
} from "@vention/machine-apps-components"

Types

type LogType = "error" | "warning" | "info"

interface LogEntry {
  id: string
  date: string // ISO or locale string parsable by Date
  level: LogType
  code: string
  message: string
  description: string
}

type SortOrder = "latest" | "oldest"

interface LogFilterFormValues {
  fromDate?: string // YYYY-MM-DD
  toDate?: string // YYYY-MM-DD
  logType?: LogType
  sortOrder: SortOrder
}

type PaginationMode = "pagination" | "infinite-scroll" | "none"

interface PaginationConfig {
  mode: PaginationMode
  pageSize?: number // Default: 10
  initialPage?: number // Default: 1
}

// Parameters passed to the dataFetcher
interface FetchParams {
  filters: LogFilterFormValues // Current filter state
  page: number // Current page number
  pageSize: number // Items per page
}

// Expected return type from dataFetcher
interface FetchResult {
  logs: LogEntry[] // The log entries for this page
  totalCount: number // Total number of logs (after filtering)
  totalPages: number // Total number of pages
  currentPage: number // Current page number
  hasMore: boolean // Whether there are more pages available
}

// Type definition for data fetcher
type DataFetcher = (params: FetchParams) => Promise<FetchResult>

interface LogsPanelHandle {
  refresh: () => Promise<void>
  resetFilters: () => void
  applyFilters: (filters: Partial<LogFilterFormValues>) => void
  getCurrentFilters: () => LogFilterFormValues
  exportLogs: () => LogEntry[]
}

LogsPanel

A composite component that combines filter UI and table display with loading states and error handling. The component is a pure presentation component - it displays data and manages UI state, while delegating all data operations (filtering, sorting, pagination) to your dataFetcher function.

Features:

  • 🔄 Automatic loading state with spinner when fetching data
  • ❌ Built-in error display with VentionAlert
  • 🔍 Filter UI for date range and log type
  • ↕️ Sort UI for latest/oldest
  • 📅 Date formatting (YYYY-MM-DD h:mm:ssa)
  • 🎨 Type-based icons (error, warning, info)
  • 📄 Pagination support (standard pagination or infinite scroll)
  • 🎯 Imperative API for programmatic control
  • 🔔 Event callbacks for integration
  • 🎨 Customizable styling and empty states
  • ⚡ Performance optimized with React.memo
How It Works

The component calls your dataFetcher function whenever filters, sort order, or page changes. You implement the filtering, sorting, and pagination logic, and return the results:

import { LogsPanel } from "@vention/machine-apps-components"
import type { FetchParams, FetchResult } from "@vention/machine-apps-components"

const fetchLogs = async (params: FetchParams): Promise<FetchResult> => {
  // params contains:
  // - params.filters.fromDate
  // - params.filters.toDate
  // - params.filters.logType
  // - params.filters.sortOrder
  // - params.page
  // - params.pageSize

  const response = await fetch(
    `/api/logs?${new URLSearchParams({
      fromDate: params.filters.fromDate || "",
      toDate: params.filters.toDate || "",
      logType: params.filters.logType || "",
      sortOrder: params.filters.sortOrder,
      page: params.page.toString(),
      pageSize: params.pageSize.toString(),
    })}`
  )

  const data = await response.json()

  return {
    logs: data.logs,
    totalCount: data.total,
    totalPages: Math.ceil(data.total / params.pageSize),
    currentPage: params.page,
    hasMore: params.page < Math.ceil(data.total / params.pageSize),
  }
}

;<LogsPanel
  dataFetcher={fetchLogs}
  pagination={{
    mode: "pagination",
    pageSize: 20,
  }}
/>

Important: When using dataFetcher, you are responsible for:

  • ✅ Filtering logs by date range and type
  • ✅ Sorting logs by date (latest/oldest)
  • ✅ Paginating the results
  • ✅ Returning pagination metadata (totalCount, totalPages, hasMore)

The component handles:

  • ✅ Rendering the filter UI
  • ✅ Managing filter state
  • ✅ Calling your dataFetcher when filters/page changes
  • ✅ Loading and error states
  • ✅ Displaying the results
Complete Client-Side Example

Here's a complete example showing how to implement filtering, sorting, and pagination on the client-side:

import { useCallback, useMemo } from "react"
import { LogsPanel } from "@vention/machine-apps-components"
import type { FetchParams, FetchResult, LogEntry } from "@vention/machine-apps-components"
import dayjs from "dayjs"

function LogsPage() {
  // Your data source (could come from props, context, etc.)
  const allLogs: LogEntry[] = useMemo(
    () => [
      { id: "1", date: "2025-10-07T14:00:00Z", type: "error", code: "ERR_001", message: "Error", description: "..." },
      { id: "2", date: "2025-10-07T13:00:00Z", type: "warning", code: "WARN_001", message: "Warning", description: "..." },
      { id: "3", date: "2025-10-07T12:00:00Z", type: "info", code: "INFO_001", message: "Info", description: "..." },
      // ... more logs
    ],
    []
  )

  const fetchLogs = useCallback(
    async (params: FetchParams): Promise<FetchResult> => {
      // Simulate network delay (optional)
      await new Promise(resolve => setTimeout(resolve, 500))

      // 1. Apply filtering
      let filtered = [...allLogs]

      // Filter by date range
      if (params.filters.fromDate) {
        const fromTimestamp = dayjs(params.filters.fromDate).valueOf()
        filtered = filtered.filter(log => dayjs(log.date).valueOf() >= fromTimestamp)
      }

      if (params.filters.toDate) {
        const toTimestamp = dayjs(params.filters.toDate).endOf("day").valueOf()
        filtered = filtered.filter(log => dayjs(log.date).valueOf() <= toTimestamp)
      }

      // Filter by log type
      if (params.filters.logType) {
        filtered = filtered.filter(log => log.type === params.filters.logType)
      }

      // 2. Apply sorting
      filtered.sort((a, b) => {
        const aTime = dayjs(a.date).valueOf()
        const bTime = dayjs(b.date).valueOf()
        return params.filters.sortOrder === "latest" ? bTime - aTime : aTime - bTime
      })

      // 3. Apply pagination
      const totalCount = filtered.length
      const totalPages = Math.ceil(totalCount / params.pageSize)
      const start = (params.page - 1) * params.pageSize
      const end = start + params.pageSize
      const paginated = filtered.slice(start, end)

      // 4. Return result
      return {
        logs: paginated,
        totalCount,
        totalPages,
        currentPage: params.page,
        hasMore: end < totalCount,
      }
    },
    [allLogs]
  )

  return (
    <LogsPanel
      dataFetcher={fetchLogs}
      pagination={{
        mode: "pagination",
        pageSize: 10,
      }}
    />
  )
}
Props
interface LogsPanelProps {
  // Required - Data fetcher function
  dataFetcher: (params: FetchParams) => Promise<FetchResult>

  // Optional - Initial state
  initialFilters?: Partial<LogFilterFormValues>

  // Optional - Error handling
  onError?: (error: unknown) => void

  // Optional - Pagination
  pagination?: PaginationConfig

  // Optional - Event callbacks
  onFilterChange?: (filters: LogFilterFormValues) => void
  onLogClick?: (log: LogEntry) => void

  // Optional - Customization
  className?: string
  tableHeight?: string | number
  emptyStateMessage?: string
  emptyStateIcon?: ReactNode
}
States
  • Loading: Shows VentionSpinner with "Loading logs..." message
  • Error: Displays VentionAlert with error title and description
  • Empty: Shows customizable empty state message
  • Success: Renders filterable/sortable table with data
Initial Filters
<LogsPanel
  data={logs}
  initialFilters={{
    fromDate: "2025-09-01",
    toDate: "2025-09-30",
    logType: "error",
    sortOrder: "latest",
  }}
/>
Pagination
// Standard pagination (default: 10 items per page)
<LogsPanel
  data={logs}
  pagination={{
    mode: "pagination",
    pageSize: 20,
    initialPage: 1,
  }}
/>

// Infinite scroll (loads more as you scroll)
<LogsPanel
  data={logs}
  pagination={{
    mode: "infinite-scroll",
    pageSize: 10,
  }}
/>

// No pagination (show all logs)
<LogsPanel
  data={logs}
  pagination={{ mode: "none" }}
/>

// Default behavior (no pagination prop = show all logs)
<LogsPanel data={logs} />
Event Callbacks

Get notified when user interacts with the component:

import { LogsPanel } from "@vention/machine-apps-components"
import type { LogEntry, LogFilterFormValues } from "@vention/machine-apps-components"
;<LogsPanel
  data={logs}
  onFilterChange={(filters: LogFilterFormValues) => {
    console.log("User changed filters:", filters)
    // Track analytics, sync to URL params, etc.
  }}
  onLogClick={(log: LogEntry) => {
    console.log("User clicked log:", log)
    // Show details modal, navigate to details page, etc.
  }}
/>
Imperative API

Control the component programmatically using a ref:

import { useRef } from "react"
import { LogsPanel } from "@vention/machine-apps-components"
import type { LogsPanelHandle } from "@vention/machine-apps-components"

function MyComponent() {
  const logsPanelRef = useRef<LogsPanelHandle>(null)

  const handleRefresh = async () => {
    // Manually refresh async data
    await logsPanelRef.current?.refresh()
  }

  const handleResetFilters = () => {
    // Reset all filters to default
    logsPanelRef.current?.resetFilters()
  }

  const handleShowErrors = () => {
    // Programmatically apply filters
    logsPanelRef.current?.applyFilters({ logType: "error" })
  }

  const handleExport = () => {
    // Get current filtered/sorted logs
    const logs = logsPanelRef.current?.exportLogs()
    if (logs) {
      // Export to CSV, JSON, etc.
      downloadAsCSV(logs)
    }
  }

  const handleGetFilters = () => {
    // Get current filter state
    const filters = logsPanelRef.current?.getCurrentFilters()
    console.log("Current filters:", filters)
  }

  return (
    <>
      <div>
        <button onClick={handleRefresh}>Refresh</button>
        <button onClick={handleResetFilters}>Reset Filters</button>
        <button onClick={handleShowErrors}>Show Errors Only</button>
        <button onClick={handleExport}>Export Logs</button>
      </div>
      <LogsPanel ref={logsPanelRef} data={fetchLogs} />
    </>
  )
}

Available Methods:

  • refresh() - Manually re-fetch data with current filters/page
  • resetFilters() - Reset all filters to their default values
  • applyFilters(filters) - Programmatically set filters
  • getCurrentFilters() - Get the current filter state
  • exportLogs() - Get currently displayed logs array
Customization

Customize the appearance and behavior:

import { LogsPanel } from "@vention/machine-apps-components"
import { VentionIcon } from "@ventionco/machine-ui"
;<LogsPanel
  data={logs}
  className="my-custom-logs-panel"
  tableHeight="600px"
  emptyStateMessage="No logs match your filters"
  emptyStateIcon={<VentionIcon type="inbox" size={48} color="gray" />}
/>

Customization Props:

  • className - Add custom CSS class to the root container
  • tableHeight - Set custom table height (e.g., "500px", 600)
  • emptyStateMessage - Custom message when no logs (default: "You have no logs")
  • emptyStateIcon - Custom React node to display above empty message

LogsTable

Just the table component. Useful if you want to handle filtering/sorting yourself or compose your own custom layout with filters.

import { LogsTable } from "@vention/machine-apps-components"
import type { LogEntry } from "@vention/machine-apps-components"

<LogsTable 
  logs={logs} 
  onLogClick={log => showDetailsModal(log)} 
  tableHeight="500px" 
  emptyStateMessage="No logs available" 
/>

Props:

interface LogsTableProps {
  logs?: LogEntry[]
  isLoading?: boolean
  error?: string | null
  onLoadMoreLogs?: () => void // For infinite scroll
  hasMoreLogs?: boolean // For infinite scroll
  onLogClick?: (log: LogEntry) => void
  tableHeight?: string | number
  emptyStateMessage?: string
  emptyStateIcon?: ReactNode
}

LogFilterForm

Just the filter UI. Use this when you want full control over the filtering logic.

import { LogFilterForm } from "@vention/machine-apps-components"
import type { LogFilterFormValues } from "@vention/machine-apps-components"

const handleFilterChange = (filters: LogFilterFormValues) => {
  // Apply filters to your own data source
  const filtered = myLogs.filter(log => {
    // Your custom filter logic
  })
}

const handleReset = () => {
  // Handle reset
}

;<LogFilterForm onFilterChange={handleFilterChange} onReset={handleReset} initialFilters={{ logType: "error" }} />

LogsPagination

Just the pagination controls. Use this for custom pagination implementations.

import { LogsPagination } from "@vention/machine-apps-components"
;<LogsPagination currentPage={currentPage} totalPages={totalPages} onPageChange={page => setCurrentPage(page)} />

Development

Running Tests

# Run tests
nx test machine-apps-components

# Run tests with coverage
nx test machine-apps-components --coverage

# Run tests in watch mode
cd projects/machine-code/libs/machine-apps-components
npx vitest

Testing Setup

This library uses Vitest with jsdom environment for testing React components. Test utilities are provided in src/test-utils.tsx that wrap components with necessary providers:

  • ThemeProvider with machineUiTheme
  • MemoryRouter for routing

Example test:

import { renderWithProviders } from "../../test-utils"
import { NavigationBar } from "./navigation-bar"

it("should render correctly", () => {
  renderWithProviders(<NavigationBar navigationItems={mockNavigationItems} />)
  expect(screen.getByText("Operation")).toBeDefined()
})

Building

nx build machine-apps-components

Linting

nx lint machine-apps-components