Skip to main content
The ui/src/ tree is organised into three layers — app/, features/, and shared/ — each with a package-local import alias defined in ui/package.json. Components follow the pattern of separating stateful page components from presentational layout components, and data-fetching hooks from UI state.

Feature structure

ui/src/
├── app/                      # Root wiring
│   ├── App.tsx               # Pathname-based route switch
│   ├── AppProviders.tsx      # QueryClientProvider + Toaster
│   ├── NotFoundPage.tsx
│   └── routes.ts             # appRoutes constants + isControlRoomPath()
├── features/
│   ├── coffee-shop/          # Order management feature
│   │   ├── api/coffee.ts     # Fetch helpers (fetchMenu, fetchOrders, createOrder, updateOrderStatus)
│   │   ├── components/       # Page, layout, customer panel, barista panel
│   │   ├── hooks/            # TanStack Query hooks + composed shop state
│   │   └── lib/              # Domain types and pure helpers
│   └── assistant/            # Browser AI assistant feature
│       ├── components/       # Landing page, transcript, composer, tool-activity drawer
│       ├── hooks/            # useLfmCoffeeAssistant + reducer support
│       └── lib/              # Model loading, generation, MCP client, tool parsing
└── shared/
    ├── hooks/                # useThemePreference
    ├── lib/
    └── ui/
        ├── retroui/          # shadcn/ui-style primitives
        ├── SelectField.tsx
        ├── StatusBadge.tsx
        ├── TextAreaField.tsx
        ├── TextField.tsx
        └── ThemeToggle.tsx

coffee-shop feature

Page and layout components

CoffeeShopPage (features/coffee-shop/components/CoffeeShopPage.tsx) is the stateful root. It calls useCoffeeShopState() to collect all derived data and mutations, then passes them as props into the purely presentational CoffeeShopLayout:
export function CoffeeShopPage() {
  const state = useCoffeeShopState();
  const { actions, createOrderMutation, draftState, errorMessage,
          menu, menuCount, orders, ordersQuery, queue, theme,
          toggleTheme, workspace } = state;

  return (
    <CoffeeShopLayout
      activeOrders={queue.activeOrders.length}
      baristaPanel={<BaristaPanel ... />}
      customerPanel={<CustomerPanel ... />}
      errorMessage={errorMessage}
      isRefreshing={ordersQuery.isFetching}
      ...
    />
  );
}
CoffeeShopLayout renders the full page grid: PageHeader, HeroBanner, an optional Alert for backend errors, ViewModeTabs (switching between customer and barista views), and ReceiptDialog.

Customer panel

CustomerPanel (features/coffee-shop/components/customer/CustomerPanel.tsx) contains two cards:
  • OrderComposerCard — drink selector, order fields (customer name, size, extras), price preview, and submit button.
  • MenuCatalogCard — grid of MenuItemCard tiles that highlight the currently selected drink. Clicking a tile calls onSelectDrink.
The panel shows an Alert with status "info" while the menu query is still loading.

Barista panel

BaristaPanel (features/coffee-shop/components/barista/BaristaPanel.tsx) contains four components:
ComponentPurpose
QueueSummaryMetric strip: active count, ready count, queue load, history count
QueueBoardCardTable of active orders with inline QueueRowActions for lifecycle transitions
RecentActivityCardUp to 4 completed or cancelled orders
OrderDetailsDrawerRadix UI drawer that opens when a barista selects a ticket; shows full order details and action buttons
Order lifecycle actions are start-brewing, mark-ready, pick-up, and cancel, defined in features/coffee-shop/api/coffee.ts.

TanStack Query data layer

All server state lives in features/coffee-shop/hooks/useCoffeeQueries.ts. The hooks use stable query keys so mutations can invalidate exactly the right cache entries:
const menuKey  = ["menu"]   as const;
const ordersKey = ["orders"] as const;

export function useMenuQuery() {
  return useQuery({
    queryKey: menuKey,
    queryFn: fetchMenu,
    staleTime: 60_000,       // menu changes rarely
  });
}

export function useOrdersQuery() {
  return useQuery({
    queryKey: ordersKey,
    queryFn: fetchOrders,
    refetchInterval: 4_000,  // poll every 4 s for live queue updates
  });
}

export function useCreateOrderMutation() {
  const queryClient = useQueryClient();
  return useMutation({
    mutationFn: async (payload: PlaceOrderRequest) => createOrder(payload),
    onSuccess: async () => queryClient.invalidateQueries({ queryKey: ordersKey }),
  });
}
useOrderActionMutation follows the same pattern for the start-brewing, mark-ready, pick-up, and cancel actions. useCoffeeShopState (features/coffee-shop/hooks/useCoffeeShopState.ts) composes all four hooks with local state for the selected order, receipt overlay, view mode, and order draft to produce a single state object consumed by CoffeeShopPage. AppProviders (app/AppProviders.tsx) wraps the tree in QueryClientProvider and configures the global defaults:
const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      retry: 1,
      refetchOnWindowFocus: true,
    },
  },
});

Shared retroui primitives

The shared/ui/retroui/ directory contains the project’s shadcn/ui-derived component library. All components are authored directly in the repo rather than auto-generated:
ComponentRadix UI dependency
Alertnone (custom)
Badgenone (custom)
Button@radix-ui/react-slot
Cardnone (custom)
Dialog@radix-ui/react-dialog
Drawervaul
Inputnone (custom)
Label@radix-ui/react-label
Progress@radix-ui/react-progress
Select@radix-ui/react-select
Sonnersonner
Spinnernone (custom)
Tabnone (custom)
Tablenone (custom)
Textnone (custom)
Textareanone (custom)
The shared layer also exports four field components with Storybook stories: SelectField, StatusBadge, TextAreaField, and TextField.

Storybook and component testing

Stories are co-located alongside components and follow the *.stories.{ts,tsx} naming convention. The Storybook config (ui/.storybook/main.ts) uses @storybook/react-vite and shares the same Vite + Tailwind config as the app build. Components with stories in the coffee-shop feature include:
  • CoffeeShopLayout.stories.tsx
  • PageHeader.stories.tsx
  • OrderComposerCard.stories.tsx
Components with stories in the assistant feature include:
  • BrowserMcpLandingView.stories.tsx
  • DemoComposer.stories.tsx
  • DemoStatusPanel.stories.tsx
Shared primitives with stories include Button.stories.tsx, Input.stories.tsx, Select.stories.tsx, and several field components.

Running tests

Vitest browser mode powers story-level interaction tests via @storybook/addon-vitest:
bun run --cwd ui test
# equivalent to: vitest run --project=storybook
Tests run in the browser context via @vitest/browser with Playwright as the runner, so component behaviour is exercised in a real DOM rather than jsdom.

Build docs developers (and LLMs) love