UI reference

Search (MicroSearch)

Platform-level command palette for searching within apps. Each app declares its searchable types in the manifest; the platform provides the UI shell, keyboard shortcuts, and result rendering.

JS: public/js/microsearch.js CSS: public/js/microsearch.css Trigger: Ctrl+K / Cmd+K, or click the navbar search bar


How it works

  1. User presses Ctrl+K or clicks the search bar — the command palette opens
  2. User types a query — MicroSearch debounces (200ms) then hits the active app's search API
  3. Results render with colored dots matching the searchable type
  4. User navigates with arrow keys, presses Enter to open, or Esc to close

Manifest: declaring searchables

Every app declares what it can search in manifest.py:

# apps/tasks/manifest.py
manifest = {
    # ...
    "searchables": [
        {"type": "task",        "label": "Tasks",        "icon": "assignment",   "color": "#F39C12"},
        {"type": "board",       "label": "Boards",       "icon": "view_kanban",  "color": "#3B82F6"},
        {"type": "schedule",    "label": "Schedules",    "icon": "schedule",     "color": "#8B5CF6"},
        {"type": "deliverable", "label": "Deliverables", "icon": "task_alt",     "color": "#10B981"},
        {"type": "member",      "label": "Members",      "icon": "group",        "color": "#5D9288"},
        {"type": "topic",       "label": "Topics",       "icon": "label",        "color": "#E8A838"},
    ],
}

Searchable fields

Field Type Description
type str Machine key — used in API types filter and result payloads.
label str Human-readable label shown in the legend.
icon str Material Symbols icon name (for future use).
color str Hex color for the dot in legend and results.

Search API contract

Each app must expose a search endpoint:

GET /api/{app}/search?q={query}&types={comma-separated}

Request

Param Type Description
q str Search query (required).
types str Comma-separated type filter (optional). Empty = search all types.

Response

{
  "results": [
    {
      "type": "task",
      "id": "PL-1",
      "title": "Write launch blog post",
      "subtitle": "Product Launch · To Do",
      "url": "/board/board-1?task=PL-1"
    }
  ]
}

Result fields

Field Type Description
type str Must match a type from the manifest's searchables.
id str Short identifier shown as a badge before the title. Can be empty.
title str Primary text.
subtitle str Secondary text (context, status, parent).
url str Navigation URL when the result is selected.

Initializing MicroSearch

MicroSearch is loaded globally via base.html. Each app initializes it with its own config:

<script>
document.addEventListener('DOMContentLoaded', function() {
  MicroSearch.init({
    app: 'tasks',
    apiBase: 'http://api.local.localhost:8000',
    searchables: [
      { type: 'task', label: 'Tasks', icon: 'assignment', color: '#F39C12' },
      { type: 'board', label: 'Boards', icon: 'view_kanban', color: '#3B82F6' },
      // ...
    ],
  });
});
</script>

MicroSearch.init(opts)

Option Type Description
app str App slug — used to construct the API URL.
apiBase str Base URL of the API subdomain.
searchables array Same shape as the manifest's searchables. Defines the legend and color mapping.

Other methods

Method Description
MicroSearch.open() Programmatically open the palette.
MicroSearch.close() Programmatically close the palette.

UI anatomy

┌─────────────────────────────────────────────────┐
│ 🔍  Search Tasks...                        Esc  │  ← input row
├─────────────────────────────────────────────────┤
│ ● Tasks  ● Boards  ● Schedules  ● Members ...  │  ← legend (clickable filters)
├─────────────────────────────────────────────────┤
│ ● PL-1  Write launch blog post                  │  ← result (colored dot + id + title)
│         Product Launch · To Do                   │  ← subtitle
│ ● PL-5  API rate limiting                        │
│         Product Launch · In Progress             │
│ ◆ PL    Product Launch                           │  ← board result (different color)
│         9 tasks · 2 done                         │
├─────────────────────────────────────────────────┤
│ ↑↓ navigate    Enter open    Esc close          │  ← footer hints
└─────────────────────────────────────────────────┘

Legend filtering

Clicking a legend chip toggles that type on/off. When all chips are active (default), all types are searched. Deactivating a chip excludes that type from the API request via the types parameter.


CSS classes

Class Element Purpose
.ms-overlay div Fixed full-screen backdrop with blur. Click outside palette to close.
.ms-palette div The command palette container. Max-width 620px, centered.
.ms-input-row div Search input row with icon and Esc badge.
.ms-input input The search text input.
.ms-kbd kbd Keyboard shortcut badge.
.ms-legend div Row of filterable type chips.
.ms-legend-chip button Individual type filter. .active when included.
.ms-dot span 8px colored circle. Used in legend chips and results.
.ms-results div Scrollable results container (hidden scrollbar).
.ms-result a Single result row.
.ms-result-selected .ms-result Keyboard-selected result (highlighted).
.ms-result-id span ID badge before the title.
.ms-result-subtitle div Secondary line of text.
.ms-empty div Empty/loading state message.
.ms-footer div Keyboard hint bar at the bottom.
html.ms-open html Added when palette is open. Prevents body scroll.

Implementing search for a new app

1. Add searchables to your manifest

# apps/papers/manifest.py
"searchables": [
    {"type": "document", "label": "Documents", "icon": "description", "color": "#3B82F6"},
    {"type": "folder",   "label": "Folders",   "icon": "folder",      "color": "#F59E0B"},
    {"type": "template", "label": "Templates", "icon": "draft",       "color": "#8B5CF6"},
]

2. Add a search API route

# apps/papers/plugin.py
api.GET("/papers/search", "apps.papers.apis.api_search")

3. Implement the handler

async def api_search(req: Request, res: Response, ctx: Context):
    q = (req.queries.get("q") or "").strip().lower()
    types = [t for t in (req.queries.get("types") or "").split(",") if t]

    results = []

    if not types or "document" in types:
        # search documents...
        pass

    if not types or "folder" in types:
        # search folders...
        pass

    return res.out(HTTPStatus.OK, {"results": results[:50]})

4. Include the search init partial

{# In your app's templates #}
<script>
document.addEventListener('DOMContentLoaded', function() {
  MicroSearch.init({
    app: 'papers',
    apiBase: 'http://api.local.localhost:8000',
    searchables: [
      { type: 'document', label: 'Documents', icon: 'description', color: '#3B82F6' },
      { type: 'folder', label: 'Folders', icon: 'folder', color: '#F59E0B' },
      { type: 'template', label: 'Templates', icon: 'draft', color: '#8B5CF6' },
    ],
  });
});
</script>

Keyboard shortcuts

Shortcut Action
Ctrl+K / Cmd+K Open/close the search palette
Esc Close the palette
/ Navigate results
Enter Open the selected result