mkui 0.2.4__tar.gz → 0.2.6__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. {mkui-0.2.4 → mkui-0.2.6}/PKG-INFO +6 -4
  2. {mkui-0.2.4 → mkui-0.2.6}/README.md +5 -3
  3. {mkui-0.2.4 → mkui-0.2.6}/mkui/__init__.py +1 -1
  4. {mkui-0.2.4 → mkui-0.2.6}/mkui/static/examples/mkio-table/config/client.toml +25 -0
  5. {mkui-0.2.4 → mkui-0.2.6}/mkui/static/src/components/app.js +2 -0
  6. {mkui-0.2.4 → mkui-0.2.6}/mkui/static/src/components/workspace.js +17 -1
  7. {mkui-0.2.4 → mkui-0.2.6}/mkui/static/src/lib/icons.js +2 -0
  8. {mkui-0.2.4 → mkui-0.2.6}/mkui/static/src/widgets/mkio-table.js +597 -22
  9. {mkui-0.2.4 → mkui-0.2.6}/mkui/static/styles/mkui.css +174 -4
  10. {mkui-0.2.4 → mkui-0.2.6}/mkui.egg-info/PKG-INFO +6 -4
  11. {mkui-0.2.4 → mkui-0.2.6}/pyproject.toml +1 -1
  12. {mkui-0.2.4 → mkui-0.2.6}/LICENSE +0 -0
  13. {mkui-0.2.4 → mkui-0.2.6}/mkui/__main__.py +0 -0
  14. {mkui-0.2.4 → mkui-0.2.6}/mkui/static/examples/library-js/index.html +0 -0
  15. {mkui-0.2.4 → mkui-0.2.6}/mkui/static/examples/mkio-table/seed.csv +0 -0
  16. {mkui-0.2.4 → mkui-0.2.6}/mkui/static/examples/mkio-table/seed.py +0 -0
  17. {mkui-0.2.4 → mkui-0.2.6}/mkui/static/examples/mkio-table/server.toml +0 -0
  18. {mkui-0.2.4 → mkui-0.2.6}/mkui/static/examples/mkio-table/static/app.js +0 -0
  19. {mkui-0.2.4 → mkui-0.2.6}/mkui/static/examples/mkio-table/static/index.html +0 -0
  20. {mkui-0.2.4 → mkui-0.2.6}/mkui/static/examples/standalone-json/client.json +0 -0
  21. {mkui-0.2.4 → mkui-0.2.6}/mkui/static/examples/standalone-json/index.html +0 -0
  22. {mkui-0.2.4 → mkui-0.2.6}/mkui/static/src/auth.js +0 -0
  23. {mkui-0.2.4 → mkui-0.2.6}/mkui/static/src/components/frame.js +0 -0
  24. {mkui-0.2.4 → mkui-0.2.6}/mkui/static/src/components/menubar.js +0 -0
  25. {mkui-0.2.4 → mkui-0.2.6}/mkui/static/src/components/statusbar.js +0 -0
  26. {mkui-0.2.4 → mkui-0.2.6}/mkui/static/src/core.js +0 -0
  27. {mkui-0.2.4 → mkui-0.2.6}/mkui/static/src/index.js +0 -0
  28. {mkui-0.2.4 → mkui-0.2.6}/mkui/static/src/layout/drag.js +0 -0
  29. {mkui-0.2.4 → mkui-0.2.6}/mkui/static/src/layout/tree.js +0 -0
  30. {mkui-0.2.4 → mkui-0.2.6}/mkui/static/src/lib/copy.js +0 -0
  31. {mkui-0.2.4 → mkui-0.2.6}/mkui/static/src/lib/expr.js +0 -0
  32. {mkui-0.2.4 → mkui-0.2.6}/mkui/static/src/lib/expressions.js +0 -0
  33. {mkui-0.2.4 → mkui-0.2.6}/mkui/static/src/lib/rich.js +0 -0
  34. {mkui-0.2.4 → mkui-0.2.6}/mkui/static/src/lib/timeparse.js +0 -0
  35. {mkui-0.2.4 → mkui-0.2.6}/mkui/static/src/mkio-bridge.js +0 -0
  36. {mkui-0.2.4 → mkui-0.2.6}/mkui/static/src/widgets/button.js +0 -0
  37. {mkui-0.2.4 → mkui-0.2.6}/mkui/static/src/widgets/mkui-dialog.js +0 -0
  38. {mkui-0.2.4 → mkui-0.2.6}/mkui/static/src/widgets/text.js +0 -0
  39. {mkui-0.2.4 → mkui-0.2.6}/mkui.egg-info/SOURCES.txt +0 -0
  40. {mkui-0.2.4 → mkui-0.2.6}/mkui.egg-info/dependency_links.txt +0 -0
  41. {mkui-0.2.4 → mkui-0.2.6}/mkui.egg-info/entry_points.txt +0 -0
  42. {mkui-0.2.4 → mkui-0.2.6}/mkui.egg-info/top_level.txt +0 -0
  43. {mkui-0.2.4 → mkui-0.2.6}/setup.cfg +0 -0
  44. {mkui-0.2.4 → mkui-0.2.6}/tests/test_cli.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mkui
3
- Version: 0.2.4
3
+ Version: 0.2.6
4
4
  Summary: Config-driven, zero-dependency web GUI framework with dockable panes
5
5
  Author: Mark Kim
6
6
  License-Expression: Apache-2.0
@@ -314,8 +314,10 @@ Ctrl/Cmd+C and Ctrl/Cmd+A shortcuts take), `table.filter` (`args =
314
314
  table's column filters from a menu — same shapes as the pane's `filters`
315
315
  key; omit `pane` for the focused pane), and `table.sort` (`args = { pane
316
316
  = "<id>", sort = <spec> }` sets its sort order — same shape as the pane's
317
- `sort` key; no `sort` clears it). Register custom actions with
318
- `app.registerAction(name, fn)`.
317
+ `sort` key; no `sort` clears it), and `table.columns` (`args = { pane =
318
+ "<id>", visible = [...] }` sets which columns show — same shape as the
319
+ pane's `visible` key; no `visible` shows them all). Register custom
320
+ actions with `app.registerAction(name, fn)`.
319
321
 
320
322
  A typical Edit menu:
321
323
 
@@ -422,7 +424,7 @@ root.workspace.addFrame({ x: 0.5, y: 0.1, w: 0.4, h: 0.4,
422
424
  - `text` — static or `bind`-ed to a state path
423
425
  - `button` — fires an action by name
424
426
  - Pane types (whole-pane custom rendering):
425
- - `mkio-table` — subscribes to an mkio service (query, subpub, or stream) and renders a live-updating table with flash animations for inserts, deletes, and field changes. Row identity is per-protocol: query uses `_mkio_row`, stream uses `_mkio_ref`, subpub uses `_mkio_topic`. Column headers render immediately when `columns` is configured; optional `labels` maps column keys to display text (e.g. `{ "ts": "Timestamp" }`). Rows are **virtualized** — only the slice overlapping the viewport exists in the DOM, so scrolling, window moves, and window resizes stay fast into the millions of rows. Columns are fully interactive: click a header to sort (shift+click adds secondary sort keys), the hamburger button opens a per-column filter dropdown with search and value checkboxes — the filter remembers what you meant rather than what was listed: untick values (or Select all, then untick) to hide just those and let everything else through, including values that arrive later via live inserts, updates, or the next stream page; Clear, then tick values to show only those, with anything new staying hidden — numeric columns and columns of timestamps (mkio refs, ISO-8601 dates/date-times, or `HH:MM[:SS]` clock times) add a **Range** mode with From/To bounds (native date/time pickers for time columns, plus Today / Last hour / Last 15 min presets that keep moving with the clock) and an *include empty* toggle; other date formats aren't guessed — declare them with `types = { col = { type = "time", parse = "%d/%m/%Y %H:%M" } }` (or `unit = "ms"` for epoch numbers, `tz = "local"` for naive local timestamps). **Default filters** come from config: `filters = { status = { exclude = ["cancelled"] }, qty = { from = 100, to = 500 }, ts = { preset = "1h" } }` seeds the same filters before any data arrives (a bare list `["open", "new"]` means *only those*; `from`/`to` bounds make a number range when they are numbers and a time range when they are strings such as `"2026-03-01"` or `"2026-03-01 09:30"`, a date covering its whole day; `empty = true` lets blank values through; `preset` is `today`, `1h`, or `15m`), and reopening the pane restores them. The same shape drives the programmatic API — `workspace.setPaneFilters(id, filters, { merge })` / `getPaneFilters(id)` — and the `table.filter` menu action. **Sort** likewise comes from config — `sort = "-ts"` or `sort = [{ col = "status" }, "-name"]` (a column name, `-name` for descending, `{ col, dir }`, or an array in priority order), restored on pane reopen and driven programmatically by `workspace.setPaneSort(id, sort)` / `getPaneSort(id)` and the `table.sort` action. Whatever is active shows as **chips** on the table toolbar, so the state is visible and undoable without scrolling the header into view: the sort keys in priority order, then the filtered columns with the same summary the header tooltip gives. Click a sort chip to flip its direction or a filter chip to open that column's dropdown, × removes one, and the group's icon clears the whole group. The chips share the toolbar row with the selection buttons — buttons pinned left, chips flowing from the right, the cluster dropping to its own line when a narrow pane can't fit both — and the toolbar disappears when it has nothing to show — when a column is sorted, that header button's icon becomes the sort caret (still opening the filter dropdown, and showing the sort priority as a digit knocked out of the caret under multi-sort) — dragging a header reorders columns, and dragging the grip centered on a column divider resizes the column to its left — double-click the grip to auto-size the column to fit its content (capped at 80% of the viewport; with a selection active, all selected columns fit at once, so select-all + double-click fits the whole table). Column widths start at the header row's width and grow to fit records as they arrive (capped at 50% of the pane width, never shrinking, and never overriding a manual resize) — paging to another page never resizes them, and they don't change when the window resizes — extra pane width flows into a filler column that extends the header row to the pane's right edge, and subtle dividers separate the columns. Columns whose values are all numeric right-align with each value padded so decimal points line up down the column, in the cells and in the filter dropdown's value list alike. Subscriptions are deferred until the pane is first visible, unsubscribed immediately when the frame is closed, and dropped after 5 minutes of being hidden (e.g. inactive tab) — brief tab switches preserve the live connection. Large query snapshots render progressively in chunked batches to avoid freezing the UI. Stream tables support time-anchored paged navigation with a toolbar showing `◀ Earlier | time range | Later ▶ | ● Live | ⟳`. By default, the initial fetch starts from local midnight (`start: "today"`); `start: ""` starts from the beginning of the buffer. The toolbar displays the time range of visible rows in the browser's local timezone with adaptive precision — `HH:MM` down to nanoseconds in 3-digit increments depending on how close the boundary timestamps are; cross-day ranges include the date. Boundary indicators (`(start)`, `(end)`, `(all)`) show when you've reached the edges of the dataset. The `⟳` button re-fetches the current page. Navigation is ref-based (each page fetches relative to the first or last row's `_mkio_ref`), with `before: true` for backward fetches, so pages stay correct even when records are added or deleted mid-session. When navigating backward yields no data, the previous page is automatically restored and the Earlier button is disabled. A toggleable "Live" button switches to real-time streaming from the current page's last ref; in live mode, clicking Earlier uses a separate subscription to fetch and prepend historical pages without interrupting the live stream (toolbar shows `HH:mm – Live`). Live streams follow the tail like a terminal: entering live jumps to the newest row, and while the viewport is parked at the bottom, arriving rows keep scrolling into view — scroll up to inspect history and the viewport stays put. Set `live: true` to start in live mode: the start page loads first and then hands off to the live stream, so `start` is still honored rather than the whole buffer being replayed; exiting live returns to that start page, and reopening the pane re-arms it. Exiting live mode re-fetches the saved page from the server so that rows inserted or deleted during live mode are reflected. When the WebSocket disconnects while live mode is active, the toolbar shows "Disconnected" (or `HH:mm – Disconnected` with earlier pages) in muted text instead of the green blinking "Live" indicator — live mode stays active for seamless reconnect. Stream tables track the last received ref and use it on re-subscribe to avoid duplicate data transfer — existing rows are preserved and only new records are fetched. Query and subpub snapshots fully replace the table on each arrival, so records deleted on the server between disconnect and reconnect are properly removed. Tables are fully selectable, Excel-style: a sticky **row-number column** (disable with `rowColumn: false`) selects rows — click, ctrl/cmd-click to toggle, shift-click for ranges, drag for a range, or click the header corner to select all — while clicking a cell places the **focused cell** (accent outline; its whole row gets a subtle highlight so the record stays readable, distinct from the stronger row-selection tint). Drag across cells for a rectangle, ctrl/cmd-click to add or remove individual cells, shift-click or shift+arrows to extend; arrows/Home/End/PageUp/PageDown move the cursor, Space (or Shift+Space) selects the focused row, ctrl/cmd+Space toggles it, Ctrl/Cmd+A selects all visible rows, and Escape clears the selection. **Ctrl/Cmd+C copies** the selection to the OS clipboard in both TSV and HTML flavors, so pasting into Excel/Sheets preserves cell and row structure — row selections include a header row of column labels; cell selections copy values only, with blanks outside the selected rectangles; the copied cells pulse and the statusbar briefly shows "Copied N rows/cells" (via the `status.message` state path). Selection follows the filtered view (hidden rows are never selected or copied) and tracks records rather than positions: re-sorting moves the selection with its rows (rows that sort into the middle of a selected block don't join it), rows inserted live inside a selected range stay unselected, and members hidden by a filter rejoin the selection when the filter is relaxed. Table toolbar buttons can declare a selection `unit` (`"rows"` default, `"row"`, `"cells"`, `"cell"` — singular units imply exactly-one): row-unit buttons receive the rows containing the cell selection (or the focused cell) even when no row is explicitly selected, and cell-unit buttons get a `cells` array of `{ row, column, value }` in their action context. Set `select = { state = "<path>" }` to mirror the current row into app state as the selection moves, so a detail pane or chart can follow the table without any custom code; clearing the cursor publishes `null` (as do deleting that row and closing the pane), and a live update to that row republishes it. Set `values = { col = "<expr>" }` to derive a column with an expression over the row — the derived value is what the table renders, sorts, filters, and copies, and it may be a virtual column that no row carries (list it in `columns`). **Conditional styling** colors cells and rows from their values: `styles = { col = <styler> }` styles a cell, `rowStyle = <styler>` styles the whole row, where a styler is a rule array evaluated first-match-wins — each rule is `{ when = "<expr>", ...style keys }`, a rule without `when` being the fallback — or a single expression yielding a style map; style keys are `color`, `background`, `bold`, `italic`, `underline`, `strike`, custom `class`, and arbitrary `css`, and any string value may be a `${...}` template. Styled backgrounds blend with, rather than hide, the selection tint, and styles are recomputed on every live update. Toolbar buttons take `enable.when = "<expr>"` over the selected rows. **Display templates** (`display = { col = "${...}" }`) control presentation without touching the value the table sorts and filters by: `${NUM(value, digits: 2, group: TRUE)}` formats, and the `mkui` function library produces rich text — `BOLD`, `ITALIC`, `COLOR`, `MUTED`, `MONO`, `BADGE(x, color)`, `ICON(name)`, `BAR(frac, color)`, `LINK(x, url)`, `HEAT(v, lo, hi)` — which renders as styled spans in the cell, pastes into spreadsheets with its colors and weights intact, and flattens to plain text everywhere else; a template that errors shows `#ERR` with the message on hover.
427
+ - `mkio-table` — subscribes to an mkio service (query, subpub, or stream) and renders a live-updating table with flash animations for inserts, deletes, and field changes. Row identity is per-protocol: query uses `_mkio_row`, stream uses `_mkio_ref`, subpub uses `_mkio_topic`. Column headers render immediately when `columns` is configured; optional `labels` maps column keys to display text (e.g. `{ "ts": "Timestamp" }`). Rows are **virtualized** — only the slice overlapping the viewport exists in the DOM, so scrolling, window moves, and window resizes stay fast into the millions of rows. Columns are fully interactive: click a header to sort (shift+click adds secondary sort keys), the hamburger button opens a per-column filter dropdown with search and value checkboxes (the list opens tall enough for every value, stopping short of the app's statusbar, and its corner grip resizes it — the height you drag is kept while the pane lives; the column picker's list behaves the same) — the filter remembers what you meant rather than what was listed: untick values (or Select all, then untick) to hide just those and let everything else through, including values that arrive later via live inserts, updates, or the next stream page; Clear, then tick values to show only those, with anything new staying hidden — numeric columns and columns of timestamps (mkio refs, ISO-8601 dates/date-times, or `HH:MM[:SS]` clock times) add a **Range** mode with From/To bounds (native date/time pickers for time columns, plus Today / Last hour / Last 15 min presets that keep moving with the clock) and an *include empty* toggle; other date formats aren't guessed — declare them with `types = { col = { type = "time", parse = "%d/%m/%Y %H:%M" } }` (or `unit = "ms"` for epoch numbers, `tz = "local"` for naive local timestamps). **Default filters** come from config: `filters = { status = { exclude = ["cancelled"] }, qty = { from = 100, to = 500 }, ts = { preset = "1h" } }` seeds the same filters before any data arrives (a bare list `["open", "new"]` means *only those*; `from`/`to` bounds make a number range when they are numbers and a time range when they are strings such as `"2026-03-01"` or `"2026-03-01 09:30"`, a date covering its whole day; `empty = true` lets blank values through; `preset` is `today`, `1h`, or `15m`), and reopening the pane restores them. The same shape drives the programmatic API — `workspace.setPaneFilters(id, filters, { merge })` / `getPaneFilters(id)` — and the `table.filter` menu action. **Sort** likewise comes from config — `sort = "-ts"` or `sort = [{ col = "status" }, "-name"]` (a column name, `-name` for descending, `{ col, dir }`, or an array in priority order), restored on pane reopen and driven programmatically by `workspace.setPaneSort(id, sort)` / `getPaneSort(id)` and the `table.sort` action. **Column visibility** — `visible = ["id", "symbol", "qty"]` shows only those columns, in that order; without it every column shows, *including columns added later* to the config or the data, whereas an explicit list is exactly those — so a config can grow a table without disturbing a layout that was written down. Restored on pane reopen and driven programmatically by `workspace.setPaneColumns(id, visible)` / `getPaneColumns(id)` (which returns `null` while every column shows) and the `table.columns` action. A **Columns button** in a narrow gutter at the right edge of the header row (it stays put as the table scrolls, never covers a column or its resize grip, and wears a badge with the hidden count) opens the **column picker**, the one place for bulk changes: a checkbox per column and, under the search box, the same row the filter dropdown has — *Show all*, *Hide all* (keeps one column; the natural start when picking a few out of hundreds), and *Reset* (back to the config's list). Type a query and the first two become *Show N matching* / *Hide N matching* for whatever the list narrowed to. Unscoped *Show all* takes two clicks, the second a confirm, because on a table with hundreds of columns an accidental show-all is the one action that hurts. Each header's dropdown offers *Hide column* (the last one refuses); nothing under one column controls any other. **Column groups** — `groups = [{ label = "Pricing", columns = ["price", "notional"] }, …]` — categorise the picker: one collapsible section per group with a tri-state checkbox that shows or hides the whole group (bounded, its size printed beside it, one more click reverses it), folded unless it holds a shown column, plus an implicit *Other* for ungrouped columns; a search matches group labels too and unfolds the groups with matches. Groups are structure only, `visible` stays the truth about what shows, and when `columns` is omitted the display order follows the groups so a category's columns sit together. Sorting, filtering, and reordering keep working on hidden columns, and a hidden column comes back at the width it had. Whatever is active shows as **chips** on the table toolbar, so the state is visible and undoable without scrolling the header into view: the sort keys in priority order, then the filtered columns with the same summary the header tooltip gives. Click a sort chip to flip its direction or a filter chip to open that column's dropdown (showing the column first if it is hidden), × removes one, and the group's icon clears the whole group. The chips share the toolbar row with the selection buttons — buttons pinned left, chips flowing from the right, the cluster dropping to its own line when a narrow pane can't fit both — and the toolbar disappears when it has nothing to show — when a column is sorted, that header button's icon becomes the sort caret (still opening the filter dropdown, and showing the sort priority as a digit knocked out of the caret under multi-sort) — dragging a header reorders columns, and dragging the grip centered on a column divider resizes the column to its left — double-click the grip to auto-size the column to fit its content (capped at 80% of the viewport; with a selection active, all selected columns fit at once, so select-all + double-click fits the whole table). Column widths start at the header row's width and grow to fit records as they arrive (capped at 50% of the pane width, never shrinking, and never overriding a manual resize) — paging to another page never resizes them, and they don't change when the window resizes — extra pane width flows into a filler column that extends the header row to the pane's right edge, and subtle dividers separate the columns. Columns whose values are all numeric right-align with each value padded so decimal points line up down the column, in the cells and in the filter dropdown's value list alike. Subscriptions are deferred until the pane is first visible, unsubscribed immediately when the frame is closed, and dropped after 5 minutes of being hidden (e.g. inactive tab) — brief tab switches preserve the live connection. Large query snapshots render progressively in chunked batches to avoid freezing the UI. Stream tables support time-anchored paged navigation with a toolbar showing `◀ Earlier | time range | Later ▶ | ● Live | ⟳`. By default, the initial fetch starts from local midnight (`start: "today"`); `start: ""` starts from the beginning of the buffer. The toolbar displays the time range of visible rows in the browser's local timezone with adaptive precision — `HH:MM` down to nanoseconds in 3-digit increments depending on how close the boundary timestamps are; cross-day ranges include the date. Boundary indicators (`(start)`, `(end)`, `(all)`) show when you've reached the edges of the dataset. The `⟳` button re-fetches the current page. Navigation is ref-based (each page fetches relative to the first or last row's `_mkio_ref`), with `before: true` for backward fetches, so pages stay correct even when records are added or deleted mid-session. When navigating backward yields no data, the previous page is automatically restored and the Earlier button is disabled. A toggleable "Live" button switches to real-time streaming from the current page's last ref; in live mode, clicking Earlier uses a separate subscription to fetch and prepend historical pages without interrupting the live stream (toolbar shows `HH:mm – Live`). Live streams follow the tail like a terminal: entering live jumps to the newest row, and while the viewport is parked at the bottom, arriving rows keep scrolling into view — scroll up to inspect history and the viewport stays put. Set `live: true` to start in live mode: the start page loads first and then hands off to the live stream, so `start` is still honored rather than the whole buffer being replayed; exiting live returns to that start page, and reopening the pane re-arms it. Exiting live mode re-fetches the saved page from the server so that rows inserted or deleted during live mode are reflected. When the WebSocket disconnects while live mode is active, the toolbar shows "Disconnected" (or `HH:mm – Disconnected` with earlier pages) in muted text instead of the green blinking "Live" indicator — live mode stays active for seamless reconnect. Stream tables track the last received ref and use it on re-subscribe to avoid duplicate data transfer — existing rows are preserved and only new records are fetched. Query and subpub snapshots fully replace the table on each arrival, so records deleted on the server between disconnect and reconnect are properly removed. Tables are fully selectable, Excel-style: a sticky **row-number column** (disable with `rowColumn: false`) selects rows — click, ctrl/cmd-click to toggle, shift-click for ranges, drag for a range, or click the header corner to select all — while clicking a cell places the **focused cell** (accent outline; its whole row gets a subtle highlight so the record stays readable, distinct from the stronger row-selection tint). Drag across cells for a rectangle, ctrl/cmd-click to add or remove individual cells, shift-click or shift+arrows to extend; arrows/Home/End/PageUp/PageDown move the cursor, Space (or Shift+Space) selects the focused row, ctrl/cmd+Space toggles it, Ctrl/Cmd+A selects all visible rows, and Escape clears the selection. **Ctrl/Cmd+C copies** the selection to the OS clipboard in both TSV and HTML flavors, so pasting into Excel/Sheets preserves cell and row structure — row selections include a header row of column labels; cell selections copy values only, with blanks outside the selected rectangles; the copied cells pulse and the statusbar briefly shows "Copied N rows/cells" (via the `status.message` state path). Selection follows the filtered view (hidden rows are never selected or copied) and tracks records rather than positions: re-sorting moves the selection with its rows (rows that sort into the middle of a selected block don't join it), rows inserted live inside a selected range stay unselected, and members hidden by a filter rejoin the selection when the filter is relaxed. Table toolbar buttons can declare a selection `unit` (`"rows"` default, `"row"`, `"cells"`, `"cell"` — singular units imply exactly-one): row-unit buttons receive the rows containing the cell selection (or the focused cell) even when no row is explicitly selected, and cell-unit buttons get a `cells` array of `{ row, column, value }` in their action context. Set `select = { state = "<path>" }` to mirror the current row into app state as the selection moves, so a detail pane or chart can follow the table without any custom code; clearing the cursor publishes `null` (as do deleting that row and closing the pane), and a live update to that row republishes it. Set `values = { col = "<expr>" }` to derive a column with an expression over the row — the derived value is what the table renders, sorts, filters, and copies, and it may be a virtual column that no row carries (list it in `columns`). **Conditional styling** colors cells and rows from their values: `styles = { col = <styler> }` styles a cell, `rowStyle = <styler>` styles the whole row, where a styler is a rule array evaluated first-match-wins — each rule is `{ when = "<expr>", ...style keys }`, a rule without `when` being the fallback — or a single expression yielding a style map; style keys are `color`, `background`, `bold`, `italic`, `underline`, `strike`, custom `class`, and arbitrary `css`, and any string value may be a `${...}` template. Styled backgrounds blend with, rather than hide, the selection tint, and styles are recomputed on every live update. Toolbar buttons take `enable.when = "<expr>"` over the selected rows. **Display templates** (`display = { col = "${...}" }`) control presentation without touching the value the table sorts and filters by: `${NUM(value, digits: 2, group: TRUE)}` formats, and the `mkui` function library produces rich text — `BOLD`, `ITALIC`, `COLOR`, `MUTED`, `MONO`, `BADGE(x, color)`, `ICON(name)`, `BAR(frac, color)`, `LINK(x, url)`, `HEAT(v, lo, hi)` — which renders as styled spans in the cell, pastes into spreadsheets with its colors and weights intact, and flattens to plain text everywhere else; a template that errors shows `#ERR` with the message on hover.
426
428
  - Dialogs:
427
429
  - `openDialog(spec, context, app, extra)` — config-driven modal dialog with typed fields (text, number, select, checkbox, textarea, readonly, hidden), validation, conditional visibility (`showWhen`), async service-backed options (`optionsFrom`), and RPC submission with error handling. The dialog floats as a non-docking frame whose title text doubles as a drag handle; if the rendered form is taller than the initial frame, the frame grows to fit it (capped at 90% of the workspace) and re-centers. A **pin button** (SVG pin icon) in the titlebar keeps the dialog open after successful submission — when active, the pin rotates 45° counterclockwise with a smooth transition; the form resets to defaults only after the server confirms success; errors leave the form intact for retry.
428
430
  - Custom pane types are the primary extensibility surface. Register with
@@ -294,8 +294,10 @@ Ctrl/Cmd+C and Ctrl/Cmd+A shortcuts take), `table.filter` (`args =
294
294
  table's column filters from a menu — same shapes as the pane's `filters`
295
295
  key; omit `pane` for the focused pane), and `table.sort` (`args = { pane
296
296
  = "<id>", sort = <spec> }` sets its sort order — same shape as the pane's
297
- `sort` key; no `sort` clears it). Register custom actions with
298
- `app.registerAction(name, fn)`.
297
+ `sort` key; no `sort` clears it), and `table.columns` (`args = { pane =
298
+ "<id>", visible = [...] }` sets which columns show — same shape as the
299
+ pane's `visible` key; no `visible` shows them all). Register custom
300
+ actions with `app.registerAction(name, fn)`.
299
301
 
300
302
  A typical Edit menu:
301
303
 
@@ -402,7 +404,7 @@ root.workspace.addFrame({ x: 0.5, y: 0.1, w: 0.4, h: 0.4,
402
404
  - `text` — static or `bind`-ed to a state path
403
405
  - `button` — fires an action by name
404
406
  - Pane types (whole-pane custom rendering):
405
- - `mkio-table` — subscribes to an mkio service (query, subpub, or stream) and renders a live-updating table with flash animations for inserts, deletes, and field changes. Row identity is per-protocol: query uses `_mkio_row`, stream uses `_mkio_ref`, subpub uses `_mkio_topic`. Column headers render immediately when `columns` is configured; optional `labels` maps column keys to display text (e.g. `{ "ts": "Timestamp" }`). Rows are **virtualized** — only the slice overlapping the viewport exists in the DOM, so scrolling, window moves, and window resizes stay fast into the millions of rows. Columns are fully interactive: click a header to sort (shift+click adds secondary sort keys), the hamburger button opens a per-column filter dropdown with search and value checkboxes — the filter remembers what you meant rather than what was listed: untick values (or Select all, then untick) to hide just those and let everything else through, including values that arrive later via live inserts, updates, or the next stream page; Clear, then tick values to show only those, with anything new staying hidden — numeric columns and columns of timestamps (mkio refs, ISO-8601 dates/date-times, or `HH:MM[:SS]` clock times) add a **Range** mode with From/To bounds (native date/time pickers for time columns, plus Today / Last hour / Last 15 min presets that keep moving with the clock) and an *include empty* toggle; other date formats aren't guessed — declare them with `types = { col = { type = "time", parse = "%d/%m/%Y %H:%M" } }` (or `unit = "ms"` for epoch numbers, `tz = "local"` for naive local timestamps). **Default filters** come from config: `filters = { status = { exclude = ["cancelled"] }, qty = { from = 100, to = 500 }, ts = { preset = "1h" } }` seeds the same filters before any data arrives (a bare list `["open", "new"]` means *only those*; `from`/`to` bounds make a number range when they are numbers and a time range when they are strings such as `"2026-03-01"` or `"2026-03-01 09:30"`, a date covering its whole day; `empty = true` lets blank values through; `preset` is `today`, `1h`, or `15m`), and reopening the pane restores them. The same shape drives the programmatic API — `workspace.setPaneFilters(id, filters, { merge })` / `getPaneFilters(id)` — and the `table.filter` menu action. **Sort** likewise comes from config — `sort = "-ts"` or `sort = [{ col = "status" }, "-name"]` (a column name, `-name` for descending, `{ col, dir }`, or an array in priority order), restored on pane reopen and driven programmatically by `workspace.setPaneSort(id, sort)` / `getPaneSort(id)` and the `table.sort` action. Whatever is active shows as **chips** on the table toolbar, so the state is visible and undoable without scrolling the header into view: the sort keys in priority order, then the filtered columns with the same summary the header tooltip gives. Click a sort chip to flip its direction or a filter chip to open that column's dropdown, × removes one, and the group's icon clears the whole group. The chips share the toolbar row with the selection buttons — buttons pinned left, chips flowing from the right, the cluster dropping to its own line when a narrow pane can't fit both — and the toolbar disappears when it has nothing to show — when a column is sorted, that header button's icon becomes the sort caret (still opening the filter dropdown, and showing the sort priority as a digit knocked out of the caret under multi-sort) — dragging a header reorders columns, and dragging the grip centered on a column divider resizes the column to its left — double-click the grip to auto-size the column to fit its content (capped at 80% of the viewport; with a selection active, all selected columns fit at once, so select-all + double-click fits the whole table). Column widths start at the header row's width and grow to fit records as they arrive (capped at 50% of the pane width, never shrinking, and never overriding a manual resize) — paging to another page never resizes them, and they don't change when the window resizes — extra pane width flows into a filler column that extends the header row to the pane's right edge, and subtle dividers separate the columns. Columns whose values are all numeric right-align with each value padded so decimal points line up down the column, in the cells and in the filter dropdown's value list alike. Subscriptions are deferred until the pane is first visible, unsubscribed immediately when the frame is closed, and dropped after 5 minutes of being hidden (e.g. inactive tab) — brief tab switches preserve the live connection. Large query snapshots render progressively in chunked batches to avoid freezing the UI. Stream tables support time-anchored paged navigation with a toolbar showing `◀ Earlier | time range | Later ▶ | ● Live | ⟳`. By default, the initial fetch starts from local midnight (`start: "today"`); `start: ""` starts from the beginning of the buffer. The toolbar displays the time range of visible rows in the browser's local timezone with adaptive precision — `HH:MM` down to nanoseconds in 3-digit increments depending on how close the boundary timestamps are; cross-day ranges include the date. Boundary indicators (`(start)`, `(end)`, `(all)`) show when you've reached the edges of the dataset. The `⟳` button re-fetches the current page. Navigation is ref-based (each page fetches relative to the first or last row's `_mkio_ref`), with `before: true` for backward fetches, so pages stay correct even when records are added or deleted mid-session. When navigating backward yields no data, the previous page is automatically restored and the Earlier button is disabled. A toggleable "Live" button switches to real-time streaming from the current page's last ref; in live mode, clicking Earlier uses a separate subscription to fetch and prepend historical pages without interrupting the live stream (toolbar shows `HH:mm – Live`). Live streams follow the tail like a terminal: entering live jumps to the newest row, and while the viewport is parked at the bottom, arriving rows keep scrolling into view — scroll up to inspect history and the viewport stays put. Set `live: true` to start in live mode: the start page loads first and then hands off to the live stream, so `start` is still honored rather than the whole buffer being replayed; exiting live returns to that start page, and reopening the pane re-arms it. Exiting live mode re-fetches the saved page from the server so that rows inserted or deleted during live mode are reflected. When the WebSocket disconnects while live mode is active, the toolbar shows "Disconnected" (or `HH:mm – Disconnected` with earlier pages) in muted text instead of the green blinking "Live" indicator — live mode stays active for seamless reconnect. Stream tables track the last received ref and use it on re-subscribe to avoid duplicate data transfer — existing rows are preserved and only new records are fetched. Query and subpub snapshots fully replace the table on each arrival, so records deleted on the server between disconnect and reconnect are properly removed. Tables are fully selectable, Excel-style: a sticky **row-number column** (disable with `rowColumn: false`) selects rows — click, ctrl/cmd-click to toggle, shift-click for ranges, drag for a range, or click the header corner to select all — while clicking a cell places the **focused cell** (accent outline; its whole row gets a subtle highlight so the record stays readable, distinct from the stronger row-selection tint). Drag across cells for a rectangle, ctrl/cmd-click to add or remove individual cells, shift-click or shift+arrows to extend; arrows/Home/End/PageUp/PageDown move the cursor, Space (or Shift+Space) selects the focused row, ctrl/cmd+Space toggles it, Ctrl/Cmd+A selects all visible rows, and Escape clears the selection. **Ctrl/Cmd+C copies** the selection to the OS clipboard in both TSV and HTML flavors, so pasting into Excel/Sheets preserves cell and row structure — row selections include a header row of column labels; cell selections copy values only, with blanks outside the selected rectangles; the copied cells pulse and the statusbar briefly shows "Copied N rows/cells" (via the `status.message` state path). Selection follows the filtered view (hidden rows are never selected or copied) and tracks records rather than positions: re-sorting moves the selection with its rows (rows that sort into the middle of a selected block don't join it), rows inserted live inside a selected range stay unselected, and members hidden by a filter rejoin the selection when the filter is relaxed. Table toolbar buttons can declare a selection `unit` (`"rows"` default, `"row"`, `"cells"`, `"cell"` — singular units imply exactly-one): row-unit buttons receive the rows containing the cell selection (or the focused cell) even when no row is explicitly selected, and cell-unit buttons get a `cells` array of `{ row, column, value }` in their action context. Set `select = { state = "<path>" }` to mirror the current row into app state as the selection moves, so a detail pane or chart can follow the table without any custom code; clearing the cursor publishes `null` (as do deleting that row and closing the pane), and a live update to that row republishes it. Set `values = { col = "<expr>" }` to derive a column with an expression over the row — the derived value is what the table renders, sorts, filters, and copies, and it may be a virtual column that no row carries (list it in `columns`). **Conditional styling** colors cells and rows from their values: `styles = { col = <styler> }` styles a cell, `rowStyle = <styler>` styles the whole row, where a styler is a rule array evaluated first-match-wins — each rule is `{ when = "<expr>", ...style keys }`, a rule without `when` being the fallback — or a single expression yielding a style map; style keys are `color`, `background`, `bold`, `italic`, `underline`, `strike`, custom `class`, and arbitrary `css`, and any string value may be a `${...}` template. Styled backgrounds blend with, rather than hide, the selection tint, and styles are recomputed on every live update. Toolbar buttons take `enable.when = "<expr>"` over the selected rows. **Display templates** (`display = { col = "${...}" }`) control presentation without touching the value the table sorts and filters by: `${NUM(value, digits: 2, group: TRUE)}` formats, and the `mkui` function library produces rich text — `BOLD`, `ITALIC`, `COLOR`, `MUTED`, `MONO`, `BADGE(x, color)`, `ICON(name)`, `BAR(frac, color)`, `LINK(x, url)`, `HEAT(v, lo, hi)` — which renders as styled spans in the cell, pastes into spreadsheets with its colors and weights intact, and flattens to plain text everywhere else; a template that errors shows `#ERR` with the message on hover.
407
+ - `mkio-table` — subscribes to an mkio service (query, subpub, or stream) and renders a live-updating table with flash animations for inserts, deletes, and field changes. Row identity is per-protocol: query uses `_mkio_row`, stream uses `_mkio_ref`, subpub uses `_mkio_topic`. Column headers render immediately when `columns` is configured; optional `labels` maps column keys to display text (e.g. `{ "ts": "Timestamp" }`). Rows are **virtualized** — only the slice overlapping the viewport exists in the DOM, so scrolling, window moves, and window resizes stay fast into the millions of rows. Columns are fully interactive: click a header to sort (shift+click adds secondary sort keys), the hamburger button opens a per-column filter dropdown with search and value checkboxes (the list opens tall enough for every value, stopping short of the app's statusbar, and its corner grip resizes it — the height you drag is kept while the pane lives; the column picker's list behaves the same) — the filter remembers what you meant rather than what was listed: untick values (or Select all, then untick) to hide just those and let everything else through, including values that arrive later via live inserts, updates, or the next stream page; Clear, then tick values to show only those, with anything new staying hidden — numeric columns and columns of timestamps (mkio refs, ISO-8601 dates/date-times, or `HH:MM[:SS]` clock times) add a **Range** mode with From/To bounds (native date/time pickers for time columns, plus Today / Last hour / Last 15 min presets that keep moving with the clock) and an *include empty* toggle; other date formats aren't guessed — declare them with `types = { col = { type = "time", parse = "%d/%m/%Y %H:%M" } }` (or `unit = "ms"` for epoch numbers, `tz = "local"` for naive local timestamps). **Default filters** come from config: `filters = { status = { exclude = ["cancelled"] }, qty = { from = 100, to = 500 }, ts = { preset = "1h" } }` seeds the same filters before any data arrives (a bare list `["open", "new"]` means *only those*; `from`/`to` bounds make a number range when they are numbers and a time range when they are strings such as `"2026-03-01"` or `"2026-03-01 09:30"`, a date covering its whole day; `empty = true` lets blank values through; `preset` is `today`, `1h`, or `15m`), and reopening the pane restores them. The same shape drives the programmatic API — `workspace.setPaneFilters(id, filters, { merge })` / `getPaneFilters(id)` — and the `table.filter` menu action. **Sort** likewise comes from config — `sort = "-ts"` or `sort = [{ col = "status" }, "-name"]` (a column name, `-name` for descending, `{ col, dir }`, or an array in priority order), restored on pane reopen and driven programmatically by `workspace.setPaneSort(id, sort)` / `getPaneSort(id)` and the `table.sort` action. **Column visibility** — `visible = ["id", "symbol", "qty"]` shows only those columns, in that order; without it every column shows, *including columns added later* to the config or the data, whereas an explicit list is exactly those — so a config can grow a table without disturbing a layout that was written down. Restored on pane reopen and driven programmatically by `workspace.setPaneColumns(id, visible)` / `getPaneColumns(id)` (which returns `null` while every column shows) and the `table.columns` action. A **Columns button** in a narrow gutter at the right edge of the header row (it stays put as the table scrolls, never covers a column or its resize grip, and wears a badge with the hidden count) opens the **column picker**, the one place for bulk changes: a checkbox per column and, under the search box, the same row the filter dropdown has — *Show all*, *Hide all* (keeps one column; the natural start when picking a few out of hundreds), and *Reset* (back to the config's list). Type a query and the first two become *Show N matching* / *Hide N matching* for whatever the list narrowed to. Unscoped *Show all* takes two clicks, the second a confirm, because on a table with hundreds of columns an accidental show-all is the one action that hurts. Each header's dropdown offers *Hide column* (the last one refuses); nothing under one column controls any other. **Column groups** — `groups = [{ label = "Pricing", columns = ["price", "notional"] }, …]` — categorise the picker: one collapsible section per group with a tri-state checkbox that shows or hides the whole group (bounded, its size printed beside it, one more click reverses it), folded unless it holds a shown column, plus an implicit *Other* for ungrouped columns; a search matches group labels too and unfolds the groups with matches. Groups are structure only, `visible` stays the truth about what shows, and when `columns` is omitted the display order follows the groups so a category's columns sit together. Sorting, filtering, and reordering keep working on hidden columns, and a hidden column comes back at the width it had. Whatever is active shows as **chips** on the table toolbar, so the state is visible and undoable without scrolling the header into view: the sort keys in priority order, then the filtered columns with the same summary the header tooltip gives. Click a sort chip to flip its direction or a filter chip to open that column's dropdown (showing the column first if it is hidden), × removes one, and the group's icon clears the whole group. The chips share the toolbar row with the selection buttons — buttons pinned left, chips flowing from the right, the cluster dropping to its own line when a narrow pane can't fit both — and the toolbar disappears when it has nothing to show — when a column is sorted, that header button's icon becomes the sort caret (still opening the filter dropdown, and showing the sort priority as a digit knocked out of the caret under multi-sort) — dragging a header reorders columns, and dragging the grip centered on a column divider resizes the column to its left — double-click the grip to auto-size the column to fit its content (capped at 80% of the viewport; with a selection active, all selected columns fit at once, so select-all + double-click fits the whole table). Column widths start at the header row's width and grow to fit records as they arrive (capped at 50% of the pane width, never shrinking, and never overriding a manual resize) — paging to another page never resizes them, and they don't change when the window resizes — extra pane width flows into a filler column that extends the header row to the pane's right edge, and subtle dividers separate the columns. Columns whose values are all numeric right-align with each value padded so decimal points line up down the column, in the cells and in the filter dropdown's value list alike. Subscriptions are deferred until the pane is first visible, unsubscribed immediately when the frame is closed, and dropped after 5 minutes of being hidden (e.g. inactive tab) — brief tab switches preserve the live connection. Large query snapshots render progressively in chunked batches to avoid freezing the UI. Stream tables support time-anchored paged navigation with a toolbar showing `◀ Earlier | time range | Later ▶ | ● Live | ⟳`. By default, the initial fetch starts from local midnight (`start: "today"`); `start: ""` starts from the beginning of the buffer. The toolbar displays the time range of visible rows in the browser's local timezone with adaptive precision — `HH:MM` down to nanoseconds in 3-digit increments depending on how close the boundary timestamps are; cross-day ranges include the date. Boundary indicators (`(start)`, `(end)`, `(all)`) show when you've reached the edges of the dataset. The `⟳` button re-fetches the current page. Navigation is ref-based (each page fetches relative to the first or last row's `_mkio_ref`), with `before: true` for backward fetches, so pages stay correct even when records are added or deleted mid-session. When navigating backward yields no data, the previous page is automatically restored and the Earlier button is disabled. A toggleable "Live" button switches to real-time streaming from the current page's last ref; in live mode, clicking Earlier uses a separate subscription to fetch and prepend historical pages without interrupting the live stream (toolbar shows `HH:mm – Live`). Live streams follow the tail like a terminal: entering live jumps to the newest row, and while the viewport is parked at the bottom, arriving rows keep scrolling into view — scroll up to inspect history and the viewport stays put. Set `live: true` to start in live mode: the start page loads first and then hands off to the live stream, so `start` is still honored rather than the whole buffer being replayed; exiting live returns to that start page, and reopening the pane re-arms it. Exiting live mode re-fetches the saved page from the server so that rows inserted or deleted during live mode are reflected. When the WebSocket disconnects while live mode is active, the toolbar shows "Disconnected" (or `HH:mm – Disconnected` with earlier pages) in muted text instead of the green blinking "Live" indicator — live mode stays active for seamless reconnect. Stream tables track the last received ref and use it on re-subscribe to avoid duplicate data transfer — existing rows are preserved and only new records are fetched. Query and subpub snapshots fully replace the table on each arrival, so records deleted on the server between disconnect and reconnect are properly removed. Tables are fully selectable, Excel-style: a sticky **row-number column** (disable with `rowColumn: false`) selects rows — click, ctrl/cmd-click to toggle, shift-click for ranges, drag for a range, or click the header corner to select all — while clicking a cell places the **focused cell** (accent outline; its whole row gets a subtle highlight so the record stays readable, distinct from the stronger row-selection tint). Drag across cells for a rectangle, ctrl/cmd-click to add or remove individual cells, shift-click or shift+arrows to extend; arrows/Home/End/PageUp/PageDown move the cursor, Space (or Shift+Space) selects the focused row, ctrl/cmd+Space toggles it, Ctrl/Cmd+A selects all visible rows, and Escape clears the selection. **Ctrl/Cmd+C copies** the selection to the OS clipboard in both TSV and HTML flavors, so pasting into Excel/Sheets preserves cell and row structure — row selections include a header row of column labels; cell selections copy values only, with blanks outside the selected rectangles; the copied cells pulse and the statusbar briefly shows "Copied N rows/cells" (via the `status.message` state path). Selection follows the filtered view (hidden rows are never selected or copied) and tracks records rather than positions: re-sorting moves the selection with its rows (rows that sort into the middle of a selected block don't join it), rows inserted live inside a selected range stay unselected, and members hidden by a filter rejoin the selection when the filter is relaxed. Table toolbar buttons can declare a selection `unit` (`"rows"` default, `"row"`, `"cells"`, `"cell"` — singular units imply exactly-one): row-unit buttons receive the rows containing the cell selection (or the focused cell) even when no row is explicitly selected, and cell-unit buttons get a `cells` array of `{ row, column, value }` in their action context. Set `select = { state = "<path>" }` to mirror the current row into app state as the selection moves, so a detail pane or chart can follow the table without any custom code; clearing the cursor publishes `null` (as do deleting that row and closing the pane), and a live update to that row republishes it. Set `values = { col = "<expr>" }` to derive a column with an expression over the row — the derived value is what the table renders, sorts, filters, and copies, and it may be a virtual column that no row carries (list it in `columns`). **Conditional styling** colors cells and rows from their values: `styles = { col = <styler> }` styles a cell, `rowStyle = <styler>` styles the whole row, where a styler is a rule array evaluated first-match-wins — each rule is `{ when = "<expr>", ...style keys }`, a rule without `when` being the fallback — or a single expression yielding a style map; style keys are `color`, `background`, `bold`, `italic`, `underline`, `strike`, custom `class`, and arbitrary `css`, and any string value may be a `${...}` template. Styled backgrounds blend with, rather than hide, the selection tint, and styles are recomputed on every live update. Toolbar buttons take `enable.when = "<expr>"` over the selected rows. **Display templates** (`display = { col = "${...}" }`) control presentation without touching the value the table sorts and filters by: `${NUM(value, digits: 2, group: TRUE)}` formats, and the `mkui` function library produces rich text — `BOLD`, `ITALIC`, `COLOR`, `MUTED`, `MONO`, `BADGE(x, color)`, `ICON(name)`, `BAR(frac, color)`, `LINK(x, url)`, `HEAT(v, lo, hi)` — which renders as styled spans in the cell, pastes into spreadsheets with its colors and weights intact, and flattens to plain text everywhere else; a template that errors shows `#ERR` with the message on hover.
406
408
  - Dialogs:
407
409
  - `openDialog(spec, context, app, extra)` — config-driven modal dialog with typed fields (text, number, select, checkbox, textarea, readonly, hidden), validation, conditional visibility (`showWhen`), async service-backed options (`optionsFrom`), and RPC submission with error handling. The dialog floats as a non-docking frame whose title text doubles as a drag handle; if the rendered form is taller than the initial frame, the frame grows to fit it (capped at 90% of the workspace) and re-centers. A **pin button** (SVG pin icon) in the titlebar keeps the dialog open after successful submission — when active, the pin rotates 45° counterclockwise with a smooth transition; the form resets to defaults only after the server confirms success; errors leave the form intact for retry.
408
410
  - Custom pane types are the primary extensibility surface. Register with
@@ -2,7 +2,7 @@
2
2
 
3
3
  from pathlib import Path
4
4
 
5
- __version__ = "0.2.4"
5
+ __version__ = "0.2.6"
6
6
 
7
7
  static_dir = Path(__file__).parent / "static"
8
8
  """Path to the directory containing mkui's JS, CSS, and example files.
@@ -40,6 +40,11 @@ items = [
40
40
  { label = "All Orders: newest first", action = "table.sort", args = { pane = "all-orders", sort = "-ts" } },
41
41
  { label = "All Orders: by symbol, largest first", action = "table.sort", args = { pane = "all-orders", sort = ["symbol", { col = "qty", dir = "desc" }] } },
42
42
  { label = "All Orders: clear sort", action = "table.sort", args = { pane = "all-orders" } },
43
+ { sep = true },
44
+ # table.columns sets which columns a pane shows, in that order; the shape
45
+ # matches `visible` under [panes.<id>]. No `visible` shows every column.
46
+ { label = "All Orders: compact view", action = "table.columns", args = { pane = "all-orders", visible = ["symbol", "side", "qty", "notional"] } },
47
+ { label = "All Orders: all columns", action = "table.columns", args = { pane = "all-orders" } },
43
48
  ]
44
49
 
45
50
  [[menubar]]
@@ -83,6 +88,22 @@ protocol = "query"
83
88
  columns = ["id", "ts", "side", "symbol", "qty", "price", "notional", "status"]
84
89
  labels = { ts = "Time" }
85
90
  values = { notional = "ROUND(qty * price, 2)" }
91
+ # Which columns show, in this order. `id` and `price` start hidden — the
92
+ # Columns button at the header's right edge (badge: 2) opens the picker
93
+ # that brings them back; each header's dropdown has "Hide column". Hidden
94
+ # columns still work: `notional` is derived from the hidden
95
+ # `price`, and sorts and filters on hidden columns still apply. Without
96
+ # `visible`, every column shows, including ones added later.
97
+ visible = ["ts", "side", "symbol", "qty", "notional", "status"]
98
+ # Column groups categorise the picker (sections with show/hide-the-group
99
+ # toggles; ungrouped columns fall into "Other"). Structure only — `visible`
100
+ # says what shows. With hundreds of columns this is how a user finds the
101
+ # few they want; the config's `visible` is the curated default they can
102
+ # always reset to.
103
+ groups = [
104
+ { label = "Order", columns = ["id", "ts", "side", "symbol"] },
105
+ { label = "Pricing", columns = ["qty", "price", "notional"] },
106
+ ]
86
107
  # Mirror the current row into app state; the order-detail pane follows it.
87
108
  select = { state = "selected_order" }
88
109
  # Conditional styling, config-only: rules are evaluated first-match-wins;
@@ -167,6 +188,10 @@ columns = ["id", "side", "symbol", "qty", "price", "status"]
167
188
  # largest quantity first. Active sorts and filters show as chips on the
168
189
  # toolbar, where they can be flipped, opened, or cleared.
169
190
  sort = ["symbol", "-qty"]
191
+ # Only these columns show, in this order (the rest are one "hidden" chip
192
+ # away). Without `visible`, every column shows — including any added to
193
+ # `columns` later; with it, the set is exactly this until someone changes it.
194
+ visible = ["symbol", "side", "qty", "price"]
170
195
 
171
196
  [[panes.pending.buttons]]
172
197
  label = "Fill"
@@ -68,6 +68,8 @@ class MkuiApp extends HTMLElement {
68
68
  // `args = { pane = "<id>", sort = <spec> }` — a column name, "-col",
69
69
  // { col, dir }, or an array of those; no `sort` clears.
70
70
  this._app.registerAction("table.sort", (app, a = {}) => ws.setPaneSort(a.pane ?? null, a.sort ?? null));
71
+ // Visible columns: `args = { pane, visible }`; no `visible` shows all.
72
+ this._app.registerAction("table.columns", (app, a = {}) => ws.setPaneColumns(a.pane ?? null, a.visible ?? null));
71
73
 
72
74
  const hasAuth = !!config.auth;
73
75
  const st = this._app.state;
@@ -137,7 +137,8 @@ class MkuiWorkspace extends HTMLElement {
137
137
  return fn ? fn() !== false : false;
138
138
  }
139
139
 
140
- // A pane's view hook (`_filters` or `_sort`, exposed by mkio-table). By
140
+ // A pane's view hook (`_filters`, `_sort`, or `_columns`, exposed by
141
+ // mkio-table). By
141
142
  // id, a pane that was never shown is built first when `build` is set,
142
143
  // so a setter can run ahead of opening it; with no id, the focused
143
144
  // frame's active pane is the target.
@@ -178,6 +179,21 @@ class MkuiWorkspace extends HTMLElement {
178
179
  return this._paneHook(paneId, "_sort", false)?.get() ?? null;
179
180
  }
180
181
 
182
+ // Visible columns: the shape the pane's `visible` key takes — a column
183
+ // name or an array in display order; null shows every column (and follows
184
+ // new ones). Returns whether a pane took it.
185
+ setPaneColumns(paneId, visible) {
186
+ const hook = this._paneHook(paneId, "_columns", true);
187
+ if (!hook) return false;
188
+ hook.set(visible);
189
+ return true;
190
+ }
191
+
192
+ // The list back, null when every column shows (or without a columns hook).
193
+ getPaneColumns(paneId) {
194
+ return this._paneHook(paneId, "_columns", false)?.get() ?? null;
195
+ }
196
+
181
197
  setApp(app) {
182
198
  this._app = app;
183
199
  this._panes = new Map(Object.entries(app.config.panes ?? {}));
@@ -26,6 +26,8 @@ const OUTLINE = {
26
26
  "chevron-right": ["m9 18 6-6-6-6"],
27
27
  // Sort group icon on the table toolbar chips (Lucide arrow-up-down).
28
28
  sort: ["m21 16-4 4-4-4", "M17 20V4", "m3 8 4-4 4 4", "M7 4v16"],
29
+ // Hidden-columns group icon on the table toolbar chips (Lucide columns-3).
30
+ columns: ["M5 3h14a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2z", "M9 3v18", "M15 3v18"],
29
31
  // Status glyphs for rich cell text (ICON('check') / ICON('clock')).
30
32
  check: ["M20 6 9 17l-5-5"],
31
33
  clock: ["M12 2a10 10 0 1 1 0 20 10 10 0 0 1 0-20z", "M12 6v6l4 2"],