Pattern // message archive
Class B // sealed interactive archive.
A reusable scaffold for a conversation archive: one self-contained .html per archive timeline, rendered from a source-neutral normalized model, sealed for audit, and readable offline with client-side search and year navigation.
on this page
What this pattern is
One delivered file per archive timeline. A timeline may consume one or more source threads or channels and merge them chronologically, preserving channel, source, and fidelity provenance per message. The source threads are provenance inputs, not separate output files — "thread" is not a synonym for the delivered object.
It is a consumption pattern, not a generator. design-system-ASK owns the presentation and semantic-role contract; each consumer owns source extraction, identity resolution, chronology, deduplication, media acquisition, rendering, and sealing. No source adapter, chat-database code, or extraction logic belongs in this repo.
Sealed and interactive are not in tension
The archive carries client-side search and year navigation and still freezes for audit. Interaction here navigates fixed content; it never fetches it. The delivered file has no network dependency, no sidecar, and no live upstream link — the same freeze-for-audit posture as the static Class B output-artifact, with an offline reading affordance on top. This is a Class B artifact, not a new class.
Normalized archive contract
The seam that stops every consumer reinventing the model is this source-neutral shape — not a shared extractor or renderer:
archive id · title · source_types[] · start_at · end_at · message_count · media_mode · provenance · caveats[] participants[] id // stable, source-neutral identity label is_author archive_role // persistent role token — NOT frequency-derived channels[] id · label · source_type · participant_ids[] messages[] id · channel_id · sender_id · sent_at · text · attachments[] · reactions[] · fidelity attachments[] id · kind · display_name · mime_type · state · source_pointer reactions[] type · actor_id · target_message_id · reacted_at // reacted_at optional where unavailable
Reaction fidelity // two valid source representations
reactions[] is a destination, not a mandate. Sources represent reactions in more than one way, and forcing every reaction-like record into that array can destroy source truth rather than normalize it.
Structured targeted reaction // manifest mode structured-targeted. Use reactions[] only where the source carries a deterministic target relationship — enough to populate target_message_id without speculative association.
Source-native reaction message // manifest mode message-records. Where the source itself exports a reaction as an ordinary message record, with its own sender and timestamp, and supplies no deterministic target relationship, preserve it 1:1 in messages[]:
- keep its source provenance and its place in the ordinary message count;
- do not infer a target from quoted text, proximity, matching prose, or any other heuristic evidence;
- do not delete the message merely to populate
reactions[]; - do not reduce the archive's message count to force a different model.
A source that genuinely contains both representations is mixed, and mixed is valid. A source carrying neither is none. What is not valid is collapsing one representation into the other silently.
The distinction the manifest must preserve is between a relationship the source recorded and one the consumer inferred. An inferred association presented as source truth is a fidelity failure even when the guess is good — and a heuristic that resolves most records still strands the remainder in a different representation from their siblings.
Attachment fidelity // four states, none silent
embedded bytes inlined into the sealed file referenced named by filename; bytes live beside or outside the artifact omitted-by-export the source export carried no media bytes-unavailable the message row exists; the bytes never reached the source device
The consumer selects a media-fidelity profile, and the delivered archive stays self-contained under it. Provenance must state the profile and the counts for every omitted or unavailable item. No attachment may disappear silently — an archive that drops media without saying so is a false record.
Bubble roles
author .ma-author right, plum, ALWAYS; never recolored, never reassigned participant-default .ma-default left, frosted — the two-party counterpart participant-a … -e .ma-role-* left, distinct fill + sender label (group mode)
Party mode is one seam on the root element, not a second template:
data-party-mode="two-party" author right · frosted other left · no legend · no sender labels data-party-mode="group" full-fill participant roles · legend · sender labels
Participant → role mapping is manifest-owned and persistent. It is never frequency-derived: a refresh must not recolor a person because their message count moved. Reassignment happens only under explicit operator intent. One owner change alters a role's value without reassigning anyone: see A successor to an AA-compliant archive, under One flavor below.
Participant color is identity, not state
Color answers which person. It never encodes urgency, evidence, status, approval, or emphasis, and no Spectral State or Evidence State hue is used. The muted foundation roles lead so ordinary archives read calmly; the softened derivations of the emphasis hues appear only once a conversation has enough additional participants to reach them.
The pattern-local values are not foundation tokens. They do not enter the general palette, are not available as general-purpose colors, and authorize no use outside this pattern. Where a role reuses an existing canonical token it does so by reference, without redefining it. The three raw emphasis accents are unchanged and remain sparing.
Supported capacity // seven participants
Supported capacity: seven distinct participants total, including the author. More than seven requires a future owner-approved overflow contract before rendering.
The seven roles are author · participant-default · participant-a … participant-e.
A larger distinct participant set is not supported by the current pattern version. A consumer that encounters more than seven must fail closed and surface that a new DS-owner decision is required. It must not:
cycle or reuse participant colors assign one role to multiple people silently collapse overflow participants into the frosted role invent participant-f / g / … roles drop participants
This is a bounded initial capacity, not a permanent claim that message archives can never exceed seven participants. Overflow behaviour is deferred until a real archive creates that pressure, at which point it is ratified against an actual case rather than designed speculatively.
One flavor // default-ASK
The template carries one presentation, named by one attribute on the root element:
data-flavor="default-ASK" the ASK-preferred visual default
| Role | Value |
|---|---|
| Roleauthor // light | Valuevar(--ask-ui-accent-1) #8b79a2 |
| Roleauthor // dark | Value#6e5c86 |
| Roleparticipant a | Valuevar(--ask-ui-accent-2) #ae87c2 |
| Roleparticipant b | Valuevar(--ask-surface) #bfb3d4 |
| Roleparticipant c | Value#c44fc4 |
| Roleparticipant d | Value#a56bd4 |
| Roleparticipant e | Value#569dbd |
| Roleparticipant ink | Valuevar(--fg-high-contrast) #201d26 |
default-ASK is the preferred treatment — not a defect, not a legacy state, not a version awaiting correction. Its lower-contrast metadata rendering is intentional and must not be pulled toward AA.
The AA-compliant alternative is retired. This version of the pattern no longer offers it: the template carries none of its values, and AA-compliant is not a supported data-flavor value. Do not set it, and do not record it for a new render from this version: the template would draw default-ASK under the AA-compliant name, and the hard-fail checklist fails an archive that does. Archives already sealed with AA-compliant keep their recorded version and bytes (see Snapshot / retention doctrine).
A successor to an AA-compliant archive. Re-rendering does not change an archive's template version. A successor rendered from the version its manifest already records keeps that version's rules and values, AA-compliant included. An AA-compliant archive moves to default-ASK only when its consumer deliberately adopts this version: it re-copies the template from this version, records the new commit and template hash, and renders the successor from them. That successor keeps every participant's role and records default-ASK and each role's new bound value in its manifest. The author's light fill and participants c and d change value, and the archive's metadata, page chrome, title, controls and focus indicator take the default-ASK treatment. The change comes from this pattern's retirement of AA-compliant, not from the archive, so it is not a reassignment under the persistence rule. It permits nothing else: any other recoloring, and any frequency-derived remap, still fails, and a reassignment still needs explicit operator intent.
The pattern makes no contrast-compliance claim beyond the accessibility items in its hard-fail checklist. An archive that needs one is validated by its consumer, on the final rendered archive with its content, media and Tier 3 overlay applied.
Foreground is inherited // no global rebind
The template adds no global --fg-* rebind. Every foreground is role-scoped and applied by class, so none can leak into inherited prose. #201d26 reaches this pattern only as the foundation's opt-in high-contrast role (--fg-high-contrast), as participant ink (--ma-ink): bubble text on the sanctioned colored fills, and the search-highlight text that rides those same fills. Page chrome, the archive title and the focus indicator stay on the ordinary foreground roles.
This is registered as one bounded pattern use in design-system-ASK, under High-contrast foreground — registered uses. The role does not become the default foreground anywhere.
Source data is untrusted // escaping contract
Message text, participant labels, attachment filenames, channel labels, reaction values, source pointers, and the precomputed data-s search string are all source-derived data emitted into HTML. A malformed or markup-bearing message must not be able to corrupt the archive or become executable content.
HTML-escape every source-derived text node AND attribute value. Never insert source payload through innerHTML. Treat message text, labels, filenames, channel names, reactions, source pointers, and precomputed search strings as untrusted data. If linkification is added, escape both label and URL, and allowlist protocols.
A rendered archive hard-fails if source content can become executable markup. The pattern's own search implementation reads dataset.s and uses classList / textContent only — it never assigns innerHTML, and a consumer's generator must hold the same line.
Every locally declared color value this template uses
Foundation tokens are consumed by reference and are not restated here. The values below are pattern-local — they exist only inside this pattern, are not foundation tokens, and authorize no use elsewhere. Nothing is hidden in the CSS.
| Value | Role |
|---|---|
Value#6e5c86 |
Roleauthor bubble fill, dark mode |
Value#569dbd |
Roleparticipant e fill |
Value#c44fc4 |
Roleparticipant c fill |
Value#a56bd4 |
Roleparticipant d fill |
Value#ffffff |
Roleauthor bubble foreground, light mode |
Value#f3ecfa |
Roleauthor bubble foreground, dark mode |
Value#e8def3 |
Rolefrosted default-bubble foreground, dark mode |
Valuergba(255,255,255,.62) / rgba(212,198,225,.10) |
Rolefrosted default-bubble fill, light / dark |
Valuergba(255,255,255,.24) / rgba(212,198,225,.08) |
Roleday-header + control chip fill, light / dark |
Valuergba(212,198,225,.58) / rgba(16,14,20,.72) |
Rolesticky-bar backdrop, light / dark |
The bubble foregrounds and translucent chrome fills are implementation-local surface values, not identity colors: they carry no participant meaning and were not part of the ratified identity ramp. They are recorded here so the template holds no undocumented color.
How to use it
- Copy
message-archive.template.htmlinto the consuming project and sync a local_dsa-tokens/mirror. For the editable template the mirror is a linked build input; for the delivered archive it is inlined and sealed away — see Seal-time font handling below, because inlining the token CSS alone does not seal the fonts. - Emit the message markup from your normalized model. Preserve
data-pid,data-role,data-channel, anddata-s. - Keep
data-flavor="default-ASK"and setdata-party-modeon the root element. - Replace every
[placeholder]. - Record the manifest — see
MANIFEST.md.example. - Run
hard-fail-checklist.mdbefore sealing.
Whitespace contract: .ma-m carries white-space: pre-wrap so message text keeps its own line breaks. A generator must emit the opening tag and the first child with nothing between them — any newline or indentation there is rendered as literal leading whitespace inside the bubble.
Seal-time font handling
The editable template links ./_dsa-tokens/colors_and_type.css as a build input. A delivered single-file archive must carry no sibling stylesheet and no font dependency — and inlining the token CSS alone does not achieve that: colors_and_type.css declares four @font-face blocks whose src values are relative (fonts/InterVariable.woff2 and siblings). Inlined unchanged, those URLs resolve against the archive's own location and break.
Two sealing modes are valid. Declare which one the artifact used in its manifest. The mechanical mode can be inspected in the delivered bytes; input-file provenance, recorded hashes, and whether a fidelity downgrade was deliberate cannot be established from those bytes alone.
embedded-data-uri // preferred, full fidelity
For a single delivered HTML:
- inline
colors_and_type.css; - preserve the
@font-facedeclarations; - replace every relative font URL with a base64
data:URI generated from the copied WOFF2 bytes; - preserve Inter and JetBrains Mono;
- record the input font files, their SHA-256, and the sealing mode in the manifest.
Bind each embedded payload to the copied mirror file and its recorded hash. Base64-encoding an arbitrary local font that merely shares a name is not this mode.
fallback-stacks // permitted, documented downgrade
Where the consumer deliberately chooses not to embed font bytes:
- remove the
@font-facedeclarations entirely; - retain the existing
--font-sans/--font-monofallback stacks so font roles survive; - leave no relative or external font URL anywhere in the delivered artifact;
- record in the manifest that canonical font files are not carried by the artifact and that exact typography is not guaranteed by it.
Be precise about what this mode does and does not determine. The retained stacks still begin with 'Inter' and 'JetBrains Mono', so rendering is environment-dependent: a viewing system with those families installed locally may resolve them; another falls through to the later generic entries. The invariant is not a prediction about which family appears — it is that the artifact carries no canonical font bytes and therefore cannot guarantee typography across environments.
This is a legitimate sealed-output option. It is not typography parity — do not present a fallback-mode archive as equivalent to one with embedded Inter + JetBrains Mono, and do not claim guaranteed typography on the strength of a machine that happens to have the fonts installed.
Prior sealed archives remain valid at their recorded SHA. This clarification records how to seal correctly going forward; it creates no retrofit obligation (see below).
Snapshot / retention doctrine
An archive is frozen at render time. Regenerating an archive produces a new artifact, not an edit of the old one. A new render may supersede an earlier one under the consumer's own status and naming convention, but every reviewed, accepted, or shared prior render remains retained — supersession is history, not deletion. This pattern prescribes no universal supersession filename marker. Prior sealed archives remain valid at their recorded SHA and are not retrofitted when this pattern changes.
Tier 3 overlay slot
Consumers supply their own Tier 3 identity, source-truth posture, content, and domain structure. Hosting this scaffold here does not make design-system-ASK the owner of downstream archive content.
What not to edit
Do not fork the template, duplicate the message markup or script, add a second flavor or a flavor-specific branch, rebind --fg-* globally, promote the pattern-local participant values into colors_and_type.css, or introduce a semantic state system on participant color.
Class A vs Class B
This is Class B (project-output artifacts). Class A is the system / architecture diagram family. The classes stay distinct; do not fuse. Both inherit Tier 1 + Tier 2, but they bind color differently because they apply it differently. Class A represents system / architecture structures; the static Class A patterns are explicitly structural and state-free, while some Class A surfaces opt into a semantic state or function role (Spectral State, Evidence State, Three Functions). This pattern uses color for participant identity — a third axis, and pattern-local.