Workspaces (rail)#

Several distinct areas behind a VS Code-style icon rail — a mail + calendar + contacts suite, an IDE, a creative tool. Each area is a workspace with its own sidebar, and each can use a different navigation pattern.

Workspace rail (mail suite) — light theme Workspace rail (mail suite) — dark theme

How it works#

add_workspace(key, *, text, icon) adds a rail icon and returns a workspace that exposes the same content API the shell hasadd_page, list_nav, tree_nav, @detail, headers, and panel. So a single-tier app and a workspace are authored identically; the rail appears automatically once there is more than one workspace. add_footer_workspace() pins one (Settings, Account) to the rail bottom.

with shell.add_workspace("mail", text="Mail", icon="envelope") as ws:
    ws.list_nav(inbox)
    @ws.detail
    def read(message): ...

with shell.add_workspace("calendar", text="Calendar", icon="calendar3") as ws:
    with ws.add_page("today", text="Today", icon="calendar-day"):
        ...

Clicking the active rail icon hides the sidebar; clicking a different one switches workspace and shows it (the VS Code gesture). navigate(workspace, page) jumps to a page in a specific workspace. Each workspace remembers its own active page, so switching back and forth is lossless.

Example#

 1"""Workspaces — multiple sections behind an icon rail (a mail + calendar suite).
 2
 3Each ``add_workspace`` adds a rail icon and its own sidebar, authored with the
 4same page API — and each can use a *different* provider. Here Mail is a
 5master–detail list, Calendar is static pages, and Contacts is another list: the
 6rail appears automatically with more than one workspace. ``add_footer_workspace``
 7pins Settings to the rail bottom; ``navigate(workspace, page)`` jumps to a page.
 8"""
 9import bootstack as bs
10from bootstack.data import MemoryDataSource
11
12inbox = MemoryDataSource().load([
13    {"id": 1, "title": "Dana Reyes", "text": "Q3 roadmap review", "icon": "envelope", "body": "Can we move it to Thursday?"},
14    {"id": 2, "title": "Billing", "text": "Your receipt for June", "icon": "envelope-open", "body": "Thanks for your payment."},
15])
16contacts = MemoryDataSource().load([
17    {"id": 1, "title": "Dana Reyes", "text": "dana@acme.io", "icon": "person-circle", "phone": "555-0142"},
18    {"id": 2, "title": "Sam Okonkwo", "text": "sam@acme.io", "icon": "person-circle", "phone": "555-0177"},
19])
20
21with bs.AppShell(title="Workspace", size=(980, 620)) as shell:
22    shell.commandbar.add_spacer()
23    shell.commandbar.add_button(icon="circle-half", on_click=bs.toggle_theme)
24
25    # Mail — a master–detail list.
26    with shell.add_workspace("mail", text="Mail", icon="envelope") as ws:
27        ws.list_nav(inbox, chevron=True)
28
29        @ws.detail
30        def read(message):
31            with bs.VStack(fill="both", expand=True, anchor_items="w", gap=12, padding=20):
32                bs.Label(message["text"], font="heading-lg")
33                bs.Label(f"From {message['title']}", font="caption")
34                bs.Separator()
35                bs.Label(message["body"])
36
37    # Calendar — static pages.
38    with shell.add_workspace("calendar", text="Calendar", icon="calendar3") as ws:
39        with ws.add_page("today", text="Today", icon="calendar-day"):
40            with bs.VStack(fill="both", expand=True, anchor_items="w", gap=8, padding=20):
41                bs.Label("Today", font="heading-lg")
42                bs.Label("No events.")
43        with ws.add_page("week", text="Week", icon="calendar-week"):
44            with bs.VStack(fill="both", expand=True, anchor_items="w", gap=8, padding=20):
45                bs.Label("This week", font="heading-lg")
46
47    # Contacts — another list.
48    with shell.add_workspace("contacts", text="Contacts", icon="people") as ws:
49        ws.list_nav(contacts, chevron=True)
50
51        @ws.detail
52        def card(person):
53            with bs.VStack(fill="both", expand=True, anchor_items="w", gap=12, padding=20):
54                bs.Label(person["title"], font="heading-lg")
55                bs.Label(person["text"], font="caption")
56                bs.Label(f"Phone: {person['phone']}")
57
58    with shell.add_footer_workspace("settings", text="Settings", icon="gear") as ws:
59        with ws.add_page("general", text="General", icon="sliders"):
60            with bs.VStack(fill="both", expand=True, anchor_items="w", gap=8, padding=20):
61                bs.Label("Settings", font="heading-lg")
62
63    # Mail is the first workspace, so it opens active with its first message
64    # selected — no explicit navigate needed.
65
66shell.run()

When to use#

Use workspaces when the app has several distinct areas, each warranting its own sidebar — especially when those areas want different navigation shapes (a list here, authored pages there). With a single area, skip the rail and author pages at the shell level (a single-tier app); the two styles are mutually exclusive.