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 → ScreenContractThis 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"
}MenuSpec properties
| Property | Type | Default | Purpose |
|---|---|---|---|
panel_title | string | required | Menu panel title |
tabs | array | required | Tab groups |
top_left | string | null | Text in top-left corner (application name) |
top_right | string | null | Text 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"
}
}
]
}Menu actions
Each item has an action that determines what happens when the user presses Enter:
| Type | JSON | Behavior |
|---|---|---|
| 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 |
Sub-menus
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)External links
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.
Popup menus
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" }
}
]
}
}Popup item actions
Each popup item supports the same action types as regular menu items, except separator and popup (no nesting):
| Type | Behavior |
|---|---|
open_screen | Fetches the URL and pushes it as a new screen |
open_menu | Fetches the URL and pushes it as a sub-menu |
open_url | Opens the URL in the system default browser |
message | Shows the text in the status bar |
When to use popup vs sub-menu
| Popup | Sub-menu (open_menu) | |
|---|---|---|
| Items | 2–6 tightly related destinations | Large or structured navigation |
| Navigation | Overlaid inline, Escape returns instantly | Pushes a new screen, Escape pops back |
| Server route needed | No | Yes — 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.