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.
How it works#
add_workspace(key, *, text, icon) adds a rail icon and returns a workspace that
exposes the same content API the shell has — add_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.