@artube/ui
v2.0.2
Published
Artube UI
Keywords
Readme
@artube/ui Integration Handbook
Overview
@artube/uibundles 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. ArtubeUIFacadediff-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
Install the package from your internal registry (example is npm syntax):
npm install @artube/uiImport the distributed stylesheet once in your bundle so the HUD has baseline styling:
import '@artube/ui/style.css';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_URLin 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
- Prepare config builders – create functions that map your game state to
Partial<ButtonsConfig>andPartial<ModalsConfig>. - Instantiate – create
new ArtubeUIFacade(). - Mount – call
artubeUI.init(targetHTMLElement)once you have a DOM container. The widget renders itself inside that element. - Provide initial state – call
updateButtons(...)andupdateModals(...)with baseline values (visibility, labels, callbacks, modal payloads). - 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:
updateButtonsis shallow by section (spin,autoplay,betPanel, etc.) and each section is merged field-by-field.updateModalssupports partial top-level modal updates;bet,menu, andautoplaycan also be updated incrementally while preserving previous values.- Visibility can be toggled independently (
visibleonly) 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)payoutsdescribes 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 (descriptionis 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)
- Bootstrap data providers
- Implement helpers (like
getPaytableData(formatFn, bet)orgetRulesData(rtp)) that return the exact data objects the widget expects. - Keep them free from UI concerns so other games can reuse them.
- Implement helpers (like
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',
});Create Artube UI
import { ArtubeUIFacade } from '@artube/ui'; const artubeUI = new ArtubeUIFacade(); artubeUI.init(document.getElementById('artube-ui-container'));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'), }, });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
updateModalswith the affected slices.
- When the balance or bet changes, call
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-containerstyles 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.cssrule that forcesbuttonto 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
updateButtonswith 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.
