Context
Today every track in a config must live inside a GroupConfig. The default UniProt viewer has 15 groups, each with one or more tracks, so the rule fits the canonical layout — but it forces awkward modelling for embedders whose data shapes don't naturally cluster. A small lab pointing the viewer at a single CRISPR-guide track has to wrap that one track in a one-track group with a synthetic id and label. Likewise, configs that want a couple of standalone "headline" tracks alongside the canonical UniProt grouping have no way to express it: every track gets a group wrapper or none does.
The schema's authoring ergonomics don't need a one-or-the-other choice — the renderer treats a one-track group the same as a standalone track for layout purposes. We can let authors mix freely: top-level tracks for the simple cases, groups for clusters, both in the same config when that's the right model.
Task
Extend the config schema to accept top-level tracks alongside top-level groups, and thread the change through validate / normalize / render. Authors should be able to write:
groups:
- id: signal_peptide # standalone track, no group wrapper
kind: features
filter: SIGNAL
data: features
- id: DOMAINS # group with child tracks
tracks:
- id: domain
kind: features
filter: DOMAIN
data: features
- id: confidence # another standalone track
kind: confidence-score
data: { source: alphafold-prediction }
…and have the viewer render two single rows (Signal peptide, Confidence) bracketing one collapsible group (Domains).
Scope:
- Schema discrimination.
GroupConfig is currently distinguished by the presence of tracks: TrackConfig[]. Extend the JSON Schema and types.ts so each entry under the top-level groups: is a discriminated union of GroupConfig and TrackConfig. Use tracks: as the discriminator (presence → group; absence → track). Keep groups: as the field name for back-compat — renaming to a more generic items: is a separate breaking-change conversation.
- Validator. Extend the unique-id check to span the entire top-level array (groups and standalone tracks share an id namespace at the top level — collisions across the two shapes are still a config bug). Update the "Track has no rendering path" check so standalone tracks (no parent group component) still validate when they have a
kind or explicit component.
- Normalizer. Standalone tracks need their own
NormalizedTrack shape at the top level, OR they can be wrapped in a synthetic single-track NormalizedGroup with label === track.label and a CSS hint so the renderer omits the collapsible header. Pick one — wrapping is simpler for downstream code; the bare-track shape is more honest. Document the decision in the architecture doc.
- Renderer. A standalone-track entry should render as a single row with no group-collapse affordance. A one-track group should keep its collapse affordance — the difference is author-controlled, not auto-collapsed.
- Inheritance. Standalone tracks inherit from
defaults.rendering directly, with no group-rendering layer. The cascade for a standalone track is defaults → kind preset → track. Update normalize.ts's cascade and the JSDoc that documents it.
- Extends merger. Mixed-shape merging needs care: a child config replacing a base group with a standalone track (or vice versa) should be permitted but explicit. Either it's fine and child-wins, or it's a structural config bug that should fail validation. Pick one and document the choice.
- Tests. Add coverage for: a config with one standalone track and zero groups; a config with mixed groups + standalone tracks; an extends merge replacing a group with a standalone track; a unique-id collision between a group id and a standalone track id; a standalone track with no
kind and no component (validation should catch it the same way it catches the group-track equivalent).
- Migration. Scan
src/default-config.yaml for groups whose only purpose is wrapping a single track. If any are clear candidates for standalone-track expression, update the default and confirm the rendered output is unchanged. (Likely zero candidates given the canonical layout, but worth a pass.)
- Architecture doc. Update
docs/architecture.md's "How a config becomes pixels" section to mention that standalone tracks bypass the group-rendering layer, and add a bullet under "Conventions and gotchas" about the shared id namespace.
Notes:
- The cleanest implementation models a standalone track as
GroupConfig | TrackConfig at the top level rather than introducing a separate topLevelTracks: array. Two arrays at the same level encourages authors to think about ordering twice; one array preserves declaration order naturally.
- The discriminator-by-presence-of-
tracks approach has a sharp edge: a track that happens to have a property called tracks (it shouldn't, but YAML is forgiving) would be misclassified. The structural validator should explicitly forbid tracks on a track-shaped entry to keep the discrimination unambiguous.
- Don't add a "type": "group" / "type": "track" field — that's a worse authoring surface than presence-based discrimination, and the schema can express the discrimination cleanly via
oneOf.
- Keep
groups: as the field name even though it now accepts non-groups. Renaming to items: would be a breaking change for every existing config in the world, including the default, with marginal authoring benefit.
Context
Today every track in a config must live inside a
GroupConfig. The default UniProt viewer has 15 groups, each with one or more tracks, so the rule fits the canonical layout — but it forces awkward modelling for embedders whose data shapes don't naturally cluster. A small lab pointing the viewer at a single CRISPR-guide track has to wrap that one track in a one-track group with a synthetic id and label. Likewise, configs that want a couple of standalone "headline" tracks alongside the canonical UniProt grouping have no way to express it: every track gets a group wrapper or none does.The schema's authoring ergonomics don't need a one-or-the-other choice — the renderer treats a one-track group the same as a standalone track for layout purposes. We can let authors mix freely: top-level tracks for the simple cases, groups for clusters, both in the same config when that's the right model.
Task
Extend the config schema to accept top-level tracks alongside top-level groups, and thread the change through validate / normalize / render. Authors should be able to write:
…and have the viewer render two single rows (
Signal peptide,Confidence) bracketing one collapsible group (Domains).Scope:
GroupConfigis currently distinguished by the presence oftracks: TrackConfig[]. Extend the JSON Schema andtypes.tsso each entry under the top-levelgroups:is a discriminated union ofGroupConfigandTrackConfig. Usetracks:as the discriminator (presence → group; absence → track). Keepgroups:as the field name for back-compat — renaming to a more genericitems:is a separate breaking-change conversation.kindor explicitcomponent.NormalizedTrackshape at the top level, OR they can be wrapped in a synthetic single-trackNormalizedGroupwithlabel === track.labeland a CSS hint so the renderer omits the collapsible header. Pick one — wrapping is simpler for downstream code; the bare-track shape is more honest. Document the decision in the architecture doc.defaults.renderingdirectly, with no group-rendering layer. The cascade for a standalone track isdefaults → kind preset → track. Update normalize.ts's cascade and the JSDoc that documents it.kindand nocomponent(validation should catch it the same way it catches the group-track equivalent).src/default-config.yamlfor groups whose only purpose is wrapping a single track. If any are clear candidates for standalone-track expression, update the default and confirm the rendered output is unchanged. (Likely zero candidates given the canonical layout, but worth a pass.)docs/architecture.md's "How a config becomes pixels" section to mention that standalone tracks bypass the group-rendering layer, and add a bullet under "Conventions and gotchas" about the shared id namespace.Notes:
GroupConfig | TrackConfigat the top level rather than introducing a separatetopLevelTracks:array. Two arrays at the same level encourages authors to think about ordering twice; one array preserves declaration order naturally.tracksapproach has a sharp edge: a track that happens to have a property calledtracks(it shouldn't, but YAML is forgiving) would be misclassified. The structural validator should explicitly forbidtrackson a track-shaped entry to keep the discrimination unambiguous.oneOf.groups:as the field name even though it now accepts non-groups. Renaming toitems:would be a breaking change for every existing config in the world, including the default, with marginal authoring benefit.