busy-office-ui
Menu

Timeline & audit trail

The approval-workflow pair: a status timeline (done / current / pending / rejected) and a chronological audit trail. Two-channel state throughout — glyphs are aria-hidden, state reaches AT via visually-hidden text and aria-current. Composed example: the approval-workflow pattern. Not for a flow the user is walking right now — a timeline reports history and status; use Stepper when the point is which step they are on and what comes next.

Timeline states

  1. Completed: Submitted
    R. Vance ·
  2. Cost-center approval
    waiting · 2 days
  3. Rejected: Finance release
    budget hold
  4. Not started: Archive
<ol class="bo-timeline" role="list">
  <li class="bo-timeline__step" data-state="done">
    <span class="bo-timeline__marker" aria-hidden="true">✓</span>
    <div>
      <div class="bo-timeline__title"><span class="bo-visually-hidden">Completed: </span>Submitted</div>
      <div class="bo-timeline__meta">R. Vance · <time datetime="2026-08-10T09:14">2026-08-10 09:14</time></div>
    </div>
  </li>
  <li class="bo-timeline__step" data-state="current" aria-current="step">
    <span class="bo-timeline__marker" aria-hidden="true">●</span>
    <div><div class="bo-timeline__title">Cost-center approval</div></div>
  </li>
  <li class="bo-timeline__step" data-state="rejected">
    <span class="bo-timeline__marker" aria-hidden="true">✕</span>
    <div><div class="bo-timeline__title"><span class="bo-visually-hidden">Rejected: </span>Finance release</div></div>
  </li>
  <li class="bo-timeline__step" data-state="pending">
    <span class="bo-timeline__marker" aria-hidden="true">4</span>
    <div><div class="bo-timeline__title"><span class="bo-visually-hidden">Not started: </span>Archive</div></div>
  </li>
</ol>
<!-- Two-channel per state: the marker glyph + tint is one channel; the
     visually-hidden "Completed:/Rejected:/Not started:" prefix (and
     aria-current on the active step) is the announced one. -->

Audit trail

  1. R. Vance submitted PO-88213
  2. J. Kim approved "Within Q3 budget."

Rejected → resubmitted — the real approval loop

  1. M. Osei rejected "Wrong cost center — should be CC-2205, not CC-4021."
  2. R. Vance updated cost center and resubmitted
  3. M. Osei approved

The loop every ERP actually runs: rejection with a reason in __detail, a fix, a resubmit. In the paired timeline the step flips data-state="rejected" → back to current on resubmit — the audit trail keeps the full history while the timeline shows only the present.

--discussion — a comment thread

A discussion is the same list carrying a different kind of entry, so it is a setting, not a second component. An audit trail's __detail is small print about a record; a comment is the content itself. Composing a thread without this modifier rendered the message at 12px underneath a 13px byline — the metadata outranking the thing it introduces — so --discussion returns the detail to body size and primary ink. Everything else is already shared: the time column, Byline for the author, .bo-composer below to write one.

  1. Vendor confirmed a 3-week lead time on the coupling. Asked whether we split the line and take 40 now.

  2. Split it — budget is committed either way and the line stoppage costs more than a second delivery.

View code
<!-- Same list, different kind of entry. --discussion says the
     detail IS the content, so it keeps body size and primary ink. -->
<ol class="bo-audit bo-audit--discussion" role="list">
  <li class="bo-audit__entry">
    <time class="bo-audit__time" datetime="2026-08-20T09:12">08-20 09:12</time>
    <div class="bo-audit__detail">
      <span class="bo-byline"><strong>M. Osei</strong> · Requester</span>
      <div class="bo-prose"><p>Vendor confirmed a 3-week lead time on the coupling.
      Asked whether we split the line and take 40 now.</p></div>
    </div>
  </li>
  <li class="bo-audit__entry">
    <time class="bo-audit__time" datetime="2026-08-20T11:40">08-20 11:40</time>
    <div class="bo-audit__detail">
      <span class="bo-byline"><strong>J. Kim</strong> · Cost-centre manager</span>
      <div class="bo-prose"><p>Split it — budget is committed either way and the
      line stoppage costs more than a second delivery.</p></div>
    </div>
  </li>
</ol>

Use it for approval notes, chasing history and handover threads. It is a flat thread on purpose — no nesting, no replies-to-replies, no left/right bubbles. Those are messaging-app shapes; an ERP discussion hangs off a record and reads in one order.

Collaborating — who can see this, and what is settled

A thread people act on needs two things a thread people only read does not. data-visibility="external" marks a comment that leaves the building, and data-state="resolved" marks a question that is settled.

  1. Resolved

    Split raised as PO-88240 for the balance.

  2. External · sent to vendor

    Please confirm the revised delivery date for the 40-unit first drop.

External · goes to the vendor
View code
<!-- Internal is the default: no attribute, no edge. -->
<ol class="bo-audit bo-audit--discussion" role="list">
  <li class="bo-audit__entry" data-state="resolved">
    <time class="bo-audit__time" datetime="2026-08-21T08:05">08-21 08:05</time>
    <div class="bo-audit__detail">
      <span class="bo-byline"><strong>M. Osei</strong> · Requester</span>
      <span class="bo-badge">Resolved</span>
      <div class="bo-prose"><p>Split raised as PO-88240 for the balance.</p></div>
    </div>
  </li>
  <li class="bo-audit__entry" data-visibility="external">
    <time class="bo-audit__time" datetime="2026-08-21T09:30">08-21 09:30</time>
    <div class="bo-audit__detail">
      <span class="bo-byline"><strong>M. Osei</strong> · Requester</span>
      <span class="bo-badge bo-badge--warning">External · sent to vendor</span>
      <div class="bo-prose"><p>Please confirm the revised delivery date for the
      40-unit first drop.</p></div>
    </div>
  </li>
</ol>

<!-- The COMPOSER carries it too, which is the point: you can see where the
     words go while you are writing them, not after. -->
<form class="bo-composer" data-visibility="external">
  <span class="bo-avatar bo-byline__avatar" aria-hidden="true">MO</span>
  <div class="bo-composer__body">
    <span class="bo-badge bo-badge--warning">External · goes to the vendor</span>
    <label class="bo-visually-hidden" for="c-ext">Message to vendor</label>
    <textarea class="bo-input" id="c-ext" rows="2"></textarea>
    <div class="bo-composer__actions">
      <button class="bo-btn bo-btn--sm" type="button">Send to vendor</button>
    </div>
  </div>
</form>

Internal is the default — no attribute, no edge. Getting the hook wrong then means a message that should have gone out stayed in, which someone chases; the reverse is unrecoverable, which is why the dangerous direction is the one you have to type.

The edge is geometry, and the badge is the other channel. The stripe survives forced-colors and a monochrome print, but a stripe alone tells a screen-reader user nothing — so name the audience in a badge, as above. That is your text to write: "External \u00b7 sent to vendor" is useful, "External" on its own is not.

Why the composer carries it as well. Every ERP we measured against puts this on a small tab pair above an otherwise identical box — so the surface you are looking at while you type is the one thing that never says where the words are going, and a mis-click is a broadcast. Marking the composer means the answer is in front of you at the moment it matters. Not a permission check: this is a signal, and the server still decides who may post what.

Resolved returns the comment to the audit trail's own treatment, because that is what it now is — history. Only a --discussion resolves; an audit entry is immutable by definition. A visible "Resolved" marker is required, not optional — measured under forced-colors: active, the UA repaints both inks to the same black, so the ink difference is the whole signal in normal use and no signal there. The external edge is geometry and survives; this state has no non-colour channel of its own, so the marker is it.

.bo-composer — writing a new entry

How a new .bo-audit entry gets written, not just read. Layout only: an avatar (.bo-byline__avatar, composed — not duplicated) + a plain .bo-input textarea + an actions row.

<form class="bo-composer">
  <span class="bo-avatar bo-byline__avatar" aria-hidden="true">RV</span>
  <div class="bo-composer__body">
    <textarea class="bo-input" rows="2"
              placeholder="Add a comment…" aria-label="Comment"></textarea>
    <div class="bo-composer__actions">
      <button class="bo-btn bo-btn--sm bo-btn--secondary" type="reset">Cancel</button>
      <button class="bo-btn bo-btn--sm" type="submit">Comment</button>
    </div>
  </div>
</form>
<!-- Submit posts a new .bo-audit__entry — your form handler's job, server or JS. -->

Markup — audit entry

<ol class="bo-audit" role="list">
  <li class="bo-audit__entry">
    <time class="bo-audit__time" datetime="2026-08-10T15:02">08-10 15:02</time>
    <span><span class="bo-audit__actor">J. Kim</span> approved</span>
    <span class="bo-audit__detail">"Within Q3 budget."</span>
  </li>
</ol>
<!-- Grid-aligned: the time column self-sizes; details land in the content
     column automatically — no width tuning when formats change. -->

Quick reference generated

ClassRole
.bo-audit block (root)
.bo-audit--discussion variant: discussion
.bo-audit__actor part: actor
.bo-audit__detail part: detail
.bo-audit__entry part: entry
.bo-audit__time part: time
.bo-composer block (root)
.bo-composer__actions part: actions
.bo-composer__body part: body
.bo-timeline block (root)
.bo-timeline__marker part: marker
.bo-timeline__meta part: meta
.bo-timeline__step part: step
.bo-timeline__title part: title

State attributes: data-statedata-visibility

API generated from CSS

Blocks bo-auditbo-composerbo-timeline
Parts bo-audit__actorbo-audit__detailbo-audit__entrybo-audit__timebo-composer__actionsbo-composer__bodybo-timeline__markerbo-timeline__metabo-timeline__stepbo-timeline__title
Variants bo-audit--discussion
data-* hooks data-state="current|done|pending|rejected|resolved"data-visibility="external"
Component tokens --bo-timeline-marker-bg--bo-timeline-marker-fg
JS required None — CSS-only.
  • State text pattern is mandatory: Completed: / Rejected: / Not started: visually-hidden in the title — done vs rejected must be distinguishable non-visually.
  • Use <time datetime> for audit timestamps.
  • Markers are fixed-size status graphics (not controls) — they deliberately ignore density.
  • .bo-composer is layout only — the textarea is a plain .bo-input, the avatar is .bo-byline__avatar; submit handling (posting a new .bo-audit__entry) is your form handler.
Contrast — 29 AA pairs this component uses (generated, CI-gated)
TextonLightDark
text-primary bg-surface 17.74 16.15
text-primary bg-canvas 16.98 18.08
text-secondary bg-surface 10.31 11.46
text-secondary bg-muted 9.37 9.75
text-muted bg-surface 7.56 6.65
text-muted bg-muted 6.87 5.66
bg-surface text-primary 17.74 16.15
accent-text accent-subtle 7.27 8.38
danger-text danger-subtle 5.91 8.06
success-text success-subtle 4.79 10.17
danger-text bg-surface 6.47 8.89
danger-text bg-canvas 6.19 9.96
success-text bg-surface 5.02 12.02
success-text bg-canvas 4.8 13.46
text-primary bg-hover 16.12 13.75
text-primary bg-selected 17.01 11.86
text-secondary bg-selected 9.88 8.41
accent-text bg-selected 7.27 8.38
text-primary bg-surface-raised 17.74 14.51
text-secondary bg-canvas 9.86 12.82
accent accent-subtle 5.25 6.66
border-control bg-muted 4.39 5.66
accent bg-muted 4.97 7.72
warning-strong bg-muted 4.56 6.69
warning-strong bg-surface 5.02 7.86
danger bg-muted 4.39 5.19
text-primary bg-muted 16.12 13.75
success-text bg-muted 4.56 10.23
danger-text bg-muted 5.88 7.57

Maturity generated

Introduced
v0.1.0
CSS floor
Chrome/Edge 99 · Firefox 97 · Safari 15.4
Alignment scored
2026-08-21
Assistive-tech evidence
None recorded

Pin this component's first release with npm i @busy-office/ui@0.1.0. The floor is derived from this component's stylesheet alone — set by @layer — so it sits at or below the whole framework's Chrome/Edge 119 · Firefox 129 · Safari 17.5. No assistive-technology runtime result is recorded for any component: it needs a human listening to VoiceOver and NVDA on owner hardware; no gate in this repo can simulate announcement behaviour. What automation covers instead is test:axe across every page at 1440 and 390, and check:forced-colors — see accessibility.

Design-system alignment generated

100% (15/15) — how closely this component follows the design language. Each rule is scored 0-3 against a checkable citation; rules that don't apply leave the denominator. Scored 2026-08-21.

Rule Score Evidence
Typography 3 / 3 title/meta from weight and size tokens; audit timestamps tabular
Colour 3 / 3 the one raw hex is inside @media print; state is two-channel by construction — the marker glyph (✓ ✕ ●) is REAL MARKUP, so it survives forced-colors even without a rule
Spacing 3 / 3 marker size and all three connector numbers now state that they are DERIVED from the marker, not chosen (94.6b); the audit grid uses max-content so there is no width token to drift
Interaction n/a an <ol> of workflow states; approving happens elsewhere
Content 3 / 3 the opener names the wrong-choice context and the alternative: a timeline reports history, so use Stepper when the point is which step the user is on (94.8)
Fit 3 / 3 status timeline plus audit trail, the two read-only halves of an approval screen
↑↓ move Enter open Esc close