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

@artube/ui

v2.0.2

Published

Artube UI

Readme

@artube/ui Integration Handbook

Overview

  • @artube/ui bundles a complete HUD and modal system for slot-style games. It ships UI primitives (buttons, selectors, panels) and high-level flows (menu, bet/autoplay/exit dialogs, FRC modals, etc.).
  • The package is framework-agnostic: you only interact with plain classes, data objects, and DOM nodes. No assumptions about React, MobX, or any other state library.
  • The core surface is ArtubeUIFacade. You instantiate it, mount it, then push state with:
    • updateButtons(partialButtonsConfig)
    • updateModals(partialModalsConfig)
  • Interaction callbacks (button clicks, modal actions, tab changes, selector changes) are passed inside those configs as function fields.

Architecture Diagram

flowchart LR
  subgraph Host_Game
    A[Game Services & State Stores] -->|builds data| B[ButtonsConfig / ModalsConfig builders]
  end

  B -->|partial updates| D[ArtubeUIFacade]

  D -->|updateButtons / updateModals| E[Widget DOM Tree]
  E -->|user actions| F[callbacks from current config]
  F -->|analytics /\ngameplay| A
  • The host game owns all authoritative data and passes plain objects (including callbacks) into ArtubeUIFacade.
  • ArtubeUIFacade diff-applies updates to its internal DOM; the host never manipulates widget markup directly.
  • User interactions travel back through the latest callbacks provided in button/modal config.

Installation & Assets

  1. Install the package from your internal registry (example is npm syntax):

    npm install @artube/ui
  2. Import the distributed stylesheet once in your bundle so the HUD has baseline styling:

    import '@artube/ui/style.css';
  3. Make sure the host game copies any referenced image/audio assets into its own build output. The widget expects resolved URLs (e.g., via import.meta.env.BASE_URL in Vite or a similar helper in other bundlers).

Container Styling

The widget renders into a plain DOM node. Give that node predictable sizing and positioning so the HUD overlaps your renderer correctly.

.artube-ui-container {
  position: fixed;
  top: 0;
  left: 50%;
  transform: translateX(-50%);
  pointer-events: none;

  width: 100%;
  height: 100%;
  max-width: 100vw;
  max-height: 100vh;
}

@media screen and (orientation: landscape) {
  .artube-ui-container {
    aspect-ratio: 9 / 16;
    width: auto;
  }
}

Lifecycle

  1. Prepare config builders – create functions that map your game state to Partial<ButtonsConfig> and Partial<ModalsConfig>.
  2. Instantiate – create new ArtubeUIFacade().
  3. Mount – call artubeUI.init(targetHTMLElement) once you have a DOM container. The widget renders itself inside that element.
  4. Provide initial state – call updateButtons(...) and updateModals(...) with baseline values (visibility, labels, callbacks, modal payloads).
  5. React to game state – whenever game data changes, call:
    • updateButtons(partialButtonsConfig) to adjust HUD buttons/panels.
    • updateModals(partialModalsConfig) to toggle modal visibility and content.

API Surface

ArtubeUIFacade

| Member | Description | | ----------------------------------------------- | ------------------------------------------------------------------------ | | new ArtubeUIFacade() | Creates the widget instance. | | init(target: HTMLElement) | Mounts the widget into the provided DOM node. Must be called once. | | updateButtons(update: Partial<ButtonsConfig>) | Updates only provided HUD button/panel slices (including callbacks). | | updateModals(update: Partial<ModalsConfig>) | Updates only provided modal slices (including callbacks and visibility). |

Partial update behavior:

  • updateButtons is shallow by section (spin, autoplay, betPanel, etc.) and each section is merged field-by-field.
  • updateModals supports partial top-level modal updates; bet, menu, and autoplay can also be updated incrementally while preserving previous values.
  • Visibility can be toggled independently (visible only) without resending full payloads.

ButtonsConfig & HUD Panels

| Key | Type | Notes | | ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | | ui.visible | visible: boolean | Master switch that shows or hides the entire HUD layer. | | sound | visible: booleanenabled: booleanloading: booleansoundsEnabled: booleanclickAction: () => void | Represent the mute button state; use loading during transitions and soundsEnabled to reflect the audio engine state. | | speed | visible: booleanenabled: booleanisActive: booleanclickAction: () => void | Drives the turbo/quick-spin toggle; isActive highlights whether the faster mode is currently applied. | | spin | visible: booleanenabled: booleancounter: numbervisibleCounter: booleanspinning: booleanclickAction: () => voidcontinuousSpin: { delay: number; enabled: boolean; onActiveChange: (active: boolean) => void} | Controls spin visuals and counters; continuousSpin.onActiveChange is fired by hold/toggle behavior. | | autoplay | visible: booleanenabled: booleanmode: 'start' \| 'stop'counter: number \| nullspinning: booleanclickAction: () => void | Reflects autoplay availability; mode names the primary action, counter shows spins left, spinning indicates active autoplay. | | menu, bet, bonus, gamble, take | visible: booleanenabled: booleanclickAction: () => void | Gate access to each action button; toggle enabled based on game rules (e.g., disable bet while reels spin). | | betPanel | values: number[]currentValue: numberformat: (value: number) => stringonValueChange: (value: number, index: number) => voidtitle?: stringtitleInside?: booleanenabled: boolean | Presents selectable bet options; wire onValueChange to server bet updates and use enabled to block interaction mid-spin. | | balancePanel, winPanel | visible: booleantitle: stringvalue: numberformat: (value: number) => string | Display running balance or last win with custom formatting and localization. | | frcPanel | visible: booleanleft: numbertotal: number | Shows remaining/total free rounds for FRC flows. | | promoPanel | visible: booleantext: string | Surfaces promotional copy or campaign info; keep hidden if unused. |

Button actions are host-owned: pass clickAction in each interactive button slice and update it whenever handler references change.

ModalsConfig

| Modal | Common fields | Notes | | -------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------- | | menu | visible: booleanpaytable: { title: string; data: PaytableProps }rules: { title: string; data: RulesProps }settings: { title: string; data: SettingsProps }lobby: { visible: boolean; onLobbyClick: () => void }onClose: () => voidonUIClick: () => voidonTabSwitch: (index: number) => void | Combo modal that bundles paytable, rules, settings, and an optional lobby shortcut. | | bet | visible: booleanlines: SelectorPropsbets: BetSelectorPropspayment: { currentOption: 'money' \| 'credits'; onChange: (option: 'money' \| 'credits') => void }onClose: () => voidonCancel: () => void | Lets players tweak bet size and line count, and swap currency mode if enabled. | | autoplay | visible: booleantitle: stringoptions: number[]onOptionChange: (option: number, index: number) => voidonClose: () => voidonCancel: () => void | Presents a list of predefined spin counts and reports user selection back to the host game. | | exitLobby | visible: booleandescription: stringyesButton: stringnoButton: stringonAccept: () => voidonCancel: () => void | Confirmation gate before leaving the current game session. | | reconnect | visible: booleanheader: stringalertingText: string | Read-only reconnect notice; host supplies copy tied to backend state. | | error | visible: booleanheader: stringdescription: stringbuttonText: stringonClick: () => void | Generic fatal error dialog with a single CTA (e.g., reload). | | insufficient | visible: booleanheader: stringdescription: stringbuttonText: stringonClick: () => void | Specialized error for low balance; often paired with bet adjustments. | | limit | visible: booleanheader: stringdescription: stringbuttonText: stringonClick: () => void | Limit-reached messaging (same schema as error). | | frcInfo | visible: booleandescription: stringtapToContinue: stringonClose: () => void | Informational Free Round Campaign modal explaining deferred rewards. | | frcNew | visible: booleancampaign: stringtitle: stringvalidTo: stringyesButton: stringnoButton: stringonAccept: () => voidonCancel: () => void | FRC prompt offering players new free rounds with accept/cancel flows. | | frcWin | visible: booleanheader: stringtotalWin: stringtapToContinue: stringonClose: () => void | Summarizes FRC winnings and waits for player acknowledgment. | | buyMore | visible: boolean plus purchase-specific props (amounts, copy, callbacks) | Optional upsell modal for additional bonuses/spins. | | buyFeature | visible: boolean plus props for showcasing purchasable features (carousel items, CTA callbacks) | Enables direct access to buy-feature mechanics when provided by the game. |

Each modal entry follows the “visible + props + callbacks” shape. Omitted modals simply stay hidden.

Menu Content & Components

  • Paytable (PaytableProps)

    • payouts describes payouts table, e.g.

      payouts: {
        format: value => currency.format(value),
        bet: balance.visibleBet,
        symbols: [
          {
            symbolName: 'high1',
            imageUrl: assets.basePath('images/paytable/high1.webp'),
            payouts: [
              { count: 3, factor: 10 },
              { count: 4, factor: 50 },
              { count: 5, factor: 1000 },
            ],
          },
        ],
      }
    • symbols: describes symbols and their details, e.g.:

      symbols: {
        title: t('menu.paytable.special'),
        symbols: [
          {
            symbolName: t('menu.paytable.high1'),
            imageUrl: assets.basePath('images/paytable/high1.webp'),
            points: [
              t('menu.paytable.high.t1'),
              t('menu.paytable.high.t2'),
            ],
          },
        ],
      }
    • paylines: describes how paylines are built dynamically, e.g.

      paylines: {
        title: t('menu.paytable.lines'),
        paylines: [
          {
            columns: 5,
            rows: 3,
            paylines: [
              { rowId: 1, paylineId: '1', indices: [1, 1, 1, 1, 1] },
              { rowId: 0, paylineId: '2', indices: [0, 0, 0, 0, 0] },
            ],
          },
        ],
      }
  • Rules (RulesProps)

    • sections: describes list of sections (description is optional), e.g.:

      sections: [
        {
          title: t('menu.rules.about.header'),
          description: t('menu.rules.about.description').replace('{gameName}', `<b>${gameName}</b>`),
          points: [t('menu.rules.about.payouts'), t('menu.rules.about.paylines'), t('menu.rules.about.volatility')],
        },
      ];
    • info: describes game info, e.g.:

      info: {
        gameName: 'Cash Machine 5',
        version: GAME_VERSION,
      }
  • Settings
    Combine toggles, selections, and selector widgets to drive menu settings and the credits block.

    const settingsData: SettingsProps = {
      settings: [
        {
          label: t('menu.settings.spacebar'),
          enabled: gameSettingsStore.isSpaceBarToSpin,
          onChange: (enabled) => gameSettingsStore.setIsSpaceBarToSpin(enabled),
        },
        {
          label: t('menu.settings.language'),
          options: ['English', 'Português', 'Español', 'Deutsch'],
          currentOption: localizationStore.currentLanguage,
          onChange: (lang) => localizationStore.setLanguage(lang),
        },
      ],
      credits: {
        enabled: true,
        settings: {
          label: 'Balance in Credits',
          enabled: gameSettingsStore.useCredits,
          onChange: (useCredits) => gameSettingsStore.setUseCredits(useCredits),
        },
        conversion: {
          title: `1 Credit = ${dataStore.currency}`,
          values: balanceStore.conversionRates,
          currentValue: balanceStore.currentConversionRate,
          format: (value) => value.toString(),
          onValueChange: (value) => console.log('conversion changed', value),
        },
      },
    };

    The selector interfaces used above share a consistent shape:

    const betSelector: SelectorProps = {
      title: 'Bet',
      values: gameSettingsStore.useCredits ? balanceStore.allowedCredits : balanceStore.allowedBets,
      currentValue: gameSettingsStore.useCredits ? balanceStore.visibleCredit : balanceStore.visibleBet,
      format: (value) => formatCurrency(value),
      onValueChange: (value, index) => balanceStore.setServerBetFromIndex(index),
      titleInside: true,
      enabled: !stateMachine.isSpinning,
    };

    Reuse the same structure for menu selectors, modal bet sliders, or the credits conversion block.

Integration Workflow (Framework-Agnostic)

  1. Bootstrap data providers
    • Implement helpers (like getPaytableData(formatFn, bet) or getRulesData(rtp)) that return the exact data objects the widget expects.
    • Keep them free from UI concerns so other games can reuse them.

Example Data Builders

import { assets } from '../services/assets';
import { formatCurrency } from '../utils/number';

type FormatAmount = (value: number) => string;

export function getPaytableData(formatAmount: FormatAmount, currentBet: number): PaytableProps {
  return {
    payouts: {
      format: (value) => formatAmount(value * currentBet),
      bet: currentBet,
      symbols: [
        {
          symbolName: 'high1',
          imageUrl: assets.basePath('images/paytable/high1.webp'),
          payouts: [
            { count: 3, factor: 5 },
            { count: 4, factor: 25 },
            { count: 5, factor: 500 },
          ],
        },
        {
          symbolName: 'wild',
          imageUrl: assets.basePath('images/paytable/wild.webp'),
          payouts: [
            { count: 3, factor: 10 },
            { count: 4, factor: 50 },
            { count: 5, factor: 1000 },
          ],
        },
      ],
    },
    symbols: {
      title: 'Special Symbols',
      symbols: [
        {
          symbolName: 'Wild',
          imageUrl: assets.basePath('images/paytable/wild.webp'),
          points: ['Substitutes for all symbols except Scatter', 'Doubles any win it participates in'],
        },
        {
          symbolName: 'Scatter',
          imageUrl: assets.basePath('images/paytable/scatter.webp'),
          points: ['Pays on any position', '3+ awards Free Spins'],
        },
      ],
    },
    paylines: {
      title: 'Paylines',
      paylines: [
        {
          columns: 5,
          rows: 3,
          paylines: [
            { rowId: 1, paylineId: '1', indices: [1, 1, 1, 1, 1] },
            { rowId: 0, paylineId: '2', indices: [0, 0, 0, 0, 0] },
          ],
        },
      ],
    },
  };
}

type RulesContext = {
  rtp: number;
  version: string;
  gameName: string;
};

export function getRulesData(ctx: RulesContext): RulesProps {
  return {
    sections: [
      {
        title: 'About the Game',
        description: `${ctx.gameName} is a high volatility slot with classic symbols.`,
        points: [
          `RTP: ${ctx.rtp}%`,
          'Wins are paid from left to right on active paylines.',
          'Scatter wins pay on any position.',
        ],
      },
      {
        title: 'Free Spins',
        points: ['3+ Scatter symbols award 10 Free Spins.', 'Retriggers add 5 additional Free Spins.'],
      },
    ],
    info: {
      gameName: ctx.gameName,
      version: ctx.version,
    },
  };
}

// Usage
const paytableData = getPaytableData(formatCurrency, wagers.current);
const rulesData = getRulesData({
  rtp: gameConfig.rtp,
  version: GAME_VERSION,
  gameName: 'Cash Machine 5',
});
  1. Create Artube UI

    import { ArtubeUIFacade } from '@artube/ui';
    
    const artubeUI = new ArtubeUIFacade();
    
    artubeUI.init(document.getElementById('artube-ui-container'));
  2. Set initial state

    artubeUI.updateButtons({
      ui: { visible: false },
      spin: {
        visible: true,
        enabled: false,
        spinning: false,
        visibleCounter: false,
        counter: 0,
        clickAction: () => game.startSpin(),
        continuousSpin: {
          enabled: false,
          delay: 0,
          onActiveChange: (active) => analytics.track('continuous-spin', active),
        },
      },
      autoplay: {
        visible: true,
        enabled: true,
        mode: 'start',
        counter: null,
        spinning: false,
        clickAction: () => toggleAutoplay(),
      },
      menu: {
        visible: true,
        enabled: true,
        clickAction: () => modalStore.open('menu'),
      },
      bet: {
        visible: true,
        enabled: true,
        clickAction: () => modalStore.open('bet'),
      },
      // ...other buttons/panels
    });
    
    artubeUI.updateModals({
      menu: {
        visible: false,
        paytable: { title: t('menu.paytable.header'), data: getPaytableData(formatAmount, balance.currentBet) },
        rules: { title: t('menu.rules.header'), data: getRulesData(game.rtp) },
        settings: { title: t('menu.settings.header'), data: settingsData },
        lobby: { visible: Boolean(game.lobbyUrl), onLobbyClick: () => navigation.openLobby() },
        onClose: () => modalStore.close('menu'),
        onUIClick: () => sound.play('ui-click'),
        onTabSwitch: (index) => analytics.track('menu-tab-switch', { index }),
      },
      autoplay: {
        visible: false,
        title: t('autoplay.title'),
        options: [10, 20, 50, 100],
        onOptionChange: (option, index) => autoplay.select(option, index),
        onClose: () => modalStore.close('autoplay'),
        onCancel: () => modalStore.close('autoplay'),
      },
    });
  3. Wire runtime updates

    • When the balance or bet changes, call artubeUI.updateButtons({ balancePanel: { value: balance.amount } }).
    • When the game enters/exits states (spinning, auto-play, gamble), update the corresponding button states.
    • When a modal should open, e.g. artubeUI.updateModals({ bet: { visible: true, ... } }).
    • When menu/modal payload changes (e.g. localization), call updateModals with the affected slices.
  4. Cleanup (if needed)

    • If the host game hot-reloads or swaps layouts, dispose of the DOM node and create a new widget instance to avoid stale callbacks.

Runtime Patterns & Examples

Synchronizing Auto-Play

autoplay.onSpinsLeftChange((spinsLeft) => {
  artubeUI.updateButtons({
    autoplay: {
      visible: true,
      enabled: true,
      mode: spinsLeft ? 'stop' : 'start',
      counter: spinsLeft,
      spinning: spinsLeft !== null,
    },
  });
});

Updating Bet Selector

const betSelector = {
  values: wagers.getAllowedBets(),
  currentValue: wagers.current,
  format: (value) => currency.format(value),
  onValueChange: (value, index) => {
    wagers.select(index);
    analytics.track('bet-change', { value, index });
  },
};

artubeUI.updateButtons({
  betPanel: { title: 'Bet', titleInside: true, enabled: true, ...betSelector },
});

artubeUI.updateModals({
  bet: {
    visible: modalStore.isBetOpen(),
    bets: { ...betSelector, minLabel: 'Min', maxLabel: 'Max' },
    payment: {
      currentOption: settings.useCredits ? 'credits' : 'money',
      onChange: (option) => settings.setUseCredits(option === 'credits'),
    },
    onClose: () => modalStore.close('bet'),
    onCancel: () => modalStore.close('bet'),
  },
});

Updating the Bet Modal

const betModalPayload = {
  lines: {
    title: 'Lines',
    values: lines.getLines(),
    currentValue: lines.currentLine,
    format: (value) => value.toString(),
    onValueChange: (value) => lines.setLines(value),
  },
  bets: {
    ...betSelector,
    minLabel: 'Min',
    maxLabel: 'Max',
  },
  payment: {
    currentOption: settings.useCredits ? 'credits' : 'money',
    onChange: (option) => settings.setUseCredits(option === 'credits'),
  },
  onClose: () => modalStore.close('bet'),
  onCancel: () => modalStore.close('bet'),
};

modalStore.onBetVisibilityChange((visible) => {
  artubeUI.updateModals({
    bet: {
      ...betModalPayload,
      visible,
    },
  });
});

Partial Update Examples (Minimal Payloads)

The snippets below are intentionally minimal and update only the changed fields.

// Partial update example: toggle only menu visibility
artubeUI.updateModals({
  menu: { visible: true },
});

// Partial update example: close only bet modal
artubeUI.updateModals({
  bet: { visible: false },
});

// Partial update example: change only autoplay mode/counter
artubeUI.updateButtons({
  autoplay: {
    counter: 12,
  },
});

// Partial update example: update only balance numeric value
artubeUI.updateButtons({
  balancePanel: { value: balance.amount },
});

// Partial update example: swap only bet payment option
artubeUI.updateModals({
  bet: {
    payment: {
      currentOption: 'credits',
    },
  },
});

Troubleshooting

  • HUD misaligned or clipped: confirm the artube-ui-container styles are applied (fixed positioning, full viewport) and adjust the media-query aspect ratio to match your renderer’s safe area.
  • Buttons show unexpected skin: keep the style.css rule that forces button to drop default button skin. Without it, Safari/iOS reintroduce native gradients/borders that clash with the widget skin.
button {
  -webkit-appearance: none;
  appearance: none;
  background-color: transparent;
  border: none;
}

Best Practices

  • Separate data from reactions – build pure functions (formatAmount, getPaytableData, getRulesData) so you can reuse them across games.
  • Batch updates where possible – calling updateButtons with multiple keys is cheaper than firing many single-key updates in quick succession.
  • Avoid framework leakage – only expose primitive data structures to the widget. Whether you derive them from React state, MobX stores, or vanilla services is irrelevant to @artube/ui.