CngxTimelineItem
projects/common/timeline/timeline-item.component.ts
Import#
import { CngxTimelineItem } from '@cngx/common/timeline'
Description#
One event on the timeline: a marker, the rail segment below it, an optional timestamp, and the projected body.
A terminal composable unit, not a fragment of the organism. It ships
its own token SET rules and defaults to the narrative raster, so
dropping one into a hand-rolled layout renders a complete, density-
correct item with no <cngx-timeline> above it. Inside the organism it
inherits [data-mode] and re-rasters itself.
Two independent state channels. status is where the event sits in
a history (done / active / upcoming / rejected) - editorial, set
by the consumer. state is whether this row's own data is currently
loading or failed. They compose: a done item whose state errored
paints as rejected and announces the failure, without losing the
editorial status once the retry succeeds.
What it announces. The marker and rail are aria-hidden, so colour is
never the only channel - the status reaches assistive tech through a
screen-reader-only line fed from CNGX_TIMELINE_CONFIG.labels.status, and
a failed row through a visible inline error. Both are read as the row's own
content, in DOM order, rather than through aria-describedby: the host is
a plain element with no role, so a description pointed at its own
descendants would resolve nowhere and duplicate the text for anyone
browsing it. Neither element is rendered while it has nothing to say.
Standalone
<cngx-timeline-item status="done">
<cngx-time cngxTimelineTime [date]="event.at" />
<p>Deployment finished</p>
</cngx-timeline-item>With its own async state
<cngx-timeline-item [state]="rowState" position="last">
<p>{{ rowState.data()?.summary }}</p>
</cngx-timeline-item>Metadata#
Host#
Relationships
Index#
HostBindings
Inputs#
unknownThe event this row stands for. Only ever read as the $implicit of an
app-wide *cngxTimelineMarkerTpl, so bind it when the app sets one and
that template needs the payload; leave it off otherwise.
TimelineConnectorPositionPosition in the run, forwarded to the rail. 'last' and 'only'
stop the rail at this item so the timeline does not trail off into
empty space. The organism sets it; standalone items default to
'middle'.
'middle'This row's own async state, for a body that loads, mutates or fails independently of the list around it.
Optional by construction and tolerant of a bare state attribute
(the empty string coerces to undefined), so the item never forces a
binding a consumer may not have.
undefined, { transform: (value) => (typeof value === 'string' ? undefined : value) }Where the event sits in the history. Drives the marker and rail colours and the screen-reader status line. Purely editorial - it carries no flow logic, because a timeline is a record and a guided process is a stepper.
HostBindings#
| Binding | Expression |
|---|---|
[attr.data-status] | status() |
[attr.data-failed] | failed() ? "" : null |
[attr.aria-busy] | ariaBusy() |
The --cngx-timeline-* custom-property family, shared by every
artifact in @cngx/common/timeline and re-used by the <cngx-timeline>
organism in @cngx/ui/timeline. A pure token surface - registrations
plus :root chains, no structural rules and no @scope.
It lives with the atoms rather than with the organism on purpose: a
<cngx-timeline-item> dropped into a hand-rolled layout has to render
complete and density-correct on its own. A token family that only
resolved under a Level-4 host would make the molecule a fragment
instead of a terminal unit.
Where the spacing tokens are, and why not here
This file carries no -gap / -padding registration. A spacing token
is registered in the same stylesheet that SETs it from --cngx-space-*,
at that component's own host:
--cngx-timeline-gap,--cngx-timeline-row-gap,--cngx-timeline-content-padding-timeline-item.component.css--cngx-timeline-group-gap- the organism's stylesheet
Registering them here and SETting them there would put a registration
one file away from its derivation, where the two can drift silently: a
registered initial-value defeats every use-site var(token, fallback), so a token that loses its SET rule freezes at the literal
and a nested [data-density] never reaches it. Keeping the pair
together makes that failure impossible to introduce by accident, and is
what the repo's density guard checks per file.
The sizes that remain here are affordances, not compactness, and are
deliberately outside the density scale. --cngx-timeline-rail-inset in
particular derives from --cngx-timeline-marker-size: the rail has to
meet the marker's centre, and a density swap that moved one without the
other would detach the rail from the dot. That derivation is applied at
the row host (timeline-item.component.css), the nearest common ancestor
of the marker and the connector - custom properties inherit downwards
only, so deriving it on either sibling would never reach the other.
Colour and dark mode
Every colour chains to a --cngx-color-* foundation token, which
already carries its own light and dark values. Dark mode therefore
needs no per-token redefinition here - overriding
--cngx-color-primary or the active colour scheme re-themes the
timeline for free. The literal in each chain's tail is the leaf
default for consumers running without the foundation stylesheet.
--cngx-timeline-connector-color->--cngx-color-border--cngx-timeline-surface->--cngx-color-surface--cngx-timeline-text-color->--cngx-color-text--cngx-timeline-muted-color->--cngx-color-text-muted--cngx-timeline-done-color->--cngx-color-success--cngx-timeline-active-color->--cngx-color-primary--cngx-timeline-rejected-color->--cngx-color-danger--cngx-timeline-upcoming-color->--cngx-color-text-muted
Default visuals for CngxTimelineItem - the two-column raster that puts
the marker and its rail in one track and the item's content in the other.
Rasters
narrative is the default and is declared on the host itself, so an
item outside any <cngx-timeline> still lays out completely. The
timestamp sits above the body:
marker time
rail bodyactivity is an override, keyed off the [data-mode] attribute the
organism sets on an ancestor. The timestamp trails the body on one line:
marker body time
rail body timePlacement
[data-row-side] says which side of the rail this row's body sits on -
start is v1 (rail leading, body trailing), end mirrors both tracks
and areas. The organism derives it per row from the loop index; a
standalone row opts in by putting the attribute on any wrapper, exactly
like [data-mode].
[data-placement='alternate'] centres the rail on symmetric 1fr auto 1fr tracks so the empty side can carry opposite content, and collapses
back to the start raster below 30rem of the cngx-timeline
container. The query lives here, next to the raster it overrides; the
organism only names the container.
Combinations are selected as :is([data-x] *) / :not([data-x] *) on
the row rather than as an ancestor chain: [data-mode] and
[data-placement] are both organism host attributes, so
[data-mode='...'] [data-placement='...'] matches nothing, while the row
is a descendant of the element carrying either one no matter how a
consumer distributes them. Every raster below is therefore settled by
specificity, not by source order.
Orientation
[data-orientation='horizontal'] transposes every raster below: the
marker and its rail share the axis row, the content stacks away from it,
and [data-row-side] selects which side of the axis rather than which
side of the column. The two axes are separate copies of the same ladder
rather than steps in one, so horizontal combines with alternate
instead of cancelling it.
activity has no horizontal variant: a scan-feed is a vertical shape.
Those rows render the narrative axis.
Rail style
[data-rail='continuous'] stretches every non-last segment across the
inter-row gap so the run reads as one line. Per segment rather than one
line behind the whole band, because each segment carries its own status
colour and a single line would flatten that channel.
Opposite content
[cngxTimelineOpposite] gets a third track, and only rows that project
into it do: an unconditional auto track would collapse to zero width
but still take a column-gap, shifting every row that predates the
slot.
The gate is :has(> [cngxTimelineOpposite]), and both halves matter.
The attribute, not the directive's host class, so it agrees with
<ng-content select="[cngxTimelineOpposite]"> by construction - a
consumer who projects the element without importing the directive still
gets the track. The child combinator, because projection lands the
node as a direct child of this host, and only a direct child can be a
grid item at all: a descendant match would hand a row a track that
nothing can occupy.
Density
The three spacing tokens are registered and SET here, in the one file
that uses them, so the registration cannot outlive its derivation. A
[data-density] swap on any ancestor re-scales all three.
--cngx-timeline-marker-size and --cngx-timeline-rail-inset are
deliberately absent: they are affordance geometry and live with the
atoms in timeline-tokens.css.
State
[data-failed] recolours the row's text and shows the inline error. The
error and the screen-reader status line are rendered only while they have
something to say, and are read as the row's own content - the host carries
no role for an aria-describedby to resolve against.
Index#
Layout
Typography
Variant / Done
Variant / Active
Variant / Upcoming
Variant / Rejected
Layout
<length>6pxDistance the rail is pulled back from the edge of the marker cell so
it meets the marker's centre. Affordance geometry, deliberately
outside the density scale - it tracks --cngx-timeline-marker-size
instead, and a density swap that moved it would detach the rail from
the dot.
<percentage>60%Size of a bare svg glyph projected into the marker, as a share of the
marker box. A percentage rather than a length so it tracks
--cngx-timeline-marker-size on its own: em would resolve against the
marker's pinned font-size, which is not the dot's size.
Photos are not covered by this - an img or picture fills the dot
edge to edge, because a cropped portrait wants no inset and a glyph does.
<length>192pxWidth of one row on the horizontal axis. Ignored in the vertical default, where a row takes the container's width.
An affordance, not compactness, so it is deliberately outside the density scale. It exists because a horizontal run has no sensible content-derived width: flex would size every card to its longest word and the axis would read as a ragged queue. Override per instance for denser or wider cards.
<length>3pxWidth of the halo ring drawn around a marker in the active status.
<length>8pxColumn gap between the marker track and the content.
SET from --cngx-space-sm.
<length>4pxRow gap inside one item, between the timestamp and the body.
SET from --cngx-space-xs.
<length>4pxPadding around an item's projected body.
SET from --cngx-space-xs.
Surface
<color>oklch(0.88 0.005 250)Colour of the connector rail in its default (unstated) status.
See: [[--cngx-color-border]]
<color>oklch(1 0 0)Background the marker is punched out of, so a coloured rail passing behind the dot does not show through it.
See: [[--cngx-color-surface]]
Typography
<color>oklch(1 0 0)Foreground colour of a glyph projected into the marker.
*0.8125remFont size for secondary text across the family - timestamps, inline errors, date headers, the refreshing tail. Registered here rather than with either consumer, because the row and the organism both read it and a registration in one is out of scope for the other.
<color>oklch(0.2 0.01 250)Primary text colour inside an item.
See: [[--cngx-color-text]]
<color>oklch(0.5 0.01 250)Secondary text colour - timestamps, group headers, the refreshing tail.
See: [[--cngx-color-text-muted]]
Variant / Done
<color>oklch(0.6 0.15 145)Marker and rail colour for the done status.
See: [[--cngx-color-success]]
Variant / Active
<color>oklch(0.66 0.19 50)Marker and rail colour for the active status.
See: [[--cngx-color-primary]]
Variant / Upcoming
<color>oklch(0.5 0.01 250)Marker and rail colour for the upcoming status. Pairs with a dashed
rail, so the state survives a colour-blind reader.
See: [[--cngx-color-text-muted]]
Variant / Rejected
<color>oklch(0.6 0.18 25)Marker and rail colour for the rejected status, and for an item
whose own [state] failed.
See: [[--cngx-color-danger]]
Motion
<time>1.6sPeriod of the busy pulse on a marker. Set to 0s to stop it without
touching the rest of the family.