Skip to content

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). Sibling elite-dangerous-interstellar-scan follows 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

flowchart TB subgraph ingest["Data ingest"] JR[Journal / IFM / API cache] end subgraph store["Store layer"] SYS[Row records keyed by domain id] VIEWS[TableView[] + active view id] UI[UiState: pin, panel mode, …] end subgraph domain["Domain (pure)"] REG[fieldRegistry: fields + format + compare] FE[filterEvaluator] HE[highlightEvaluator] end subgraph ui["UI"] TW[TableWorkspace: toolbar + modals] TB[BodiesTable / *Table: grid] end JR --> SYS VIEWS --> FE VIEWS --> HE SYS --> FE SYS --> HE REG --> FE REG --> HE FE --> TB HE --> TB TW --> VIEWS TB --> VIEWS
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.
  • [ ] TableView includes columns, sort, headerFilters, filter, highlights.
  • [ ] Pure filterEvaluator and highlightEvaluator (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-types package (TypeScript-only contracts for TableView / FilterNode) if multiple apps diverge on shapes.
  • Cursor rule template docs/templates/cursor-rules/edos-data-tables.mdc synced into table-heavy app repos (like canvas mockups).