Data table guidance (EDOS)¶
Audience: Agent + maintainer
Normative guidance for feature-complete, data-dense tables in first-party EDOS hosted apps. This is not a shared table component library and not a runtime contract (see HOSTED_APP_CONTRACT.md for URL bootstrap and manifests).
Visual styling (colors, typography, row chrome) lives in DESIGN_GUIDANCE.md — Data tables. This document defines behavior, architecture, and data contracts.
Intent¶
- Stellar Scan–grade tables are the default bar for new EDOS tools whose primary surface is a sortable, filterable grid (body lists, system lists, journal viewers with column layouts, etc.).
- Stack-agnostic: implement in Solid, React, Vue, or vanilla — copy the patterns and shapes, not necessarily the same file layout.
- Reference implementation:
elite-dangerous-stellar-scan(BodiesTable,TableWorkspace,domain/views.ts). Siblingelite-dangerous-interstellar-scanfollows the same architecture (SystemsTable, shared view/filter/highlight model) when present in the workspace.
Adoption¶
| Audience | Expectation |
|---|---|
| First-party apps with a main data table | SHOULD meet the normative checklist below for new work and substantial table refactors. |
| Simple tables (read-only, <10 columns, no saved views) | MAY implement a subset; document omissions in the app capsule. |
| Agents | Read this page before adding or redesigning a table UI; verify against reference source paths — do not invent incompatible view/filter models. |
Architecture¶
| Layer | Responsibility |
|---|---|
| fieldRegistry | Declares columns: fieldId, type, label, formatters, allowed filter ops, optional custom filter kinds (materials, composition, signals, …). |
| TableView | Serializable config: columns, sort, header filters, advanced filter tree, highlight rules. |
| Store | Holds row data, view list, active view, dirty state, panel mode; schedules persistence. |
| Evaluators | Pure functions: filterBodies(rows, view, context) and highlight resolution — no filter logic inlined in table JSX. |
| Table component | Renders grid only: sort clicks, header filter popovers, column resize, row highlight classes. |
| TableWorkspace | Toolbar (view select, save/update, panel toggles) and modals for columns / filters / highlights / manage views. |
Reference implementation (Stellar Scan)¶
Paths are relative to the workspace root / app repo root elite-dangerous-stellar-scan/.
| Area | Path |
|---|---|
| Grid | src/components/BodiesTable.tsx |
| Shell (toolbar + modals) | src/components/TableWorkspace.tsx |
| Modal host | src/components/EdosModal.tsx |
| Column / filter / highlight editors | src/components/ColumnPicker.tsx, FilterBuilder.tsx, HighlightBuilder.tsx, FilterGroupEditor.tsx, FilterConditionControls.tsx |
| View switch confirm | src/components/ViewSwitchConfirmDialog.tsx |
| View model + builtins | src/domain/views.ts |
| Filter AST | src/domain/filterTypes.ts, filterEvaluator.ts, filterIds.ts |
| Highlights | src/domain/highlightTypes.ts, highlightEvaluator.ts |
| Fields | src/domain/fieldRegistry.ts |
| Store | src/store/systemsStore.tsx |
| Persistence | src/store/persistence.ts |
| View export/import | src/domain/viewExport.ts |
| URL view bootstrap | src/edos/viewsBootstrap.ts |
| Table CSS | src/index.css (.bodies-table, .th-filter-*, row highlight classes) |
Tests worth grepping when verifying behavior: src/domain/filterEvaluator.test.ts, src/domain/highlightEvaluator.test.ts, src/edos/viewsBootstrap.test.ts, src/domain/viewExport.test.ts.
Data contracts¶
These shapes are normative for EDOS table apps that adopt this guidance. App-specific row types replace StellarBodyRecord / InterstellarSystemRow; the view and filter models stay stable.
ViewColumn¶
type ViewColumn = {
fieldId: string
visible: boolean
order: number
/** User-resized width in pixels; omitted until first resize. */
widthPx?: number
}
const DEFAULT_COLUMN_WIDTH_PX = 120
const MIN_COLUMN_WIDTH_PX = 56
TableView¶
type TableView = {
id: string
name: string
columns: ViewColumn[]
sort: { fieldId: string; direction: 'asc' | 'desc' }[]
headerFilters: Record<string, HeaderFilter>
filter: FilterNode | null
highlights: HighlightRule[]
isDefault?: boolean
}
HeaderFilter and FilterNode¶
type FilterOp = 'eq' | 'neq' | 'gt' | 'gte' | 'lt' | 'lte' | 'contains' | 'empty'
type HeaderFilter = { op: FilterOp; value?: string }
type FilterNode =
| { kind: 'group'; id?: string; op: 'and' | 'or'; children: FilterNode[] }
| { kind: 'cond'; id?: string; fieldId: string; op: FilterOp; value?: string }
Example advanced filter (built-in “Landable terraformables” style):
{
"kind": "group",
"op": "and",
"children": [
{ "kind": "cond", "fieldId": "isLandable", "op": "eq", "value": "true" },
{ "kind": "cond", "fieldId": "terraformingState", "op": "contains", "value": "terraform" }
]
}
HighlightRule¶
type HighlightRule = {
id: string
effect: 'positive' | 'negative' | 'info' | 'warning' | 'remarkable' | 'mentionable'
icon?: 'question' | 'info' | 'warning' | 'check-circle' | null
filter: FilterNode | null
}
Row styling uses semantic effects mapped to CSS (and optional icon column when any rule in the view sets icon).
Normative feature checklist¶
Views and persistence¶
| Rule | Detail |
|---|---|
| Saved views | Named TableView objects; at least one default; ship built-in presets for common tasks where applicable (builtinViews() in reference). |
| Active view | Single active view id in UI state; switching views changes columns, sort, filters, and highlights together. |
| Dirty detection | Track whether the active view differs from last saved snapshot; offer Update current view and Save as… (unique name). |
| Switch confirm | When dirty, confirm before switching views (optional “don’t ask again” persisted in UI state). |
| Persistence | Persist views (and related UI state) to localStorage (or equivalent) with a versioned key; normalize/filter IDs on load (ensureFilterIds, ensureHighlightRules). |
| Import / export | SHOULD support view JSON export/import for power users when the reference app does (viewExport.ts). |
| URL bootstrap | MAY apply a view or filter from query params — document keys in the app capsule and viewsBootstrap.ts. Use app-specific keys (for example edos.tableView); do not use shell path routes or edos.view for table presets — see HOSTED_APP_CONTRACT.md — In-app navigation views. |
Columns¶
| Rule | Detail |
|---|---|
| Visibility + order | Column picker modal edits visible and order; only visible columns render. |
| Resize | Drag handle on header; respect MIN_COLUMN_WIDTH_PX; persist widthPx on commit (not only during drag draft). |
| Fixed icon column | When highlights use icons, reserve a fixed-width leading column (~48px). |
| Table width | Sum of column widths (+ icon column); horizontal scroll when wider than viewport. |
Sort¶
| Rule | Detail |
|---|---|
| Header click | Cycle asc → desc → none for that column; store at most one primary sort in view.sort[0] (reference behavior). |
| Indicator | Show direction on active sort column; suppress sort click while column resize is in progress. |
| Compare | Use field registry compareFieldValues (or equivalent), not raw string sort on formatted display strings. |
Filters¶
| Rule | Detail |
|---|---|
| Header filters | Per-column popover in header (headerFilters); funnel icon active when op is empty or value is non-empty. |
| Advanced filter | Modal filter builder edits view.filter tree (AND/OR groups + conditions). |
| Single evaluator | filterEvaluator applies both header filters and advanced tree (header filters typically AND-combined with tree). |
| Field-aware ops | Default op per field type from registry; custom filter kinds for domain-specific columns (materials, signals, …). |
| IDs on nodes | Assign stable ids on filter groups/conditions for editor UI (filterIds.ts). |
Highlights¶
| Rule | Detail |
|---|---|
| Row rules | Ordered HighlightRule[]; each rule has its own FilterNode. |
| Evaluator | highlightEvaluator returns classes and optional inline styles — table applies them to <tr>. |
| Icon column | Optional stack of icons per row when viewUsesHighlightIcons(view). |
| Modal editor | Highlight builder mirrors filter builder patterns (Modals). |
Layout and secondary UI¶
| Rule | Detail |
|---|---|
| Main column | Table is the primary viewport (edge-to-edge); see DESIGN_GUIDANCE — Layout patterns. |
| Modals, not sidebars | Columns, filters, highlights, and view management open in EdosModal (or equivalent three-region dialog) — not docked drawers. |
| Panel mode | Store panel: 'main' \| 'columns' \| 'filters' \| 'highlights' \| 'manage'; toggling toolbar buttons opens the matching modal. |
| Toolbar | View <select>, save/update actions, panel toggles; keep dense (~label-mono). |
Accessibility and UX¶
| Rule | Detail |
|---|---|
| Filter affordance | Header filter control is a real <button> with visible active state (.th-filter-btn-active). |
| Resize | Do not fire sort when releasing after a resize gesture. |
| Current row | MAY highlight active entity row (e.g. current system) distinct from rule-based highlights. |
| Empty state | Clear copy when filter excludes all rows or no data loaded yet. |
Minimal structural habits¶
Cross-stack HTML/CSS conventions from Stellar Scan (class names are conventional — copy semantics, not necessarily exact names):
Header row: label + sort indicator + filter button; resize handle on trailing edge.
<th class="bodies-table-th">
<button type="button" class="th-sort-btn" aria-label="Sort by …">…</button>
<button type="button" class="th-filter-btn th-filter-btn-active" aria-label="Filter …">
<!-- funnel svg -->
</button>
<span class="col-resize-handle" role="separator" aria-orientation="vertical" />
</th>
Modal entry: TableWorkspace renders one EdosModal whose title follows panel mode (Columns, Filters, Highlights, Manage views). Footer holds primary actions (Save, Apply, Cancel) per DESIGN_GUIDANCE — Modals.
Row highlight: apply evaluator output as class list + optional style on <tr>; semantic effects map to tokens (positive, negative, …) in CSS.
Anti-patterns¶
- Ad-hoc filter state in table components instead of
TableView+ evaluators. - Side drawers for column picker or filter builder on new EDOS tables (use modals).
- No view model on “advanced” tables — users lose sort/filter/column work on refresh.
- Formatting-only tables with no field registry (sort/filter semantics become inconsistent).
- Pasting 200+ lines of table logic into docs instead of linking
BodiesTable.tsx(docs drift). - Direct upstream API calls from table-driven apps — still route through api-cache per ARCHITECTURE.md.
- Second brand/layout for table chrome — reuse DESIGN_GUIDANCE tokens.
Agent checklist¶
Before shipping or substantially refactoring a data-dense table:
- [ ] Read this page and skim the reference file map.
- [ ]
TableViewincludes columns, sort,headerFilters,filter,highlights. - [ ] Pure
filterEvaluatorandhighlightEvaluator(or equivalent) — table component stays thin. - [ ]
fieldRegistry(or equivalent) owns types, format, compare, and filter op defaults. - [ ] Saved views + builtins + dirty detection + persist with versioned storage keys.
- [ ] Column picker, filter builder, highlight builder open in modals (Modals).
- [ ] Header sort cycle asc/desc/none; column resize persisted in view.
- [ ] Table styling aligned with DESIGN_GUIDANCE — Data tables.
- [ ] App capsule documents view bootstrap keys, persistence scope, and any intentional subset omissions.
Relationship to other docs¶
| Document | Relationship |
|---|---|
| DESIGN_GUIDANCE.md | Visual tokens, modals, embed layout; links here for table behavior. |
| AGENTS.md | Playbook entry when adding table UIs. |
| apps/elite-dangerous-stellar-scan.md | Deploy, bootstrap, pin — plus table file map. |
| apps/elite-dangerous-interstellar-scan.md | Sibling table app when implemented. |
| HOSTED_APP_CONTRACT.md | URL/manifest contract — orthogonal to table internals; shell sub-navigation uses path routes, not table preset keys. |
Future work (optional)¶
- Shared
@howfe/edos-table-typespackage (TypeScript-only contracts forTableView/FilterNode) if multiple apps diverge on shapes. - Cursor rule template
docs/templates/cursor-rules/edos-data-tables.mdcsynced into table-heavy app repos (like canvas mockups).