design

package
v5.0.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 3, 2026 License: MIT Imports: 5 Imported by: 0

Documentation

Overview

Package design is Atlas Commerce OS's design system, authored entirely in Go on top of the repo's typed-CSS package (github.com/monstercameron/GoWebComponents/v5/css). There is no Tailwind build step, no .css file, and no CDN: every rule in the running app is a Go value that the css package folds into a hashed class and emits through its Sink — the in-memory buffer on native (harvested into the SSR <style> block) and a managed <style> element on wasm. Because both lanes go through the same authoring surface, the design system is written exactly once.

Read this file first if you are learning v5

Atlas is the reference example, so this package is deliberately written to be read. The rationale comments are the point: they say what failure each rule prevents. If you are copying this into a new app, copy the *shape* — tokens, then base, then a small closed set of primitives — not the hex values.

The visual direction: "Dock Manifest"

Atlas is warehouse-aware commerce. Its real subject is whether a thing can be where you need it, when you need it, and its vocabulary is hub codes, freight lanes, promise dates, discrepancies, closeouts and SKUs. Freight paperwork — dock tags, manifests, lane placards — is therefore the native visual language: light industrial paper and ink, one heavy signature device (the lane placard), and everything else quiet.

What this replaced, and why the replacement is shaped the way it is:

  • The old look was near-black rounded boxes nested four deep, all at the same visual weight, with three unrelated accent hues used ornamentally (an orange eyebrow, a teal badge, a yellow button). Nothing was more important than anything else, so nothing read as important at all.
  • So: exactly one flat surface primitive and NO nested-card primitive (see Surface); separation comes from a hairline rule or from space.
  • So: the three saturated hues are status semantics only, and the API names them so ornamental use reads wrong at the call site (see StatusException, PrimaryActionOnly).
  • So: hierarchy is carried by type role and weight, not by another box.

The three type roles ARE the information architecture

Display, Prose and Data are not three fonts, they are three claims about what a string IS:

  • Data (mono) — a machine fact: SKU, hub code, ETA, promise date, quantity, price, status code, any id. Mono is not decoration here. It gives fixed advance width, so a column of SKUs or quantities aligns without a table, and it makes a transposed digit visible. Combined with tabular figures it means "1,041" under "1,042" differs in exactly one glyph slot.
  • Prose (proportional) — a sentence. Anything a human wrote to be read.
  • Display (condensed, heavy, uppercase) — a name for a region of the page: page titles, section titles, eyebrows, button labels, table headers.

The rule to hold: every machine fact is Data and every sentence is Prose. When those two get mixed, a table stops being scannable and a paragraph starts looking like a log file. There is no fourth role, and there is no "just make it bold" — reach for a role and a step from TypeStep.

How to use it

Call Install exactly once at startup, in both lanes, before the first render. It emits the :root token block, the dark-mode override, the framework reset and the accessibility floor. Everything else is a *bundle* — a []css.Rule that you fold into a class:

design.Install()

html.Div(html.Props{Class: design.Class(design.Surface())},
    html.Div(html.Props{Class: design.Class(design.Eyebrow())}, "RECEIVING"),
    html.H2(html.Props{Class: design.Class(design.Display(design.StepSubhead))}, "Open discrepancies"),
    html.Span(html.Props{Class: design.Class(design.Data(design.StepFine))}, "SKU-40192"),
)

Bundles compose left-to-right, and later bundles win on conflicting properties, because Class concatenates them into one rule-set and the css package resolves a repeated property last-write-wins within a scope:

design.Class(design.Prose(design.StepBase), design.Data(design.StepBase)) // ends up mono

That is why primitives return []css.Rule rather than a pre-folded class name: two class names on one element would resolve by stylesheet emission order, which is not something a caller can see or reason about. One folded class has no such ambiguity.

Why bundles are package-level vars

Class is called inside render loops. css.New memoizes on the canonical serialization of the rule-set, so folding a bundle a second time is a map lookup — but building the rule slice again is not free. The bundles are therefore built once at package init and returned by value, and every one is length-clipped (see clip) so a caller who writes append(design.Surface(), x) gets a copy instead of silently clobbering the shared bundle for the whole process. Treat every returned slice as read-only anyway.

Emission stays lazy: nothing is written to the Sink until a bundle is folded (or Install runs). That is what makes the package survive css.Reset() in tests — an init-time fold would hand out class names whose CSS had been thrown away.

Class still canonicalizes on every call, which is a few microseconds. For a call site in a genuinely hot loop — a table cell rendered a thousand times — hoist the folded STRING at the caller, which turns the per-row cost into a field read:

var skuCellClass = design.Class(design.NumericCell())   // in the calling package

Three cascade traps this package works around

All three cost real debugging time, so they are called out where they occur:

  1. Within one folded class, blocks are emitted in *sorted selector order*, not source order (see canonicalize in css/rule.go). You cannot break a tie by writing one rule after another. Table wins its zebra-vs-hover tie on specificity instead.
  2. Declarations within one block are also sorted, by property name. That makes a shorthand-then-longhand pair (border, then border-top in Divider) work reliably, and it makes border-bottom-beats-border-top impossible in one block.
  3. A descendant rule inside a bundle (& tbody td, 0-1-2) outranks a bare class applied to that descendant (.numeric-cell, 0-1-0). The cell modifiers NumericCell, ProseCell and CellMeta therefore emit through a specificity-doubling variant (&& -> .c-x.c-x, 0-2-0), pinned by TestCellModifiersOutrankTable.

Two upstream bugs this package found

Both were found by asserting on emitted output rather than by reading the API, which is the argument for the tests in this package being shaped the way they are:

  • css.Transition with a multi-property css.TransitionProperty (PropColors) emits an invalid shorthand where only the last property gets the timing. See withMotion.
  • html.Props{TabIndex: 0} renders no attribute at all, so a scroll region declared that way is unreachable by keyboard. See TableScroll.

The catalog is a manifest, not a card grid

The absence of a Card (below) needs a positive answer for the one surface that is neither a queue nor a document: a product catalog. Without one, /shop gets built out of Surface + Cluster and comes out as two dozen bordered boxes at identical weight — the disease, repainted. Catalog is that answer: a list of full-width manifest lines on a shared CSS grid, with a labelled column header above it, a thumbnail column, and availability promoted to its own column because availability is the product's thesis.

It is a list rather than a <table> on purpose (a buyer reads across a row, an operator reads down a column, and only a list can restack its tracks on a phone), and it is full-width rows rather than a grid because four products in a three-column grid is an orphan row with two holes and no fix. The whole argument is in catalog.go; read it before changing the shape, because the shape is the argument.

Theming, and the three themes

Every color, font stack and the rail width are CSS custom properties emitted into :root, so a theme overrides values without regenerating a single class. The dark theme is nothing but a second :root block inside a prefers-color-scheme media query that rewrites the same names. No primitive knows a theme exists.

PRINT IS THE THIRD :ROOT BLOCK. Atlas's whole metaphor is printed paperwork, so a receipt, a transfer and a purchase order print as the documents they imitate: the rail leaves, chrome leaves, tokens become ink on white, a table header repeats on every page and a row does not shear across the fold. It costs one more token block plus a handful of per-primitive overrides precisely BECAUSE theming is token-shaped. See print.go — including the trap that browsers do not print background-color, which turns every filled element (the exception chip, the placard) invisible unless it is re-expressed as an outline.

Contrast is measured, not eyeballed

Token POLARITY between themes is not legibility. contrast_test.go computes WCAG 2.1 ratios in Go for every (foreground, background) pair the primitives actually paint, in all three themes, and it found three real failures that review had not: a 1.36:1 dark primary button, a 1.50:1 input border, and a 2.11:1 focus ring inside the light placard. Each was fixed with a token or a scoped rule, never with a lowered threshold. Adding a color token now fails the build until it is measured.

This is also why the token names are semantic rather than literal: --atlas-ink means "the color you write with", not "black". In dark mode ink becomes light and paper becomes dark, and every primitive stays correct without being touched.

Do NOT reintroduce what the old example-shell.css did: a hard `color-scheme: dark` plus `!important` gradients. That locked the app to a dark rendering while the app itself reported THEME: light, because the override could not be beaten by anything the app emitted. Install declares `color-scheme: light dark` (both are supported, follow the user) and this package emits no !important anywhere.

What is deliberately absent

A design system is defined as much by what it refuses to provide. There is no Card, Panel, SurfaceInner or nested-surface primitive; no card GRID (see Catalog); no free-form color argument anywhere; no fourth button; no shadow scale; no radius above 2px; no linear gradient; no webfont. Each absence is documented at the place a caller would go looking for it.

The two gradients that do exist are both perforations — the placard's tear edge and the rail's punched trailing edge — and both are radial-gradients tiling a single disc. TestNoThemeLockRegression matches every gradient function by name so a third one has to be argued for.

The frame participates

One more absence was closed rather than kept: the console rail used to be a generic left rail, which meant the largest persistent element on every internal screen carried none of the identity. It now has a perforated trailing edge and a column of mono route codes, so the frame reads as a placard column — see the section header in shell.go, including what came OFF to pay for it (the rail's rounded pill items).

Index

Constants

View Source
const (
	// Structure. These five carry the entire layout; a page built from only these
	// is already recognizably Atlas.
	TokenPaper     = "--atlas-paper"      // cool manila stock — the page itself
	TokenPaperSunk = "--atlas-paper-sunk" // recessed surfaces and table stripes
	TokenInk       = "--atlas-ink"        // primary text and heavy rules
	TokenGraphite  = "--atlas-graphite"   // secondary text, meta, labels
	TokenHairline  = "--atlas-hairline"   // 1px paperwork rules (decorative separation)
	TokenEdge      = "--atlas-edge"       // 1px CONTROL boundaries (must hold 3:1)

	// Structural accent. Lane blue is the placard/structure hue: links, focus
	// rings, "in the system" states. It is allowed to be structural because it is
	// low-chroma enough to sit under text without competing with it.
	TokenLane = "--atlas-lane"

	// Semantics. These three are NOT part of the palette in the decorative sense —
	// see the accessor comments. Overriding them in a theme is fine; using them for
	// ornament is the failure this package exists to prevent.
	TokenSignal = "--atlas-signal" // safety yellow — the one primary action
	TokenOxide  = "--atlas-oxide"  // exceptions, discrepancies, flags
	TokenVerify = "--atlas-verify" // approved, confirmed, closed clean

	// Derived. The lane placard needs a bar/label pair that is NOT simply ink on
	// paper in dark mode (a full-brightness bar in a dark UI is a flashbang), so it
	// gets its own two tokens and the dark theme picks a deep lane blue instead.
	// Add a derived token when a component needs a decision the palette cannot
	// make; do not smuggle the decision into the component as a literal.
	TokenPlacardBg = "--atlas-placard-bg"
	TokenPlacardFg = "--atlas-placard-fg"

	// TokenSignalFg is the second derived pair, and it exists because of a real
	// contrast failure that TestTokenContrastHoldsAAInBothThemes caught:
	// ButtonPrimary was ink-on-signal, and `ink` inverts between themes while
	// `signal` does not — safety yellow is a light color in BOTH themes. So the
	// dark theme rendered a #E8E7E1 label on a #F0C244 button at 1.36:1, which is
	// unreadable, while the light theme was fine at 9.18:1. See buttonPrimaryBundle.
	//
	// The lesson generalizes: any pair where ONE side inverts and the other does not
	// needs its own foreground token. Polarity inversion is not a safe default.
	TokenSignalFg = "--atlas-signal-fg"

	// Type. Font stacks are tokens so a future webfont can be dropped in via one
	// @font-face + one :root override, with no primitive edited. There are no
	// webfonts today: this repo ships zero @font-face rules and no CDN is
	// permitted, so all three stacks are system fonts.
	TokenFontDisplay = "--atlas-font-display"
	TokenFontBody    = "--atlas-font-body"
	TokenFontData    = "--atlas-font-data"

	// Layout. The console rail width is a token because the rail and the content
	// column must agree on it, and because a density preference is a plausible
	// future theme axis.
	TokenRailWidth = "--atlas-rail-width"
)

The token names are exported so a theme can override them by name without importing anything else from this package:

css.Global(`[data-theme="dusk"]`, css.Raw(design.TokenPaper, "#1A1D22"))

Names are semantic, not literal: TokenInk is "the color you write with", not "black". That is what lets the dark theme invert paper and ink while every primitive keeps working untouched.

View Source
const (
	Space1 css.Length = "0.25rem" // 4px  — icon/label gaps, chip padding
	Space2 css.Length = "0.5rem"  // 8px  — dense table cell padding
	Space3 css.Length = "0.75rem" // 12px — control padding, rail item padding
	Space4 css.Length = "1rem"    // 16px — surface padding, default stack gap
	Space5 css.Length = "1.5rem"  // 24px — between page sections
	Space6 css.Length = "2rem"    // 32px — page gutters
	Space7 css.Length = "3rem"    // 48px — between major page regions
)

The spacing scale. Seven steps, roughly 1.5x, expressed in rem so a user's browser font-size scales the layout with the text — a px gutter next to rem text is how a page breaks at 200% zoom.

These are css.Length constants rather than a closed enum type, so they drop straight into any css constructor: css.Padding(design.Space4). The cost of that ergonomics is that css.Gap(css.Px(13)) still compiles — the scale has teeth in review, not in the type checker. Passing an off-scale length also mints a new hashed class, so an unbounded set of ad-hoc values grows the registry.

View Source
const (
	// HairlineWidth is the 1px rule. It is a constant rather than a token because a
	// design where separators are 3px is a different design.
	HairlineWidth css.Length = "1px"

	// ManifestRuleWidth is the heavy rule: under a table header, above a total. It
	// is the one place ink gets thick, and it is what makes a table read as a
	// printed manifest rather than as a grid of divs.
	ManifestRuleWidth css.Length = "2px"

	// RadiusNone and RadiusTag are the ONLY two radii, and RadiusTag is 2px.
	//
	// This is a direct response to the failure being fixed: rounded dark boxes
	// nested four deep, every one at the same weight. A 2px radius reads as cut
	// paper. A 20px radius reads as a card, cards invite nesting, and nesting at
	// uniform weight destroys hierarchy. There is no radius scale to reach for.
	RadiusNone css.Length = "0"
	RadiusTag  css.Length = "2px"
)

Variables

This section is empty.

Functions

func ButtonPrimary

func ButtonPrimary() []css.Rule

ButtonPrimary is the single primary action for a view: signal yellow, ink label.

One per view. If you are writing the second one, one of them is a ButtonSecondary.

func ButtonQuiet

func ButtonQuiet() []css.Rule

ButtonQuiet is the tertiary action: cancel, dismiss, "show all", a row-level action. No border, no fill until hover.

Its job is to be reachable without being visible, so it costs a queue of 40 rows nothing to have a per-row action.

func ButtonSecondary

func ButtonSecondary() []css.Rule

ButtonSecondary is every action that is not THE action: lane-blue outline that inverts on hover.

func Catalog

func Catalog(parseSpec CatalogSpec) ui.Node

Catalog renders a whole product manifest: the result note, the labelled header strip and one line per item — or the empty state when there are none.

design.Catalog(design.CatalogSpec{
    Items:         lines,
    TotalCount:    62,
    FilterSummary: "CATEGORY DESKS",
    Label:         "Product catalog",
    EmptyTitle:    "No lines match this filter",
    EmptyBody:     "Widen the category or clear the stock filter to see the full manifest.",
    EmptyActionLabel: "Clear filters",
    EmptyActionHref:  "/shop",
})

It is a component rather than a bundle for the same reason LanePlacard is: the structure IS the element. The header strip has to share the rows' track geometry, the list needs role="list" to survive list-style:none in Safari, and the empty and filtered states have to exist. Three call sites reassembling that by hand is three chances to ship a catalog that is a stack of links with no columns and a blank page when a filter misses.

The three states this closes, which the brief called out and which hand-rolled catalogs always drop:

  • EMPTY: no items at all -> the void, with copy and one recovery action.
  • FILTERED: fewer items than TotalCount -> a result note saying so, so a narrow filter does not read as an empty shop.
  • ORPHANS: structurally impossible. Rows are full-width, so four items is four rows. There is no last-row-with-two-holes case to handle, which is the whole reason a 3-column grid was rejected.

func CatalogActionLabel

func CatalogActionLabel() []css.Rule

CatalogActionLabel is the trailing affordance: "VIEW →", "NOTIFY ME". It is a <span>, never a <button> — see CatalogLine for the argument. It darkens and underlines when the row is hovered, driven from CatalogRow, and it removes itself in print.

func CatalogAvailability

func CatalogAvailability() []css.Rule

CatalogAvailability is the promoted "can I have it" cell: a StatusChip over a mono hub/promise line. It carries a leading hairline on wide viewports and a rule above it on narrow ones.

This column is the reason the whole primitive exists. If you are tempted to move it after price, or to drop it into the summary, re-read the file header: Atlas's product is not a catalog, it is an answer about availability, and the catalog is where that answer either shows up or does not.

func CatalogEmpty

func CatalogEmpty() []css.Rule

CatalogEmpty is the void: no results, no rows, nothing printed on the sheet.

The empty result set is a state a catalog is in constantly (every over-narrow filter produces one) and it is the state most likely to be left as a bare string, so it is part of the primitive rather than left to the caller. Catalog renders it automatically when there are no items, which is the point — a caller cannot forget a state the component owns.

func CatalogEmptyBody

func CatalogEmptyBody() []css.Rule

CatalogEmptyBody is the sentence under the empty state's heading: what happened and what to do about it. Prose, because it is a sentence someone wrote.

func CatalogHeaderStrip

func CatalogHeaderStrip() []css.Rule

CatalogHeaderStrip is the labelled column header above the manifest, sharing the row's exact track geometry. Hidden below 960px, where the tracks restack.

It is not decoration. Without it the availability column is an unexplained pair of mono strings, and the "reads as a table" claim this primitive makes is false.

func CatalogIdentity

func CatalogIdentity() []css.Rule

CatalogIdentity is the "what is this" cell: title, then the SKU/category line, then the summary.

func CatalogIdentityMeta

func CatalogIdentityMeta() []css.Rule

CatalogIdentityMeta is the mono line under the title: SKU, category, any other code. Set it as a Cluster of spans rather than one pre-joined string, so the separator is a styling decision and the SKU stays selectable on its own.

func CatalogLine

func CatalogLine(parseItem CatalogItem) ui.Node

CatalogLine renders one product manifest line.

The row is a single <a> laid out as a grid, and the trailing "VIEW →" is a <span> inside it, not a <button>. Three alternatives were considered:

  • Row as a container, title as the link, plus a real action button. Correct HTML, but it gives a mouse user an 800x96 target of which only the title is clickable, which everyone finds infuriating and which every catalog on the web has therefore abandoned.
  • Row as a link WITH a nested button. Invalid: interactive content cannot nest inside an <a>. Browsers recover unpredictably and keyboard order gets strange.
  • Row as a link, action as a label. One tab stop per product, whole row clickable, no nested interactive content, and the label still tells the reader what the row will do — which is the only job it had.

The third is what this is. A future contributor wanting an "Add to cart" button in the row should note that adding it requires unpicking the row-as-link decision, and that the right place for a second action is the product page, not twenty-four rows.

Accessibility

The row's accessible name is a spelled-out sentence ("Meridian Desk, SKU-40192, $1,240.00, IN STOCK from IL-HUB, promise 2026-08-04"). Read in DOM order the row would announce as a run of disconnected fragments, and the "·" separators would be read as punctuation noise, so the label is written explicitly — the same cost the lane placard pays for being graphic. The "·" glyphs and the "→" are aria-hidden.

func CatalogManifest

func CatalogManifest() []css.Rule

CatalogManifest is the <ul> holding the product lines. Apply it to the list element; the rows carry their own hairlines, so there is no gap here — the lines are ruled, not spaced, which is what makes the column reads work.

Put it inside a SurfaceFlush so the rows reach the sheet edge, exactly as a table does. Do NOT put it inside a padded Surface: a manifest inside a 16px gutter wastes the alignment it was chosen for.

func CatalogPrice

func CatalogPrice() []css.Rule

CatalogPrice is the price cell: mono, tabular, one step up, right-aligned into a column.

For a line the buyer cannot buy, compose it with StatusValue(ToneNeutral) rather than adding a second bundle — the price of something unavailable is context, not content, and graphite is the package's only de-emphasis tool. CatalogLine does this for you.

func CatalogPromise

func CatalogPromise() []css.Rule

CatalogPromise is the mono hub-and-date line under the availability chip: "IL-HUB · 2026-08-04". Both are machine facts, so Data — and being mono and tabular they align down the column, which is what makes twelve promise dates comparable at a glance. Format the string yourself; this package does not know Atlas's date format.

func CatalogResultNote

func CatalogResultNote() []css.Rule

CatalogResultNote is the count line above the manifest: "14 OF 62 LINES · CATEGORY DESKS". It is Data because a count and a filter expression are machine facts.

It exists because a filtered result set that does not say it is filtered is the most common way a catalog lies to a buyer: twelve products where there are sixty-two, with nothing on screen to say why, reads as "this shop is nearly empty" rather than as "your filter is narrow". The count is the cheapest possible fix and it belongs to the manifest, not to the filter bar — the filter bar is what the buyer already stopped looking at.

func CatalogRow

func CatalogRow() []css.Rule

CatalogRow is the manifest line, for callers composing their own row body. Apply it to an <a> — the row IS the link.

CatalogLine is the shape you almost always want; see its doc for why the row is a link rather than a container with a button in it.

func CatalogSummary

func CatalogSummary() []css.Rule

CatalogSummary is the product blurb: prose, graphite, clamped to two lines, hidden on narrow viewports. The clamp is not cosmetic — see the bundle.

func CatalogThumb

func CatalogThumb() []css.Rule

CatalogThumb is the product image cell: a square on pressed paper with a hairline, cropped rather than stretched. Apply it to an <img>.

It is pressed paper (not white) and it has a hairline, because an image with no frame on a manila page reads as a hole in the page. Give the <img> width and height attributes so the row does not reflow when the image loads, and loading="lazy" — a catalog is the one Atlas surface with twenty-four images below the fold.

func CatalogThumbPlate

func CatalogThumbPlate() []css.Rule

CatalogThumbPlate is the thumbnail slot when there IS no image: a bin label.

This matters more than it sounds, because Atlas's product data has no image field — repository.Product is SKU, slug, title, category, price, status, summary. A catalog primitive whose thumbnail column only works once somebody ships an image pipeline is a primitive that ships as an empty grey column.

The alternative to a grey box with a picture icon is the one a warehouse actually uses: print the code on a plate and stick it on the bin. So the empty state of the image cell is the SKU's distinguishing segment, set in mono on pressed paper — which is legible, scannable, tells the reader something true, and stops looking like a broken image. When real photography arrives, the same slot takes CatalogThumb and nothing else changes.

func CatalogTitle

func CatalogTitle() []css.Rule

CatalogTitle is the product name: proportional, semibold, ink. See the bundle for why it is Prose rather than Display — that argument is the one most likely to be "corrected" by someone applying the display rule mechanically.

func CellMeta

func CellMeta() []css.Rule

CellMeta de-emphasizes a cell that is context rather than content: a timestamp beside an event, a unit beside a quantity. Graphite is the only de-emphasis tool — there is no opacity scale, because translucent text over a zebra stripe has a different contrast on every other row.

func Class

func Class(parseBundles ...[]css.Rule) string

Class folds one or more design bundles into a single hashed class name, for html.Props{Class: ...} call sites — which is how Atlas's markup is written.

html.Props{Class: design.Class(design.Surface(), design.Stack(design.Space4))}

Folding everything into ONE class (rather than concatenating several class names) is deliberate: conflicts between bundles then resolve by argument order, last one wins, which a reader can see. Two class names on one element would resolve by stylesheet emission order instead, which nobody can see.

The parameter is []css.Rule rather than ...any so the call site stays typed. To add a one-off rule, wrap it: design.Class(design.Surface(), []css.Rule{css.MarginY(design.Space5)}).

func Cluster

func Cluster(parseGap css.Length) []css.Rule

Cluster is a horizontal group that wraps: a button row, a chip row, a set of meta fields.

It wraps by default rather than scrolling or overflowing, which is most of what makes this design system survive 380px without per-component media queries.

func ConsoleRail

func ConsoleRail() []css.Rule

ConsoleRail is the persistent left navigation rail: a torn-edge column of manifest lines. Below 960px it becomes a horizontally scrolling strip with the same markup and the tear edge on the bottom. It is hidden in print.

Compose it as RailPlate, then RailGroupLabel / RailLink+RailCode groups:

html.Aside(html.Props{Class: design.Class(design.ConsoleRail())},
    design.RailPlateBlock("Console", "IL-HUB"),
    html.Div(html.Props{Class: design.Class(design.RailGroupLabel())}, html.Text("Inbound")),
    html.A(html.Props{Href: "…", Class: design.Class(design.RailLink())},
        html.Span(html.Props{}, html.Text("Receiving")),
        html.Span(html.Props{Class: design.Class(design.RailCode())}, html.Text("RCV")),
    ),
)

func ConsoleShell

func ConsoleShell() []css.Rule

ConsoleShell is the internal two-column frame: rail, then content column.

html.Div(html.Props{Class: design.Class(design.ConsoleShell())},
    html.Aside(html.Props{Class: design.Class(design.ConsoleRail())}, …),
    html.Main(html.Props{Class: design.Class(design.ContentColumn())}, …),
)

func ContentColumn

func ContentColumn() []css.Rule

ContentColumn is the console's content area: full-width, uncapped, with gutters that tighten on narrow viewports. Cap paragraph width with Measure, not this.

func Data

func Data(parseStep TypeStep) []css.Rule

Data is the monospace role, and it is the load-bearing rule of this design system: EVERY machine fact is Data. SKU, hub code, lane id, promise date, ETA, quantity, price, status code, order number, any identifier.

Mono here is not a stylistic tic, it is three functional guarantees:

  • Fixed advance width, so a column of SKUs or quantities aligns even without a table wrapper, and a wrapping id breaks at a predictable place.
  • Combined with tabular figures, one changed digit occupies exactly one glyph slot, so a transposition (1042 vs 1024) is visible rather than plausible.
  • It marks provenance. A mono string is something the system knows; a proportional string is something a person said. That distinction is the difference between "SHORT 4 UNITS" as a computed discrepancy and as a note somebody typed, and in a warehouse app it is the distinction that matters.

If you are reaching for Data on a sentence, or Prose on an identifier, the hierarchy will read as noise. There is no third option for either.

func Display

func Display(parseStep TypeStep) []css.Rule

Display is the condensed, heavy, uppercase role: page titles, section titles, eyebrows, button labels, table headers. It NAMES a region of the page.

Use it for labels, never for content. A sentence set in condensed uppercase is unreadable past about six words, which is a useful natural limit: if the string does not fit the role, the string is prose.

(The name shadows nothing in this package. It is unrelated to css.Display, the display property.)

func Divider

func Divider() []css.Rule

Divider is a hairline rule, for an <hr> or a plain div.

This is the primary separation tool in Atlas, and it is worth being explicit about why it beats a second box: a rule costs one pixel and says "these are different sections of the same thing", while a box costs a border, a radius, two gutters and says "these are different things". Inside one Surface, the first statement is almost always the true one.

func Edge

func Edge() css.Color

Edge is the CONTROL boundary color: the 1px border that identifies an input, a select or a textarea as something you can type into.

It exists because Hairline was doing both jobs and could only be tuned for one. WCAG 1.4.11 requires 3:1 for "visual information required to identify user interface components", and an unfilled text field IS identified by its border and nothing else — so a 1.5:1 hairline field is, measurably, an invisible control. It looked fine in review because a reviewer already knows where the fields are.

Edge measures 3.46:1 against paper and 3.15:1 against paper-sunk in light, and 3.62:1 / 3.33:1 in dark. It is NOT used for paperwork rules; using it there would undo the quiet the hairline buys.

func Eyebrow

func Eyebrow() []css.Rule

Eyebrow is the small uppercase label above a title: "RECEIVING", "PROMISE DATE", "LANE 4471". It is graphite, never a hue.

The previous design used an orange eyebrow, a teal badge and a yellow button on the same screen — three saturated accents with no relationship, which is how a page ends up with three competing focal points and therefore none. An eyebrow's job is to be legible and quiet; if it needs a color, what it actually needs is to be a StatusChip.

func Field

func Field() []css.Rule

Field is the label-above-control wrapper.

Labels go above, never beside and never floating inside. Beside costs horizontal room the console does not have at 380px; floating-inside labels disappear the moment the field has a value, which is exactly when an operator re-reads the form to check what they typed.

func FieldError

func FieldError() []css.Rule

FieldError is the validation message under a control.

It is one of only three places oxide appears (here, StatusChip/StatusValue with ToneException, and a destructive confirmation), which is what keeps oxide meaning "a human needs to look at this". Prose rather than Data, because a validation message is a sentence someone wrote. Pair it with aria-invalid and aria-describedby; color is never the only signal.

func FieldHint

func FieldHint() []css.Rule

FieldHint is the helper line under a control. Wire it up with aria-describedby — a visually adjacent hint is not an announced hint.

func FieldLabel

func FieldLabel() []css.Rule

FieldLabel is the label above a control: condensed uppercase graphite, the same voice as a table header, because a label and a column header do the same job.

Use a real <label for="..."> element. The style carries no association; the attribute does.

func Graphite

func Graphite() css.Color

Graphite is secondary text: meta, labels, units, timestamps, table headers. It is the only de-emphasis tool in the system — there is no opacity scale, because translucent text over a zebra stripe changes contrast per row.

func Hairline

func Hairline() css.Color

Hairline is the 1px rule color. Nearly every separation in Atlas is a hairline; see Divider for why that is the answer instead of another box.

Hairline is DECORATIVE separation and deliberately sits below the 3:1 non-text contrast threshold (it measures ~1.5:1 against paper in both themes). That is correct and intentional: a rule between two table rows carries no information the row layout does not already carry, so WCAG 1.4.11's "purely decorative" exclusion applies, and a 3:1 separator would be a mid-grey line under every row — the page would read as a grid of cells rather than as printed paperwork.

The moment a 1px line becomes the thing that tells a user "this is a control", that exclusion stops applying. Use Edge there instead. TestTokenContrastHoldsAA pins both halves of this split.

func Ink

func Ink() css.Color

Ink is primary text and heavy rules. Pair it with Paper for body copy.

func Input

func Input() []css.Rule

Input is the text control for WORDS: search, names, titles, notes. Proportional.

InputData is the text control for MACHINE FACTS: SKU, hub code, quantity, promise date, lane id. Monospace and tabular.

The split is not cosmetic. It is the same information architecture the display / prose / data roles encode (see the package doc), pushed into the form layer, where it does real work: a mono SKU field makes a transposed character visible while the operator is still typing, and a tabular quantity field lines up down a column of repeated rows in a receiving form.

Both apply to <input>, <select> and <textarea>.

func InputData

func InputData() []css.Rule

func Install

func Install()

Install emits Atlas's global foundation. Call it exactly once at startup, in both lanes (server bootstrap and wasm main), before the first render.

It is idempotent: css.Global dedupes on (selector + rules) content, so a second call emits nothing. It is also cheap to call again after css.Reset() in a test, which is why the token block is NOT wrapped in a sync.Once — a Once would leave a test with primitives referencing tokens that no longer exist.

Order matters and is the conventional cascade order:

  1. css.Preflight — the framework's reset. Reused rather than reimplemented: box-sizing, margin zeroing, `font: inherit` on form controls, and `font-size/font-weight: inherit` on headings. That last one is what lets Display(StepSubhead) on an <h2> actually mean something instead of fighting the UA stylesheet.
  2. The :root token block, then the dark-mode override of the same names.
  3. The element baseline (html/body/table/form defaults).
  4. The accessibility floor (focus ring, selection).
  5. The print layer (see print.go) — LAST, because it is a third :root token block that has to beat both the light and the dark ones. A dark-mode user printing a receipt still wants ink on paper, and both media queries can match at once, so the tie is broken by emission order.

CONVERSION NOTE: Install assumes it owns the base layer. While Atlas still loads examples/static/css/tailwind.css and example-shell.css, calling Install will emit a second, competing reset. Remove those two stylesheets in the same change that wires this in.

func Lane

func Lane() css.Color

Lane is placard blue: structure, links, focus rings, "pending / in the system". It is the one hue allowed to appear for non-status reasons, and only for structure — a link, a focus ring, a current-page marker. Not for filling shapes.

func LanePlacard

func LanePlacard(parseSpec PlacardSpec) ui.Node

LanePlacard renders the signature dock tag: ORIGIN → DEST, promise date, lane id and posture, on an inked perforated bar.

design.LanePlacard(design.PlacardSpec{
    OriginHub: "NJ-HUB",
    DestHub:   "IL-HUB",
    Promise:   "2026-08-04",
    LaneID:    "LN-4471",
    Posture:   design.PostureOnLane,
})

It is provided as a component rather than as a bundle because the structure is the element: the hole, the perforation, the route/meta/posture ordering and the accessible label all have to hold together, and four call sites reassembling that by hand is four chances to ship a placard that reads as a dark banner.

Accessibility: the bar is a role="group" carrying a spelled-out label ("Lane NJ-HUB to IL-HUB, promise 2026-08-04, posture ON LANE"), and the "→" glyph is aria-hidden. A screen reader gets the sentence; a sighted operator gets the tag.

func Link() []css.Rule

Link is an inline link: lane blue, underlined.

It stays underlined at rest. In a dense manifest full of mono identifiers, color alone does not distinguish a link from a status value, and removing the underline is the most common way an app becomes unusable for a colorblind operator.

func ManifestRule

func ManifestRule() []css.Rule

ManifestRule is the heavy 2px ink rule: under a page title, above a total, closing a section. It is the design system's only "loud" structural mark besides the lane placard, and it is what makes a screen read as a printed manifest.

One per region at most. Two heavy rules in the same view cancel each other out and you are back to uniform weight.

func Measure

func Measure() []css.Rule

Measure caps a text block at ~68 characters, the readable line length.

It exists so the content column can be uncapped. A max-width on the column would make every page a centered strip of content with dead space beside it — the generic dashboard look — while an uncapped column with Measure on its prose gets full-width tables AND readable paragraphs. Apply it to paragraphs, not to containers.

The `ch` unit has no typed constructor, so this goes through css.RawLength — which is the typed escape hatch, not css.Raw, so the value still flows into MaxWidth as a Length.

func NumericCell

func NumericCell() []css.Rule

NumericCell right-aligns a quantity, price or count and pins it to tabular figures. Apply it to the <th> in the header AND every <td> in the column — a right-aligned column under a left-aligned header reads as broken.

Right alignment is not a preference: it puts the units digit of every number in the same column, so magnitude is readable as a shape and 1,000 cannot be mistaken for 100 at a glance. Every numeric column in Atlas gets this.

func PageHead() []css.Rule

PageHead is the title block at the top of a route: eyebrow, title, and the heavy ink rule that closes it.

The rule is what makes the page start. It is the one heavy mark most Atlas pages get, and pairing it with PageTitle means a route cannot accidentally ship a title that floats without a baseline.

func PageTitle

func PageTitle() []css.Rule

PageTitle is the route title: condensed, heavy, uppercase, fluid between 24px and 30px so it survives a 380px viewport without a media query.

func Paper

func Paper() css.Color

Paper is the page surface: cool manila stock. Deliberately not cream — cream plus a serif plus terracotta is a different (and by now very familiar) look, and Atlas is industrial paperwork, not a stationery brand.

func PaperSunk

func PaperSunk() css.Color

PaperSunk is the recessed shade of paper: table zebra stripes, the console rail, input-ish regions. It is one step of value, not a new hue, so a stripe reads as "same paper, pressed" rather than as a second surface.

func PlacardBar

func PlacardBar() []css.Rule

PlacardBar is the inked dock-tag bar with the punched hole and the perforated tear edge, for callers composing their own placard body. LanePlacard is the shape you almost always want.

Place it directly on Paper — the punch and the perforation are painted in the paper token, so on a Recess background they will read a shade off.

func PlacardBg

func PlacardBg() css.Color

PlacardBg and PlacardFg are the lane placard's bar and text colors.

They are exported so a caller composing a custom placard body (see PlacardBar) stays on these two tokens rather than reaching for Ink/Paper. That distinction matters precisely in dark mode: light-mode placard-bg IS ink, so Ink() would look correct in review and then render a full-brightness bar in a dark UI.

func PlacardFg

func PlacardFg() css.Color

func PrimaryActionOnly

func PrimaryActionOnly() css.Color

PrimaryActionOnly is safety yellow, and the name is awkward on purpose.

There is exactly ONE primary action per view, and this is its color. Written at a call site that is not that action — css.Bg(design.PrimaryActionOnly()) on a badge, an icon, a chart series — the name reads as a lie, which is the whole mechanism. The previous design used this hue as one of three ornamental accents and consequently had no primary action anywhere.

In practice you should not need this at all: use ButtonPrimary, which is the only primitive that spends it.

func PrintOnly

func PrintOnly() []css.Rule

PrintOnly reveals an element only in print.

Freight paperwork has a footer the screen does not need and the paper cannot do without: who printed it, when, from which hub, and the document id. A printed page that cannot be traced back to its source is not paperwork, it is a photocopy.

html.Div(html.Props{Class: design.Class(design.PrintOnly(), design.Data(design.StepMicro))},
    html.Text("RCV-ILLINOIS-001 · IL-HUB · PRINTED 2026-07-26"))

func Prose

func Prose(parseStep TypeStep) []css.Rule

Prose is the proportional role: every sentence a human wrote to be read.

The brief calls this role "Body". It is exported as Prose because design.Body() reads like the <body> element, and because "prose" names the thing being styled — running text — which is the test a caller should apply: if it is a sentence, it is Prose; if it is a machine fact, it is Data.

func ProseCell

func ProseCell() []css.Rule

ProseCell is the ONE documented exception to "every table cell is mono": a cell holding a sentence — an operator note, a rejection reason, a product description.

It exists because the alternative is worse. Without it, a 30-word note in mono either blows the column out or wraps into a wall of fixed-width text, and someone eventually "fixes" that by making the whole table proportional, which costs the column alignment the table was chosen for. One opt-out, named for what it is.

func RailCode

func RailCode() []css.Rule

RailCode is the mono route code at the trailing edge of a rail item ("RCV", "PO", "INV", "XFER"). Hidden in the narrow horizontal strip.

This is the mark that makes the rail read as a manifest column rather than as a generic nav: RailLink is already justify-between, so a label plus a RailCode puts every code in the same trailing column, and a column of fixed-advance-width codes aligns. It is the same argument the package makes for putting SKUs in mono, applied to the frame.

func RailGroupLabel

func RailGroupLabel() []css.Rule

RailGroupLabel is a section label inside the rail ("INVENTORY", "INBOUND", "SETTINGS"). It hides itself in narrow mode, where grouping no longer reads.

func RailLink() []css.Rule

RailLink and RailLinkCurrent are the rail's navigation items. Exactly two states: there is no hover-only "active-ish" third style, because the only question a rail item answers is "am I here or not".

Pair RailLinkCurrent with aria-current="page" on the element — the inset bar is a visual marker and carries no semantics on its own.

func RailLinkCurrent

func RailLinkCurrent() []css.Rule

func RailPlate

func RailPlate() []css.Rule

RailPlate is the rail's head block: which console this is and which hub it is speaking for. Pair Eyebrow with a Data code inside it, or call RailPlateBlock for the assembled shape.

It carries the rail's only structural mark besides the perforation, and that mark is a hairline on purpose. See the bundle for the argument against a 2px rule here.

func RailPlateBlock

func RailPlateBlock(parseName string, parseHubCode string) ui.Node

RailPlateBlock assembles the rail head: an eyebrow naming the console and a mono hub code under it.

design.RailPlateBlock("Operations console", "IL-HUB")

It is a component for the same reason LanePlacard is: the two-line eyebrow/code pairing is the thing that makes the rail read as a placard column, and three call sites reassembling it by hand is three chances to ship a rail with a bold label and no code, which is just a nav header again. Both strings are caller-supplied — the design system does not know which hub you are standing in.

func RailPlateCode

func RailPlateCode() []css.Rule

RailPlateCode is the hub code on the rail plate: mono, ink, slightly tracked. It is the loudest thing in the rail and it is still only 13px — the rail's job is to be unmistakably Atlas, not to be seen first.

func RailWidth

func RailWidth() css.Length

RailWidth is the console rail's width as a typed length, for callers that need to reserve or offset by it.

func Recess

func Recess() []css.Rule

Recess is pressed paper: a filter bar, a raw payload, a diff, a form region.

Use it for content that is INPUT to the page rather than output of it. Do not use it to group things a second time inside a Surface — that is the nested-card move wearing a different background, and the reason Recess has no border or radius is to make that misuse look wrong immediately.

func ScreenOnly

func ScreenOnly() []css.Rule

ScreenOnly hides an element in print. It is the escape hatch that lets this package keep its global print layer tiny.

Use it on chrome: a filter bar, a pagination row, a toolbar, a "show all" affordance, a live search box, a toast region. Do NOT use it on a form the operator filled in — the whole point of printing a receiving document is the values that were typed into it, and the design system has no way to tell those two Recess blocks apart. That judgement is the caller's, which is why this is a primitive and not a heuristic.

html.Div(html.Props{Class: design.Class(design.Recess(), design.Cluster(design.Space3), design.ScreenOnly())}, filters…)

func SectionTitle

func SectionTitle() []css.Rule

SectionTitle is a heading inside a page region. There is no third heading level: if a page needs one, it needs to be two pages or two Surfaces.

func SignalFgOnly

func SignalFgOnly() css.Color

SignalFgOnly is the label color for the one primary action, and like PrimaryActionOnly the name is awkward on purpose: it is the ONLY correct foreground for a signal-yellow fill and it is correct for nothing else.

Do not substitute Ink(). Ink inverts between themes and signal does not, so Ink-on-signal measures 9.18:1 in light and 1.36:1 in dark. This token is pinned near-black in BOTH themes, which is the whole point of it existing.

func SplitRow

func SplitRow(parseGap css.Length) []css.Rule

SplitRow is a Cluster whose children push apart: a title on the left, actions on the right, collapsing to a wrapped stack when there is no room.

It exists because "header row with actions" appears on nearly every Atlas page and hand-rolling it invites a justify-between that does not wrap, which is a horizontal scrollbar at 380px.

func Stack

func Stack(parseGap css.Length) []css.Rule

Stack is a vertical flow with a uniform gap: the other answer to "these need separating".

Gap, not margin, deliberately. Margins collapse, they double up between siblings, they need :last-child resets, and they leak out of their container. A flex gap does none of that, so vertical rhythm becomes a property of the container — one place to change it — instead of a property each child has to remember.

Pass a Space constant. Any css.Length compiles, but an off-scale value mints another hashed class and puts the page slightly out of rhythm.

func StatusChip

func StatusChip(parseTone StatusTone) []css.Rule

StatusChip is the status badge, driven by a semantic tone rather than a color.

html.Span(html.Props{Class: design.Class(design.StatusChip(design.ToneException))}, "SHORT 4")

Supply the label yourself — the words are domain vocabulary, and Atlas's are better than any generic set ("SHORT 4", "HELD AT HUB", "CLOSED CLEAN"). Color is a second channel on top of the words, never the only channel: a chip whose meaning is carried by hue alone fails for a colorblind operator and in a printed manifest.

func StatusException

func StatusException() css.Color

StatusException is oxide: a discrepancy, a short shipment, a failed check, a flag. It is a STATUS SEMANTIC, never decoration — the name says "exception" and not "red" so that using it for a decorative accent reads as a false claim about the data. Prefer StatusChip / StatusValue with ToneException, which is how a status is supposed to reach the screen.

func StatusValue

func StatusValue(parseTone StatusTone) []css.Rule

StatusValue tones a bare value rather than wrapping it in a chip: a negative on-hand quantity in a table cell, an overdue promise date, a variance figure.

It sets color and weight only, so it composes over a table cell without disturbing the cell's alignment or family. Use it when the number IS the status — a chip beside a number that already says "-4" is redundant, and a dense manifest cannot afford redundancy.

Same contract as everything in this file: a tone, never a color.

func StatusVerified

func StatusVerified() css.Color

StatusVerified is verify green: approved, confirmed, received clean, closed clean. Same contract as StatusException — a semantic, not a palette entry. Green on an Atlas screen is a claim that something was checked and passed.

func StorefrontHeader

func StorefrontHeader() []css.Rule

StorefrontHeader is the public shell's sticky header bar. Pair it with StorefrontHeaderInner for the width-constrained row inside.

func StorefrontHeaderInner

func StorefrontHeaderInner() []css.Rule

StorefrontHeaderInner is the centered row inside StorefrontHeader. Unlike the console's content column this one IS capped: the storefront is a document, and a document's header should align with the document.

func StorefrontMain

func StorefrontMain() []css.Rule

StorefrontMain is the public content column: capped, centered, with generous between-section spacing (Space7, versus the console's Space5 — the storefront is read, the console is scanned).

func Surface

func Surface() []css.Rule

Surface is the one flat paper plane: hairline border, 2px radius, no shadow, no gradient. Use it once per page region.

Do not nest it. See the file comment for why the alternatives (Divider, Stack) are the right instinct instead.

func SurfaceFlush

func SurfaceFlush() []css.Rule

SurfaceFlush is Surface without padding, for content that should reach the hairline: a data table, a full-width list, a lane placard row.

This is not a second card style. It is the same plane with the gutter removed, because a dense table inside 16px of padding wastes the density it was chosen for.

func Table

func Table() []css.Rule

Table is the dense manifest table. Apply it to the <table> element; thead, tbody, th and td are styled for you, so the markup stays plain and semantic.

html.Div(html.Props{Class: design.Class(design.SurfaceFlush())},
    html.Div(html.Props{Class: design.Class(design.TableScroll())},
        html.Table(html.Props{Class: design.Class(design.Table())}, …),
    ),
)

func TableScroll

func TableScroll() []css.Rule

TableScroll is the horizontal scroll container a wide table needs on a narrow viewport. This is how a 14-column manifest survives 380px: the table keeps its columns and the container scrolls, rather than the table reflowing into an unreadable stack of label/value pairs.

An overflow container with no tab stop is a keyboard trap in reverse — the overflowing content is unreachable without a mouse — so give the element role="region", an aria-label, and tabindex="0":

html.Div(html.Props{
    Class: design.Class(design.TableScroll()),
    Role:  "region",
    Raw:   map[string]any{"tabIndex": 0},   // NOT Props.TabIndex — see below
    Aria:  map[string]string{"label": "Discrepancy lines"},
}, table)

The tabindex has to go through Raw. html.Props silently omits a zero-valued TabIndex (html/html.go:59), and zero is precisely the value a scroll region needs, so Props{TabIndex: 0} renders no attribute at all and the region stays unreachable. This was found by asserting on rendered markup, not by reading the API. (Note the SSR serializer emits the Raw key verbatim, so the markup reads tabIndex="0"; HTML attribute names are case-insensitive, so that is correct, just unusual to see.)

func TokenNames

func TokenNames() []string

TokenNames returns every custom property this design system defines, in declaration order. Install emits exactly this set, and TestEveryReferencedTokenIsDefined walks it to prove no primitive references a name that :root never defines.

That test exists because of a specific, expensive class of bug: var(--typo) resolves to nothing, the declaration is dropped, and the element silently inherits — so a misspelled token looks like a layout bug, not a typo. A var(--name, fallback) would hide it even better. This package uses no fallbacks and proves the names instead.

Types

type Availability

type Availability int

Availability is the buyer-facing answer to Atlas's central question. It is domain vocabulary, and like Posture it maps onto the four visual StatusTone semantics through one function, so the catalog can grow states without the palette growing hues.

It exists so the catalog cannot be handed a color. A caller passes "this is inbound" and the design system decides what that looks like; there is no way to ask for a green chip on an out-of-stock line.

const (
	// AvailStocked — on hand in the hub that serves this buyer. Ships on the promise date.
	AvailStocked Availability = iota
	// AvailNearby — on hand, but in another hub. Still a yes, on a longer lane.
	AvailNearby
	// AvailInbound — not on hand, on a lane, has a promise date. A yes with a wait.
	AvailInbound
	// AvailNone — no stock and no promise. The action changes to a notification.
	AvailNone
)

func (Availability) Label

func (parseAvail Availability) Label() string

Label is the printed availability text: short, uppercase, buyer vocabulary rather than operator vocabulary. An operator says "ON HAND 4"; a buyer needs "IN STOCK".

func (Availability) Tone

func (parseAvail Availability) Tone() StatusTone

Tone maps availability onto the four visual semantics.

The interesting mapping is AvailNone -> ToneNeutral, and it is deliberate. The tempting choice is ToneException, because out-of-stock is the row a buyer most wants to skip. It is the wrong choice for two reasons:

  1. ToneException is FILLED (see status.go), and filled is the design system's scarcest resource. A catalog filtered to a thin category can easily be twelve unstocked lines, and twelve solid oxide blocks is a wall — the exact everything-shouting failure the whole system exists to prevent. Exception has to stay rare to stay loud.
  2. Oxide means "a human must act on this". Nothing has gone wrong when a product is not stocked; no discrepancy was found and nobody has to reconcile anything. Using the exception hue for "unavailable" would teach operators — who read the same palette on the console — that oxide sometimes means nothing.

The unavailable line is de-emphasized instead: neutral chip, graphite price, and a different action label. Absence of emphasis is a signal too, and it is the cheap one.

type CatalogItem

type CatalogItem struct {
	// Href is where the row points. A row with no Href renders as a non-interactive
	// line rather than as a dead link — see CatalogLine.
	Href string

	// Title is the product name, e.g. "Meridian Height-Adjustable Desk". The longest and
	// most-read string on the row.
	Title string

	// SKU is the stock code, e.g. "SKU-40192". Set in mono; also supplies the thumbnail
	// plate's code when there is no image.
	SKU string

	// Category is the taxonomy label, e.g. "Desks". Rendered uppercase by the mono meta
	// line; pass it in whatever case your data holds.
	Category string

	// Price is the formatted price, e.g. "$1,240.00". Atlas stores cents; format them at
	// the call site, where the locale is known.
	Price string

	// Summary is one or two sentences. Clamped to two rendered lines and hidden on narrow
	// viewports, so put the distinguishing detail first.
	Summary string

	// Avail is the buyer-facing availability state. It drives the chip tone, the chip
	// label, the price emphasis and the action label — one field, four consequences,
	// which is what keeps an unavailable line from being rendered as an available one.
	Avail Availability

	// Hub is the warehouse code the promise is made from, e.g. "IL-HUB". Optional.
	Hub string

	// Promise is the formatted promise date, e.g. "2026-08-04". Optional. Prefer an
	// ISO-ish sortable form: it is mono and tabular, so a column of them aligns.
	Promise string

	// ThumbSrc and ThumbAlt are the product image. When ThumbSrc is empty the slot falls
	// back to a SKU plate (see CatalogThumbPlate), which is why this primitive works
	// against Atlas's current image-less data.
	ThumbSrc string
	ThumbAlt string

	// ActionLabel overrides the generated affordance text ("VIEW", "NOTIFY ME"). Leave
	// it empty unless the generated word is wrong for the context.
	ActionLabel string

	// Label overrides the generated accessible name for the row. Leave it empty unless
	// the generated sentence is wrong.
	Label string
}

CatalogItem is one product line. Every string is caller-formatted, for the same reason PlacardSpec's are: this package does not know Atlas's currency format, date format or SKU scheme, and a design package that starts formatting domain values stops being reusable.

The field set is deliberately Atlas's real product shape (repository.Product plus the warehouse columns from InventoryRow) rather than a generic one, because a catalog primitive that cannot express warehouse-aware availability is not a catalog primitive for THIS product.

type CatalogSpec

type CatalogSpec struct {
	// Items are the lines to render, already filtered and sorted by the caller.
	Items []CatalogItem

	// TotalCount is how many lines exist BEFORE filtering. When it is greater than
	// len(Items) the result note reads "14 of 62"; when it is zero or equal the note
	// omits the total. This is the field that stops a filtered catalog from looking like
	// an empty shop.
	TotalCount int

	// FilterSummary is a formatted description of the active filter, e.g.
	// `CATEGORY DESKS · IN STOCK`. Shown in the result note and in the empty state, where
	// it is the difference between "nothing exists" and "nothing matches what you asked".
	FilterSummary string

	// Label is the accessible name of the list, e.g. "Product catalog".
	Label string

	// EmptyTitle and EmptyBody override the empty state's copy. Supply them: the defaults
	// are deliberately generic, and the words on your screen are your domain's, not this
	// package's.
	EmptyTitle string
	EmptyBody  string

	// EmptyActionLabel and EmptyActionHref add one recovery action to the empty state —
	// almost always "clear the filter". Both must be set or the action is omitted.
	EmptyActionLabel string
	EmptyActionHref  string
}

CatalogSpec is a whole manifest, including the states a hand-rolled catalog forgets.

type PlacardSpec

type PlacardSpec struct {
	// OriginHub and DestHub are hub codes, e.g. "NJ-HUB" and "IL-HUB". Short and
	// uppercase; they are set in mono at StepLede and are the loudest text on the page.
	OriginHub string
	DestHub   string

	// Promise is the promise date, already formatted, e.g. "2026-08-04". Prefer an
	// ISO-ish sortable form: it is mono and tabular, so a column of them aligns.
	Promise string

	// LaneID is the freight lane identifier, e.g. "LN-4471". Optional; the LANE field
	// is omitted when empty rather than rendered blank.
	LaneID string

	// Posture is the lane state. It drives both the printed label and the tone.
	Posture Posture

	// Label overrides the generated accessible label. Leave it empty unless the
	// generated sentence is wrong for the context.
	Label string
}

PlacardSpec is the placard's content. Every string is caller-formatted: this design system does not know Atlas's date format, its hub-code casing or its lane-id scheme, and a design package that starts formatting domain values becomes impossible to reuse.

type Posture

type Posture int

Posture is the state of a freight lane. It is domain vocabulary — the words an operator uses — and it maps onto the visual StatusTone semantics via Tone(). Keeping those two enums separate is the useful bit: the domain can grow new postures without the design system growing new hues.

const (
	// PostureOnLane — moving, expected to hit the promise date.
	PostureOnLane Posture = iota
	// PostureHeld — stopped at a hub. Not yet a failure, but a human should know.
	PostureHeld
	// PostureShort — the quantity will not be met. An exception.
	PostureShort
	// PostureClosed — delivered and reconciled. Closed clean.
	PostureClosed
)

func (Posture) Label

func (parsePosture Posture) Label() string

Label is the placard's printed posture text: short, uppercase, operator vocabulary.

func (Posture) Tone

func (parsePosture Posture) Tone() StatusTone

Tone maps a domain posture onto the four visual status semantics. This function is the entire bridge between "what the warehouse says" and "what color the screen uses", which is why it is one switch in one place.

type StatusTone

type StatusTone int

StatusTone is the closed set of states Atlas colors. Four, deliberately: a fifth tone means a fifth hue, and the point of a semantic palette is that a reader can learn it in one screen.

const (
	// ToneNeutral — no claim. A draft, an unstarted step, an informational tag.
	ToneNeutral StatusTone = iota
	// TonePending — in the system, awaited. In transit, queued, submitted, on order.
	// Lane blue, because "the system is handling it" is structural, not alarming.
	TonePending
	// ToneVerified — checked and passed. Received clean, approved, reconciled, closed.
	ToneVerified
	// ToneException — needs a human. Discrepancy, short shipment, failed check,
	// overdue promise, negative on-hand. The only filled tone.
	ToneException
)

func (StatusTone) String

func (parseTone StatusTone) String() string

String returns the tone's lowercase identifier. It is intended for a data-* attribute or a test, NOT as user-visible copy: the label a user reads is domain vocabulary ("SHORT 4", "RECEIVED CLEAN"), which the caller supplies. A design system should not be inventing the words on your screen.

type TypeStep

type TypeStep int

TypeStep is a position on the type scale. It is a closed enum, not a length: a caller cannot invent a 19px step, which keeps the number of distinct emitted classes bounded (7 steps x 3 roles = 21 possible folds) and keeps the page looking like one document.

const (
	StepMicro   TypeStep = iota // eyebrows, table headers, chips, field labels
	StepFine                    // meta, table cells, hints — the dense default
	StepBase                    // body copy
	StepLede                    // opening paragraph, placard values, stat figures
	StepSubhead                 // section titles
	StepHead                    // page titles
	StepBanner                  // storefront hero — at most one per page
)

The scale. Seven steps, and the names say what each is FOR rather than how big it is, because "small" is a comparison and "the size of a table cell" is a decision.

func (TypeStep) String

func (parseStep TypeStep) String() string

String returns the step's identifier, for a data-* attribute, a debug dump or a test name. Not user-visible copy.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL