The Consumer UI Kit provides a complete, pre-built solution for building select and pour interfaces. Built on the DeviceAssembly → Nozzle → Availability foundation, CUI Kit handles the complexity of brand/beverage/flavor navigation, selection tracking, timer management, and pour integration.
What CUI Kit Provides
CUI Kit delivers a complete selection workflow out of the box:
- Brand → Beverage → Flavor navigation – Complete drill-down workflow with state management
- Selection tracking – Synchronized local and backend selection state
- Timer lifecycle management – Automatic ambient/attract mode timer coordination
- Pour integration – Validation hooks and context management
- EventBus integration – Analytics events for all user interactions
- Companion model support – Seamless integration with custom companion models
- Type safety – Full TypeScript support with generic data types
The useCuiKit Hook
The useCuiKit hook is the entry point to the CUI Kit system. It requires a NozzleProvider parent and returns a comprehensive kit object with all selection state and handlers.
Basic Setup
import { useCuiKit } from '@kosdev-code/kos-dispense-sdk';
function SelectAndPourScreen() {
const kit = useCuiKit({
initialGroup: 'brands', // or 'beverages'
});
// kit now contains navigation, selections, group handlers, pour, and timers
}Type Parameters
CUI Kit supports generic type parameters for custom data shapes:
interface BrandData {
logoUrl: string;
tagline: string;
}
interface BeverageData {
nutritionUrl: string;
calories: number;
}
interface FlavorData {
colorCode: string;
}
const kit = useCuiKit<BrandData, BeverageData, FlavorData>({
initialGroup: 'brands'
});
// kit.brands.models are typed as AvailabilityModel<BrandData>[]
// kit.beverages.models are typed as AvailabilityModel<BeverageData>[]
// kit.flavors.models are typed as AvailabilityModel<FlavorData>[]Companion Model Integration
If you’ve created companion models for brands, beverages, or flavors, configure them in the options:
const kit = useCuiKit<BrandData, BeverageData, FlavorData,
BrandCompanionModel, BeverageCompanionModel, FlavorCompanionModel>({
initialGroup: 'brands',
options: {
companionTypes: {
brand: 'BrandCompanion',
beverage: 'BeverageCompanion',
flavor: 'FlavorCompanion'
}
}
});
// Access companions alongside models
kit.brands.companions // BrandCompanionModel[]
kit.beverages.companions // BeverageCompanionModel[]
kit.flavors.companions // FlavorCompanionModel[]Navigation State (kit.nav)
CUI Kit manages navigation between brand, beverage, and flavor groups.
Navigation Properties
kit.nav.currentGroup // Current group: 'brands' | 'beverages' | 'flavors'Navigation Methods
// Navigate back to previous level
kit.nav.back()
// Return to initial group (clears selections)
kit.nav.home()Navigation Behavior
- back() – Moves to previous level, clearing selections appropriately
- From brands → clears brand and beverage selections
- From beverages or flavors → returns to initialGroup, clears selections
- home() – Returns to initialGroup, publishes home event
- Clears selections based on initialGroup
- Publishes
/kos/dispense/{nozzlePath}/home/topic
Selection State (kit.selections)
Access current selections across all levels:
// Currently selected brand (AvailabilityModel or null)
kit.selections.brands
// Currently selected beverage (AvailabilityModel or null)
kit.selections.beverages
// Currently selected flavors (AvailabilityModel[] or null)
kit.selections.flavorsThese selections reflect both local state and backend selection state in the NozzleModel.
Group Objects
CUI Kit provides three group objects (kit.brands, kit.beverages, kit.flavors) with identical structure:
Group Structure
{
// Array of AvailabilityModels for display
models: AvailabilityModel<T>[]
// Companion models (if configured)
companions: CompanionModel[]
// Clear selection at this level
clear: () => void
// Selection handler
handlers: {
onSelected: (model: AvailabilityModel<T>) => void
}
}Brand Group (kit.brands)
// Display all visible brands
kit.brands.models.map(brand => (
<BrandCard
key={brand.id}
brand={brand}
onClick={() => kit.brands.handlers.onSelected(brand)}
/>
))
// Access brand companions
kit.brands.companions // BrandCompanionModel[]
// Clear brand selection (also clears beverage)
kit.brands.clear()Behavior: Selecting a brand updates kit.selections.brands, publishes selection event, and navigates to flavor group.
Beverage Group (kit.beverages)
// Display beverages (filtered by brand if selected)
kit.beverages.models.map(beverage => (
<BeverageCard
key={beverage.id}
beverage={beverage}
onClick={() => kit.beverages.handlers.onSelected(beverage)}
/>
))
// Access beverage companions
kit.beverages.companions // BeverageCompanionModel[]
// Clear beverage selection
kit.beverages.clear()Behavior:
- If a brand is selected, shows beverages for that brand
- If no brand exists (beverage-only brandset), shows all beverages
- Selecting updates
kit.selections.beverages, publishes event, navigates to flavors
Flavor Group (kit.flavors)
// Display all flavors (multi-select)
kit.flavors.models.map(flavor => (
<FlavorButton
key={flavor.id}
flavor={flavor}
selected={kit.selections.flavors?.includes(flavor)}
onClick={() => kit.flavors.handlers.onSelected(flavor)}
/>
))
// Access flavor companions
kit.flavors.companions // FlavorCompanionModel[]
// Clear all flavor selections
kit.flavors.clear()Behavior: Flavors support multi-select. Calling onSelected toggles the flavor in the selection array and updates backend state.
Pour Integration (kit.pour)
CUI Kit integrates with useNozzlePour to provide complete pour functionality.
Pour Status
kit.pour.status.canPour // boolean - ready to pour
kit.pour.status.isPouring // boolean - currently pouring
kit.pour.status.currentState // PourState enum
kit.pour.status.pourProgress // 0-100 progress from futurePour Operations
// Execute pour (uses selected pourable from NozzleModel)
kit.pour.handlers() // spread onto button element
// Force cancel active pour
kit.pour.cancel()Pour Validation Hook
Add custom validation before pours execute:
kit.updateOnBeforePour(async (context) => {
// Check payment, user authentication, etc.
const isValid = await validatePayment();
if (!isValid) {
showPaymentError();
return false; // Reject pour
}
return true; // Allow pour
});Behavior: If validation returns false, pour is cancelled and /kos/dispense/{nozzlePath}/pre/pour/rejected/ event is published.
Pour Context
Add custom data to pour operations for analytics, payment tracking, or external integrations:
// Add context data
kit.pour.updatePourContext('paymentId', '12345');
kit.pour.updatePourContext('userId', 'user-abc');
// Remove context data
kit.pour.removeFromPourContext('paymentId');This context is passed through the entire pour lifecycle and available in validation callbacks.
Pour Usage Pattern
<button
{...kit.pour.handlers()}
disabled={!kit.pour.status.canPour}
>
{kit.pour.status.isPouring
? `Pouring ${kit.pour.status.pourProgress}%`
: 'Pour Beverage'
}
</button>Timer Management (kit.timers)
CUI Kit provides lifecycle management for ambient/attract mode timers.
Timer Registration
import { KosTimer } from '@kosdev-code/kos-ui-sdk';
// Create timer
const attractTimer = new KosTimer({
duration: 30000, // 30 seconds
onComplete: () => navigateToAttract()
});
// Register with CUI Kit
kit.timers.add(attractTimer);Timer Operations
// Pause all registered timers
kit.timers.pause()
// Restart all registered timers
kit.timers.restart()Use Case: Pause timers during pour operations, restart when pour completes. CUI Kit tracks timers but doesn’t automatically pause/restart – your application controls timing.
EventBus Integration
CUI Kit publishes events for all user interactions, enabling analytics, logging, and external system integration.
Using useDispenseLifecycle Hook (Recommended for React)
For React components, use the useDispenseLifecycle hook to listen to CUI Kit events:
import { useDispenseLifecycle } from '@kosdev-code/kos-dispense-sdk';
function AnalyticsTracker() {
useDispenseLifecycle({
onBrandSelected: (brand) => {
analytics.track('brand_selected', { brandId: brand.id });
},
onBeverageSelected: (beverage) => {
analytics.track('beverage_selected', { beverageId: beverage.id });
},
onFlavorSelected: (flavor) => {
analytics.track('flavor_selected', { flavorId: flavor.id });
},
onPour: () => {
analytics.track('pour_started');
},
onPourCancel: () => {
analytics.track('pour_cancelled');
},
onReturnToHome: () => {
analytics.track('returned_home');
}
});
return null; // Analytics component doesn't render UI
}Available Lifecycle Callbacks
The useDispenseLifecycle hook provides callbacks for all CUI Kit events:
interface UseDispenseLifecycleProps {
// Selection events
onBrandSelected?: (brand: AvailabilityModel) => void
onBrandCleared?: () => void
onBeverageSelected?: (beverage: AvailabilityModel) => void
onBeverageCleared?: () => void
onBeverageListUpdated?: (beverages: AvailabilityModel[]) => void
onFlavorSelected?: (flavor: AvailabilityModel) => void
onFlavorCleared?: () => void
// Pour events
onPour?: () => void
onPourCancel?: () => void
onBeforePourRejected?: () => void
// Navigation events
onReturnToHome?: () => void
onInit?: () => void
// Timer events
onAmbientTimer?: () => void
onAttractTimer?: () => void
onResumeAttractTimer?: () => void
onIdleTimeout?: () => void
onAreYouThereTimeout?: () => void
}Published Topics
CUI Kit publishes to EventBus topics using the pattern /kos/dispense/{nozzlePath}/[event]/:
// Selection events
/kos/dispense/{nozzlePath}/brand/selected/
/kos/dispense/{nozzlePath}/brand/cleared/
/kos/dispense/{nozzlePath}/beverage/selected/
/kos/dispense/{nozzlePath}/beverage/cleared/
/kos/dispense/{nozzlePath}/beverage/list/updated/
/kos/dispense/{nozzlePath}/flavor/selected/
/kos/dispense/{nozzlePath}/flavor/cleared/
// Pour events
/kos/dispense/{nozzlePath}/pour/
/kos/dispense/{nozzlePath}/pour/cancel/
/kos/dispense/{nozzlePath}/pre/pour/rejected/
// Navigation events
/kos/dispense/{nozzlePath}/home/
/kos/dispense/{nozzlePath}/init/
// Timer events
/kos/dispense/{nozzlePath}/ambient/start/
/kos/dispense/{nozzlePath}/attract/start/
/kos/dispense/{nozzlePath}/attract/resume/
/kos/dispense/{nozzlePath}/idle/timeout/
/kos/dispense/{nozzlePath}/are-you-there/timeout/Beverage List Updates
The onBeverageListUpdated callback fires whenever the visible beverage list changes, providing the updated array of beverages for UI animations, analytics, or external system synchronization.
import { useDispenseLifecycle } from '@kosdev-code/kos-dispense-sdk';
function BeverageAnalytics() {
useDispenseLifecycle({
onBeverageListUpdated: (beverages) => {
// Fires when beverage list changes due to filtering or brand selection
analytics.track('beverage_list_updated', {
count: beverages.length,
beverageIds: beverages.map(b => b.id)
});
}
});
return null;
}When this callback fires:
- After brand selection (beverages filtered by selected brand)
- When clearing brand selection (all beverages shown)
- Any time the visible beverage array changes
Common use cases:
1. Transition Animations
Trigger CSS transitions when the beverage list updates:
function BeverageGrid() {
const [isTransitioning, setIsTransitioning] = useState(false);
useDispenseLifecycle({
onBeverageListUpdated: () => {
setIsTransitioning(true);
setTimeout(() => setIsTransitioning(false), 300);
}
});
return (
<div className={`beverage-grid ${isTransitioning ? 'fade-transition' : ''}`}>
{/* Grid items */}
</div>
);
}2. Accessibility Announcements
Notify screen reader users when the list changes:
function AccessibleBeverageList() {
const [announcement, setAnnouncement] = useState('');
useDispenseLifecycle({
onBeverageListUpdated: (beverages) => {
const count = beverages.length;
setAnnouncement(`${count} beverage${count !== 1 ? 's' : ''} available`);
}
});
return (
<div>
<div aria-live="polite" aria-atomic="true" className="sr-only">
{announcement}
</div>
{/* Beverage list UI */}
</div>
);
}3. Auto-Selecting Default Beverage
A common pattern when brand filtering occurs is to automatically select a default beverage:
function BeverageSelectionScreen() {
const kit = useCuiKit({ nozzlePath });
useDispenseLifecycle({
onBeverageListUpdated: (beverages) => {
// Auto-select when brand filtering leaves one beverage
if (beverages.length === 1) {
kit.beverages.handlers.onSelected(beverages[0]);
}
// Or apply your application's default selection logic
else {
const defaultBev = beverages.find(b => b.data.isRecommended);
if (defaultBev) {
kit.beverages.handlers.onSelected(defaultBev);
}
}
}
});
return <BeverageGrid />;
}This pattern allows applications to implement custom default selection logic based on business requirements while keeping the policy in application code rather than the CUI Kit.
4. External Display Synchronization
Update secondary displays or external systems:
useDispenseLifecycle({
onBeverageListUpdated: (beverages) => {
// Sync with external LED display
externalDisplay.setBeverageCount(beverages.length);
// Update companion mobile app
mobileApp.notifyBeverageListChanged(beverages.map(b => ({
id: b.id,
name: b.data.name,
available: b.available
})));
}
});This callback provides fine-grained notification of list changes, complementing the selection-focused callbacks (onBeverageSelected, onBeverageCleared) with list-level updates.
Listening in Models (Alternative)
When working with models, use @kosTopicHandler decorators:
import { kosTopicHandler } from '@kosdev-code/kos-ui-sdk';
class AnalyticsModel {
@kosTopicHandler({ topic: '/kos/dispense/+/beverage/selected/' })
handleBeverageSelected(message) {
this.analytics.track('beverage_selected', message);
}
}Complete Example: Select and Pour UI
Here’s a complete select and pour interface using CUI Kit:
import {
DeviceAssemblyProvider,
NozzleProvider,
useCuiKit
} from '@kosdev-code/kos-dispense-sdk';
// Provider hierarchy setup
function App() {
return (
<DeviceAssemblyProvider>
<NozzleProvider nozzlePath="assembly:core:nozzle:nozzle">
<SelectAndPour />
</NozzleProvider>
</DeviceAssemblyProvider>
);
}
// Select and pour interface component
function SelectAndPour() {
const kit = useCuiKit<BrandData, BeverageData, FlavorData>({
initialGroup: 'brands'
});
// Render based on current navigation group
return (
<div>
<NavigationHeader
currentGroup={kit.nav.currentGroup}
onBack={kit.nav.back}
onHome={kit.nav.home}
/>
{kit.nav.currentGroup === 'brands' && (
<BrandGrid brands={kit.brands.models} onSelect={kit.brands.handlers.onSelected} />
)}
{kit.nav.currentGroup === 'beverages' && (
<BeverageGrid beverages={kit.beverages.models} onSelect={kit.beverages.handlers.onSelected} />
)}
{kit.nav.currentGroup === 'flavors' && (
<FlavorSelection
flavors={kit.flavors.models}
selected={kit.selections.flavors ?? []}
onToggle={kit.flavors.handlers.onSelected}
/>
)}
<SelectionSummary selections={kit.selections} />
<button
{...kit.pour.handlers()}
disabled={!kit.pour.status?.canPour}
>
{kit.pour.status?.isPouring
? `Pouring ${kit.pour.status.pourProgress}%`
: 'Pour Beverage'
}
</button>
</div>
);
}
// Display components
function BrandGrid({ brands, onSelect }) {
return (
<div className="brand-grid">
{brands.map(brand => (
<div key={brand.id} onClick={() => onSelect(brand)}>
<img src={brand.data.logoUrl} alt={brand.data.name} />
<span>{brand.data.name}</span>
</div>
))}
</div>
);
}
function BeverageGrid({ beverages, onSelect }) {
return (
<div className="beverage-grid">
{beverages.map(beverage => (
<div
key={beverage.id}
onClick={() => onSelect(beverage)}
className={beverage.available ? '' : 'unavailable'}
>
<span>{beverage.data.name}</span>
{!beverage.available && <span>Unavailable</span>}
</div>
))}
</div>
);
}
function FlavorSelection({ flavors, selected, onToggle }) {
return (
<div className="flavor-selection">
{flavors.map(flavor => (
<button
key={flavor.id}
onClick={() => onToggle(flavor)}
className={selected.includes(flavor) ? 'selected' : ''}
>
{flavor.data.name}
</button>
))}
</div>
);
}
function SelectionSummary({ selections }) {
return (
<div className="summary">
{selections.brands && <div>Brand: {selections.brands.data.name}</div>}
{selections.beverages && <div>Beverage: {selections.beverages.data.name}</div>}
{selections.flavors?.length > 0 && (
<div>Flavors: {selections.flavors.map(f => f.data.name).join(', ')}</div>
)}
</div>
);
}Advanced Pattern: Timer Integration
Integrate ambient mode timers with CUI Kit lifecycle:
function SelectAndPourWithTimers() {
const kit = useCuiKit({ initialGroup: 'brands' });
const navigate = useNavigate();
useEffect(() => {
// Create ambient timer
const ambientTimer = new KosTimer({
duration: 30000,
onComplete: () => {
kit.nav.home();
navigate('/attract');
}
});
// Register with CUI Kit
kit.timers.add(ambientTimer);
// Pause timer during pours
if (kit.pour.status?.isPouring) {
kit.timers.pause();
} else {
kit.timers.restart();
}
return () => {
ambientTimer.destroy();
};
}, [kit.pour.status?.isPouring]);
// Rest of component...
}Benefits Over Manual Implementation
Building selection workflows manually requires:
- State management – Tracking brands, beverages, flavors, and navigation state
- Backend synchronization – Calling
nozzle.setSelectedBrand()andnozzle.setSelectedPourable()at the right times - Navigation logic – Managing transitions between groups
- Event publishing – Publishing analytics events for tracking
- Pour integration – Connecting selection state to pour operations
- Timer coordination – Managing timer lifecycles across interactions
CUI Kit handles all of this automatically, letting you focus on UI rendering and user experience.
Next Steps
- Learn about Pouring Operations for advanced pour workflows
- Explore Trouble Integration for displaying availability issues
- Review Extension Points for customizing CUI Kit behavior
API Validation: All APIs verified against use-cui-kit.ts:1-475, use-brands.ts:1-67, use-beverages.ts:1-76, use-flavors.ts:1-79, use-nozzle-pour.ts:1-142, use-dispense-lifecycle.ts:1-148
