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/v6/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:
- 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.
- 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.
- 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
- func ButtonPrimary() []css.Rule
- func ButtonQuiet() []css.Rule
- func ButtonSecondary() []css.Rule
- func Catalog(parseSpec CatalogSpec) ui.Node
- func CatalogActionLabel() []css.Rule
- func CatalogAvailability() []css.Rule
- func CatalogEmpty() []css.Rule
- func CatalogEmptyBody() []css.Rule
- func CatalogHeaderStrip() []css.Rule
- func CatalogIdentity() []css.Rule
- func CatalogIdentityMeta() []css.Rule
- func CatalogLine(parseItem CatalogItem) ui.Node
- func CatalogManifest() []css.Rule
- func CatalogPrice() []css.Rule
- func CatalogPromise() []css.Rule
- func CatalogResultNote() []css.Rule
- func CatalogRow() []css.Rule
- func CatalogSummary() []css.Rule
- func CatalogThumb() []css.Rule
- func CatalogThumbPlate() []css.Rule
- func CatalogTitle() []css.Rule
- func CellMeta() []css.Rule
- func Class(parseBundles ...[]css.Rule) string
- func Cluster(parseGap css.Length) []css.Rule
- func ConsoleRail() []css.Rule
- func ConsoleShell() []css.Rule
- func ContentColumn() []css.Rule
- func Data(parseStep TypeStep) []css.Rule
- func Display(parseStep TypeStep) []css.Rule
- func Divider() []css.Rule
- func Edge() css.Color
- func Eyebrow() []css.Rule
- func Field() []css.Rule
- func FieldError() []css.Rule
- func FieldHint() []css.Rule
- func FieldLabel() []css.Rule
- func Graphite() css.Color
- func Hairline() css.Color
- func Ink() css.Color
- func Input() []css.Rule
- func InputData() []css.Rule
- func Install()
- func Lane() css.Color
- func LanePlacard(parseSpec PlacardSpec) ui.Node
- func Link() []css.Rule
- func ManifestRule() []css.Rule
- func Measure() []css.Rule
- func NumericCell() []css.Rule
- func PageHead() []css.Rule
- func PageTitle() []css.Rule
- func Paper() css.Color
- func PaperSunk() css.Color
- func PlacardBar() []css.Rule
- func PlacardBg() css.Color
- func PlacardFg() css.Color
- func PrimaryActionOnly() css.Color
- func PrintOnly() []css.Rule
- func Prose(parseStep TypeStep) []css.Rule
- func ProseCell() []css.Rule
- func RailCode() []css.Rule
- func RailGroupLabel() []css.Rule
- func RailLink() []css.Rule
- func RailLinkCurrent() []css.Rule
- func RailPlate() []css.Rule
- func RailPlateBlock(parseName string, parseHubCode string) ui.Node
- func RailPlateCode() []css.Rule
- func RailWidth() css.Length
- func Recess() []css.Rule
- func ScreenOnly() []css.Rule
- func SectionTitle() []css.Rule
- func SignalFgOnly() css.Color
- func SplitRow(parseGap css.Length) []css.Rule
- func Stack(parseGap css.Length) []css.Rule
- func StatusChip(parseTone StatusTone) []css.Rule
- func StatusException() css.Color
- func StatusValue(parseTone StatusTone) []css.Rule
- func StatusVerified() css.Color
- func StorefrontHeader() []css.Rule
- func StorefrontHeaderInner() []css.Rule
- func StorefrontMain() []css.Rule
- func Surface() []css.Rule
- func SurfaceFlush() []css.Rule
- func Table() []css.Rule
- func TableScroll() []css.Rule
- func TokenNames() []string
- type Availability
- type CatalogItem
- type CatalogSpec
- type PlacardSpec
- type Posture
- type StatusTone
- type TypeStep
Constants ¶
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.
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.
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
CatalogIdentity is the "what is this" cell: title, then the SKU/category line, then the summary.
func CatalogIdentityMeta ¶
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.
Why the row is a link and the action is a span ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 Input ¶
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 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:
- 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.
- The :root token block, then the dark-mode override of the same names.
- The element baseline (html/body/table/form defaults).
- The accessibility floor (focus ring, selection).
- 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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 PrimaryActionOnly ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
RailGroupLabel is a section label inside the rail ("INVENTORY", "INBOUND", "SETTINGS"). It hides itself in narrow mode, where grouping no longer reads.
func RailLink ¶
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 RailPlate ¶
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 ¶
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 ¶
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 ¶
RailWidth is the console rail's width as a typed length, for callers that need to reserve or offset by it.
func Recess ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
StorefrontHeader is the public shell's sticky header bar. Pair it with StorefrontHeaderInner for the width-constrained row inside.
func StorefrontHeaderInner ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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:
- 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.
- 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 ¶
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.