Relationships
Related 13
Branches 3
History 1
c527aa0Created (draft)
Summary
A recurring shape has emerged across this round of specs. A primitive reads one value from text and emits one element, and a container or a layout does the rest:
colorreads a colour, andgridlays the cells out (SPEC-165 D1);fontreads a family and weights (SPEC-165 D8);amountreads money, andtallysums it (SPEC-162 D1–D2).
Each was designed on its own, and each would drift on its own: one emits data-value, another value; one formats by locale, another does not; one has a text alternative, another a bare glyph. SPEC-145 D28 already defined what a field's value element looks like to a theme. This spec makes that the contract for every value primitive, so each new one is a small instance of a known shape. It adds the one primitive the rune inventory shows is missing, date, and records two more candidates.
What the inventory found
A pass over every core and plugin rune (61 core, plus all nine plugins) against the composition plans found three value kinds that runes read by hand, each with its own parsing:
| Kind | Read today by | How |
|---|---|---|
| A date or time | timeline (date item field, plugins/business/src/tags/timeline.ts), itinerary (time, plugins/places/src/tags/itinerary.ts), changelog (VERSION_DATE_PATTERN), plan history and activity | per-rune patterns, no shared formatting |
| A place | map (coordinates and address, plugins/places/src/tags/map.ts), itinerary (location), event, organization | per-rune parsing |
| Included, not included, partial | comparison (task-list markers, drawn as ✓/✗, plugins/marketing/src/tags/comparison.ts) | a glyph whose aria-label sits on a span with no role, so it is not reliably announced |
The forms on the site include partial and written dates: ## 2023 - Founded, ## v1.2.0 - January 2025, and times such as ### 9:00 AM — Narita Airport.
timeline and itinerary already read their items with itemModel, so once BUG-038 is fixed they are close to compositions. What they lack is a primitive that turns the extracted text into a value.
Decisions
D1 — the contract
A value primitive reads one value from text or an attribute and emits a value element. Every value primitive meets all of the following.
- One element per value. The element fits the kind:
<time datetime>for a time,<data value>for a number,<address>for a place,<span>otherwise. A list of values (a colour scale, a font's weights,splitOnparts) is one element per value, as siblings. data-meta-typenames the kind, from the closedmetaTypevocabulary:temporalfor a date,quantityfor an amount, and so on. A kind with no fitting value adds one, as a vocabulary change governed by ADR-036. The additions this spec expects are listed in D3.- The raw value is always present, as data. It goes in
datetimeon<time>,valueon<data>, ordata-valueotherwise, beside the displayed text. A theme or a container never has to parse displayed text. This is D28's decision 9, generalised. - The displayed text is formatted and translated. Formatting follows the locale and ADR-043's theme-wide
formats, with the site able to override. Words in the display come through translation keys undercore.<primitive>.*(SPEC-145 D29). - A non-text display has a text alternative. A colour chip, marks, or a check glyph carries an accessible name (visually hidden text, or
role="img"witharia-label). A purely decorative part isaria-hidden. - Invalid input is reported, never guessed, and never trusted. Text the primitive cannot read reports
<primitive>-invalid, naming the text, and is rendered as written with no raw value. An author's value never reaches astyleattribute without passing the kind's syntax check (SPEC-165 found both design runes doing exactly that). - Context decides size, not the primitive. The same element is a chip inline and a fill in a block or media zone, as
coloris. A primitive declares no layout. - It is the same element a placed field emits. A
{% field %}whosemetaTypematches a kind renders through that kind's renderer, so a field value and a primitive value are interchangeable to a theme and to a container. Rendering happens at different times for a good reason: a primitive reads fixed content at stage 2, and a field is filled at stage 4 because its value can be written late (D28, "the bag is the record").
D2 — value kinds are implemented once and shared
Each kind is one module with three parts: a reader (text to raw value), a formatter (raw value and locale to display), and an element builder. The kinds are used by:
- the value primitive's transform, at stage 2;
- the engine's field rendering, at stage 4, which today has its own
transform: 'duration'path (formatDurationLocale); - containers, which read the raw value back from the element and never parse display text.
So tally reads amounts through the amount kind's raw value, a future map reads places through the place kind's coordinates, and design-context can read colours and fonts from rendered elements instead of from each rune's source syntax. That last point answers the open question in SPEC-165 that design-context breaks when a site redefines palette.
D3 — the members
| Primitive | Kind | data-meta-type | Element | Status |
|---|---|---|---|---|
color | colour | color (new) | <span> with --color, data-value, data-on | proposed, SPEC-165 D1 |
font | typeface | typeface (new) | <span> with data-value (the family) | proposed, SPEC-165 D8 |
amount | money, number | quantity | <data value> with data-currency, or data-low/data-high | proposed, SPEC-162 D1 |
date | date, time, range | temporal | <time datetime> | new, D4 |
| marks | a count out of a total | the field's own metaType | role="img" with marks inside | field shape, WORK-639 |
progress | a fraction | quantity | already has role="progressbar" and aria-value* | existing core rune; gains data-meta-type and data-value |
badge | an authored chip | tag by default | <span class="rf-badge"> | existing; already conforms |
D4 — date is a core value primitive
{% date value="2024-03-01" /%} {% date value="January 2025" /%} {% date value="9:00 AM" /%} {% date value="2024-03-01 – 2024-03-05" /%}
- It reads ISO 8601 dates and times, and partial dates (
2023,2024-03). It also reads written dates with English month names (January 2025,1 March 2025), times withAM/PMor 24-hour clock, and ranges joined by an en dash or a hyphen. - It emits
<time datetime="2025-01" data-meta-type="temporal">January 2025</time>. Thedatetimekeeps exactly the precision written, so a year stays a year. A range emits two<time>elements inside one wrapper withdata-range. - It formats through
formats.dateandformats.time(ADR-043) and the locale. - It does not guess ambiguous forms.
03/04/2024is March 4 in one locale and April 3 in another, so it reportsdate-invalidand renders as written. An author who wants it read writes it unambiguously. - Consumers:
timelineanditineraryplace it on their extracteddateandtimefields;changelogplaces it on the date it extracts from a version heading withheadingExtract; plan history and activity format their commit dates through the same kind.
D5 — two candidates, recorded and not yet decided
- A place primitive. It would read an address or coordinates and emit
<address>withdata-latanddata-lng. Amapwould then collect the places inside it, astallycollects amounts, and draw them as an ADR-044 tier-3 visual.event,itineraryandorganizationwould compose over it. It waits until a map composition is planned, because that is the consumer that defines what it must carry. - An inclusion value for
comparison: included, not included, or partial. It is a value, not a glyph, and needs a text alternative. The skeleton already has a checklist dimension for task lists (packages/skeleton/styles/dimensions/checklist.css), so this may be ametaTypewith sentiment rather than a new rune. Decided by SPEC-167 D4: ametaTypewith sentiment, read from task-list markers, with no rune of its own.
D6 — formats sets the display style of each value kind, theme-wide
ADR-043 decision 4 makes formatting style a theme-wide setting and asked for a spec. It is specified here, beside the kinds it is keyed on, because three things only this spec can settle decide whether it works: the list of kinds, the formatter it feeds, and where formatting runs.
Today the style is chosen in four places, none of them theme-wide:
| Where | What | Style |
|---|---|---|
formatDurationLocale (packages/transform/src/engine.ts) | fields with transform: 'duration' | fixed in code |
formatBudgetAmount (packages/runes/src/tags/budget.ts) | budget amounts | fixed Intl.NumberFormat currency style |
currency, date, number (packages/runes/src/functions.ts) | Markdoc functions | fixed options, and const LOCALE = 'en-US', so the site's locale is ignored |
LayoutStructureEntry.dateFormat (packages/transform/src/types.ts) | dates in layout chrome | Intl.DateTimeFormatOptions per layout entry |
Decision.
formatsmaps a kind to a named style. Only kinds with a display choice take one:Kind Styles Default dateshort,medium,long,isomediumtimeshort,longshortdurationshort("15m"),long("15 minutes")shortnumberstandard,compact("1.2M")standardcurrencysymbol("€40"),code("EUR 40"),name("40 euros")symbolEach style is a fixed set of
Intloptions, defined once in the kind's formatter. A style is a name, never a pattern string, so a theme cannot write a format that breaks in another locale.color,typeface, marks,progressandbadgehave nothing to format and take no key.Style and locale are separate. The style picks the options; the site's locale (SPEC-035) picks the language. "medium" is "Oct 12, 2026" in English and "12 okt. 2026" in Swedish.
Precedence: the site's
formats, then the theme's, then the defaults above, resolved per key. A theme declaresformatsin its config; a site overrides inrefrakt.config.json, as it overrides tokens. An unknown kind or style is reported when the config loads, naming it.Display text is formatted at stage 4, for primitives and fields alike. A field is formatted by the engine, which has the theme's config and the locale. A primitive's transform runs at stage 2, which has neither:
functions.tscannot even see the site's locale. If a primitive formatted its own display text, the same value would follow the theme as a field and ignore it as a primitive, breaking D1.8. So a primitive emits its element with the raw value and the author's text as a fallback, and the engine replaces the display text through the kind's formatter, exactly as it does for a field. Without the engine (a renderer that skips the identity transform), the author's text shows, which is correct and readable.An author's explicit format is not overridden. A primitive written with a
formatattribute keeps it;formatssets the default, as D34 of SPEC-145 does for axes. Whether primitives takeformatat all is left to each primitive's spec.What it replaces.
formatDurationLocale's fixed style becomes thedurationformatter.- Budget's formatter becomes the
currencyformatter, which SPEC-162'samountuses. - The Markdoc functions call the same formatters, with the site's locale and
formats. Their explicit arguments still win. - A layout entry's
dateFormatstays as an explicit value for that entry, and without one the entry usesformats.date.
It ships with the first kind module. There is nothing to configure before a formatter reads it.
amountanddateare the first.
Not in scope
mediatextand floated images. They were discussed beside this, and are held for a separate spec.- Units other than currency on
amount(SPEC-162's open question).
Open questions
- Written dates in other languages. D4 reads English month names. Reading
januari 2025on a Swedish site means reading in the site's locale. The month names are available throughIntl, but parsing them is not, so this needs a small table per locale or is left to authors writing ISO dates. typefaceandcolorasmetaTypevalues. They are proposed in D3 so the design primitives meet D1.2, but they widen a vocabulary every theme is expected to cover (ADR-043's coverage check). The alternative is ametaTypeofswatchor nometaTypeat all for design-only primitives.
Acceptance Criteria
- The value-element contract (D1) is documented in the theme-authoring and rune-authoring guides, with the attribute list a theme reads
- Value kinds are shared modules (reader, formatter, element builder) used by both value primitives and the engine's field rendering;
transform: 'duration'goes through the shared temporal kind color,fontandamountare built on the shared kinds and meet every point of D1, asserted by one contract test that runs against every value primitive{% date %}exists as a core rune: reads ISO, partial and written English dates, times and ranges; keeps the written precision indatetime; formats by locale andformats; reportsdate-invalidfor ambiguous formstimeline,itineraryandchangelogread their dates through the temporal kind, asserted against their current fixtures with no change in displayed textprogressgainsdata-meta-typeanddata-valueand passes the contract test- A container reads values only through raw values:
tallyfrom amounts, anddesign-contextfrom renderedcolorandfontelements - The new
metaTypevalues are added to the vocabulary and to ADR-043's coverage check formatsmapsdate,time,duration,numberandcurrencyto named styles with the defaults in D6; an unknown kind or style is reported at config load (D6)- A site's
formatsoverrides the theme's per key, and the locale and the style combine as D6.2 states (D6) - A value placed as a field and the same value written as a primitive render identical display text under any
formats, asserted for a date and an amount (D6.4) - The duration, budget-currency and Markdoc-function formatting paths go through the shared formatters, and
functions.tsuses the site's locale (D6.6)
References
- SPEC-145 D28: the value element a placed field emits, which this spec generalises.
- SPEC-162:
amountandtally. - SPEC-165:
colorandfont; the open question ondesign-context. - ADR-043:
formats, and the theme's vocabulary coverage. - ADR-044: tier 3, where a map's drawing belongs.
- WORK-639: marks as a field shape.
- BUG-038: item extraction from YAML, which
timelineanditinerarydepend on.