Skip to content

Menus

Summary

A Menu is the navigation entry point. It has horizontal tabs with vertical item lists. The main menu is the first screen the user sees after login.

What the client does with a Menu

The client renders tabs across the top and items in a vertical list. Left/Right switches tabs, Up/Down moves between items, Enter opens the selected item.

┌──────────────────────────────────────────────┐
│ 2Wee Demo Server              admin  10-03-26│
├──────────────────────────────────────────────┤
│ [Sales]  Purchasing  Warehouse  Financial    │
├──────────────────────────────────────────────┤
│  > Customers                                 │
│    Sales Orders                              │
│    Posted Invoices                           │
│    Customer Ledger Entries                   │
└──────────────────────────────────────────────┘

Building a Menu

Route

GET /menu/main → ScreenContract

This is one of the two hardcoded entry points the client knows about. The client fetches /menu/main on startup.

Response structure

json
{
  "layout": "Menu",
  "title": "Main Menu",
  "menu": {
    "panel_title": "Main Menu",
    "top_left": "My Application",
    "top_right": "admin  10-03-26",
    "tabs": [
      {
        "label": "Sales",
        "items": [
          {
            "label": "Customers",
            "action": {
              "type": "open_screen",
              "url": "/screen/customer_list"
            }
          },
          {
            "label": "Sales Orders",
            "action": {
              "type": "open_screen",
              "url": "/screen/sales_order_list"
            }
          },
          {
            "label": "Posted Invoices",
            "action": {
              "type": "open_screen",
              "url": "/screen/posted_invoice_list"
            }
          }
        ]
      },
      {
        "label": "Purchasing",
        "items": [
          {
            "label": "Vendors",
            "action": {
              "type": "open_screen",
              "url": "/screen/vendor_list"
            }
          },
          {
            "label": "Purchase Orders",
            "action": {
              "type": "open_screen",
              "url": "/screen/purchase_order_list"
            }
          }
        ]
      },
      {
        "label": "Financial",
        "items": [
          {
            "label": "Chart of Accounts",
            "action": {
              "type": "open_screen",
              "url": "/screen/gl_account_list"
            }
          },
          {
            "label": "General Journal",
            "action": {
              "type": "open_screen",
              "url": "/screen/general_journal"
            }
          }
        ]
      }
    ]
  },
  "user_display_name": "Administrator"
}
PropertyTypeDefaultPurpose
panel_titlestringrequiredMenu panel title
tabsarrayrequiredTab groups
top_leftstringnullText in top-left corner (application name)
top_rightstringnullText in top-right corner (user, date)

Tabs and items

Each tab has a label and a list of items:

json
{
  "label": "Sales",
  "items": [
    {
      "label": "Customers",
      "action": {
        "type": "open_screen",
        "url": "/screen/customer_list"
      }
    }
  ]
}

Each item has an action that determines what happens when the user presses Enter:

TypeJSONBehavior
Open Screen{ "type": "open_screen", "url": "/screen/customer_list" }Client fetches the URL and pushes the result as a new screen
Open Menu{ "type": "open_menu", "url": "/menu/reports" }Client fetches the URL and pushes it as a sub-menu
Open URL{ "type": "open_url", "url": "https://portal.example.com" }Client opens the URL in the system default browser
Message{ "type": "message", "text": "Not implemented yet." }Client shows the text in the status bar
Separator{ "type": "separator" }Renders a blank line — non-selectable visual divider
Popup{ "type": "popup", "items": [...] }Opens an inline popup menu overlaid on the column

Use open_menu to create nested menu hierarchies. Each sub-menu is a separate ScreenContract served from its own URL. This lets you organize a large application into multiple levels of navigation without putting everything in one menu.

json
{
  "label": "Reports",
  "action": {
    "type": "open_menu",
    "url": "/menu/reports"
  }
}

The client pushes the sub-menu onto the screen stack. Esc pops back to the parent menu. Sub-menus can nest to any depth — each level is just another Menu ScreenContract.

Your server serves each sub-menu independently. For example:

GET /menu/main      → Main menu (Sales, Purchasing, Financial tabs)
GET /menu/reports   → Reports sub-menu (Sales Reports, Purchase Reports tabs)
GET /menu/setup     → Setup sub-menu (General, Users, Integrations tabs)

Use open_url for links that open in the system browser — portals, documentation, external tools:

json
{
  "label": "Customer Portal",
  "action": {
    "type": "open_url",
    "url": "https://portal.example.com"
  }
}

No screen navigation happens. The client stays on the menu and shows a confirmation in the status bar.

Placeholder items

Use message for items that are not yet implemented:

json
{
  "label": "Coming Soon",
  "action": {
    "type": "message",
    "text": "This feature is coming soon."
  }
}

Separators

Use separator to add a blank line between groups of items in the same tab. The client skips separators during Up/Down navigation — they are never selectable.

json
{
  "label": "",
  "action": { "type": "separator" }
}

The label is ignored and can be left as an empty string.

Use popup when a menu item opens a small inline list of choices rather than navigating away. The client renders a floating box overlaid on the column — wider than the column width so it clearly stands out. The user navigates with Up/Down, selects with Enter, and dismisses with Escape without leaving the menu.

json
{
  "label": "Posted",
  "action": {
    "type": "popup",
    "items": [
      {
        "label": "Posted Invoices",
        "action": { "type": "open_screen", "url": "/screen/posted_invoices" }
      },
      {
        "label": "Posted Credit Memos",
        "action": { "type": "open_screen", "url": "/screen/posted_credits" }
      },
      {
        "label": "Posted Shipments",
        "action": { "type": "open_screen", "url": "/screen/posted_shipments" }
      }
    ]
  }
}

Each popup item supports the same action types as regular menu items, except separator and popup (no nesting):

TypeBehavior
open_screenFetches the URL and pushes it as a new screen
open_menuFetches the URL and pushes it as a sub-menu
open_urlOpens the URL in the system default browser
messageShows the text in the status bar

When to use popup vs sub-menu

PopupSub-menu (open_menu)
Items2–6 tightly related destinationsLarge or structured navigation
NavigationOverlaid inline, Escape returns instantlyPushes a new screen, Escape pops back
Server route neededNoYes — a separate /menu/... endpoint

Use popup for small groups like "Posted Documents" where the items are closely related and you want the user to stay oriented in the parent menu. Use open_menu when the sub-section is large enough to deserve its own screen.