Guide · Panelith 0.1.0 · plain HTML edition; the interactive edition has search and live examples
Lists and the drawer
On this page 4 sections
A ListSource provides rows. The drawer on the right shows them with search, facets and a status mark; the command bar shows the same rows under the source's sigil; a ListPane renders them as a table in a pane. These docs' Contents is one: the drawer button in the top bar, the ? sigil and the search box all read it.
Declaring a list#
class LongWords(ListSource):
id = "words"
title = "Long words"
sigil = "/" # type / in the bar to search this list
columns = ["word", "length"] # strings, or ListColumn(key, label, width, grow, mono)
search = ["word"]
facets = ["length"]
row_action = "words.open" # a command id, run with {"row": row}
empty_text = "No word matches"
def rows(self, ctx):
return [{"id": w, "word": w, "length": len(w)} for w in LONG_WORDS]
class OpenWord(Command):
id = "words.open"
title = "Words: Open"
def run(self, ctx):
word = (ctx.args.get("row") or {}).get("word")
return a.open_pane("word", name=word.upper(), state={"word": word})
Rows are plain dicts with an id. search names the fields the search box matches; facets names fields that get a filter button each, with counts; status is a ListStatus(key, marks, tones) mapping a field's values to a glyph and a tone, which Math, You uses for solved and half-solved problems. rows(ctx) is called when the drawer opens and whenever a handler returns refresh_list("words"), so a list can reflect state that changes while the app runs. The reference lists every attribute.
The row action receives the row in ctx.args["row"]. It can open a pane, send an event to an existing one, or anything else a command can do.
TopBar(buttons=["list:contents", "commands", "hotkeys"]) # the list button
a.run_builtin("list:contents") # the same toggle, from a handler
Live lists: the server searches#
By default the shell fetches a list's rows once and searches them itself with a fuzzy, multi-term matcher: good for a few thousand rows. For a search the server must do (a full-text index, a database, an API), set live = True and override search_rows(ctx, query). Every keystroke in the bar or the drawer then fetches /api/lists/<id>?q=… (debounced, last answer wins) and the shell shows the rows in the order it got them, with no filtering of its own. snippet names a row key shown dim under the row, for an excerpt.
class Contents(ListSource):
id = "contents"
title = "Contents"
sigil = "?"
columns = [ListColumn(key="title", grow=True), ListColumn(key="where", width=170)]
facets = ["section"]
live = True
snippet = "snippet"
row_action = "docs.open"
def rows(self, ctx): # the table of contents, with no query
return [...]
def search_rows(self, ctx, query): # ranked hits, with a snippet each
return INDEX.search(query) if query.strip() else self.rows(ctx)
The default search_rows is a case-insensitive substring match over the search fields, so live = True alone already moves the work to the server. Facets still work on whatever rows come back.
A list in a pane#
ListPane is a Pane preset to the table widget: set source = "words" and its initial state is the list's columns and rows, so one definition serves the drawer and a pane. A table pane's row click is a row event the kind handles itself; the drawer's row click runs row_action.
class Planets(Pane):
kind = "planets"
label = "Planets"
glyph = "table"
family = "assessment"
widget = "table"
tip = "A sortable table; a row click is an event"
def initial_state(self, ctx):
return widgets.table(PLANETS, columns={"planet": "Planet", "moons": "Moons", "au": "Distance (AU)"})
@event("row")
def row(self, ctx, payload):
row = payload.get("row") or {}
return a.notify(f"{row.get('planet')}: {row.get('moons')} moons", "info")
The cycler#
A kind with cycler = "<list id>" gets ‹ › buttons in its header. Each click sends the pane a cycle event with {"delta": ±1, "list": id}; what that means (the next problem, the previous file) is the kind's decision. Open the header variants to see one:
class Problem(Pane):
cycler = "problems"
def on_event(self, ctx, name, payload):
if name == "cycle":
return self.step(ctx, payload["delta"])