Inkan
↑↓ move↵ open
GitHub

Theming · ADR

0005: Generated record covers

Date

Context#

A photocard needs art on its front, and an archive of hundreds of records cannot have an illustration made for each. Stock images and generated pictures would weigh down the site, break the flat ink look and need an API or a licence. The art had to be free, instant, unique per record, stable across builds and recoloured by the same tokens as the rest of the page. Teams also need to tell their cards apart at a glance, and a few of them want a motif of their own.

Decision#

Inkan draws a cover for every record as an inline SVG at build time. The drawing is a pure function of the record’s id, so the same file always gets the same print, and of the project’s ink and motif, so a team’s cards look like a set. Every project is assigned an (ink, motif) pair that no other project has, for as long as pairs remain. The SVG carries ink roles rather than colours and is painted by CSS, which means a cover changes with the theme and with any --inkan-ink-* override.

Motifs are functions. Seventy-six are built in: seventeen abstract prints, ten for infrastructure, platform and security teams, ten for product teams, twelve for operations, data and hardware teams, twelve for business teams, seven for finance teams and eight for engineering, product and company functions. Each team motif carries keywords, so a project called security, frontend, dbre or legal gets a shield, a browser window, database discs or a pair of scales without any configuration. A plugin can add or replace a motif with defineMotif and the motifs key, and give it keywords of its own.

How to use it#

Pin an ink or a motif per project#

decisions/payments/_project.yml
name: "Payments"
description: "Ledger, settlement and everything that moves money."
ink: m
motif: ticker
owners: [jules, noor]
order: 20
FieldValuesWhen absent
inkc cyan, m magenta, y yellow, g green, v violet, o orangeAssigned so the (ink, motif) pair is unique
motifA built-in name (below) or a motif a plugin registeredA motif whose keywords appear in the project id or name when one is free, otherwise assigned so the (ink, motif) pair is unique

Both are optional. The ink is the project’s colour everywhere (dots, sidebar, avatar rings, card underline); the motif only affects the covers.

An ink outside the six letters fails the build in the _project.yml schema. A motif that nobody registered fails the build too, naming the file and the available motifs:

inkan: project "sre": unknown motif "ring", did you mean "rings"? (available: a11y, arcs, bank, bars, ...)

How pairs are assigned#

The allocation runs once per build over every project, including bare folders without a _project.yml. Six inks and seventy-six built-in motifs give 456 pairs; plugin motifs add six more each.

  1. Projects that pin both ink and motif are settled first. Two projects pinning the same pair share it.
  2. Projects that pin one field get the other chosen from the free pairs. A half pin avoids the motifs and inks another half pin still needs.
  3. Projects that pin nothing are allocated in id order. Each project ranks all pairs: pairs whose motif matches a keyword in the project’s id or name come first, then the rest, and within each group a stable hash of the id decides. The project takes the first pair that is still free.

Only when every pair is taken do pairs repeat. The ranking is stable across machines, so a build gives the same covers everywhere. Adding an unpinned project does not change the style of any project that comes before it in id order; a project that sorts earlier can move the ones after it. Because pinned projects are reserved before unpinned ones, adding a pin to any _project.yml can change the pair of an unpinned project. Pin both fields on the teams whose look must never move.

How a motif is matched to a team#

Every team motif owns a list of keywords (tables below). When a project does not pin a motif, the allocation splits its id and its name from _project.yml into words and looks for a motif that owns one of them:

  • Text is split at every character that is not a letter or a digit, and at the lowercase-to-uppercase step inside a word, then lowercased. payments-core gives payments core; SecurityTools gives security tools; ML Platform / v2 gives ml platform v2.
  • A word matches a keyword exactly or with a trailing s on either side, so payment finds payments and orders finds order. There is no stemming beyond that: authentication does not find auth.
  • Each built-in keyword belongs to exactly one motif, so a word never points at two built-in motifs. A project whose words hit several motifs (platform-security hits lattice and shield) ranks both equally and the hash of the id decides.
  • A match wins only while that motif still has a free ink. Six teams whose names contain security get a shield in six inks; the seventh gets the next free pair.
  • A pinned motif always wins over a keyword match. A project that pins only ink still prefers a matching motif for the field it leaves open.
  • A project that matches nothing is allocated by the hash alone, as before.

Keywords are matched against the project id (the folder name) and the name, nothing else; a description that says “security” does not count.

The built-in motifs#

Seventeen abstract prints, used by every project that has no keyword match and available to pin:

MotifWhat it draws
ringsConcentric rings around an off-centre point with a halftone fall-off, two hairlines, a wedge in the second ink and a black dot.
tickerHorizontal stripes of ink and key black down two thirds of the card, a disc in the second ink with fine rings, a dashed rule.
latticeA 7 by 11 grid with some cells filled, crosses at every intersection, and a tilted square plate in the second ink.
discsA stack of five flat discs in alternating inks, like platters or coins, between two margin rules.
arcsConcentric half-circles growing from the left or right edge in alternating inks, a black pill and a row of dots at the top.
chevronsStacked chevron bands pointing up or down, one in the second ink with a black blip, and a dashed vertical rule.
wavesA sun in the second ink over a halftone glow, with seven layered wave fronts rolling in below.
sunburstRays from a point near one edge, a disc in the second ink at the centre and a dashed horizon.
bricksA brick wall with some bricks filled in either ink or black, a disc in the second ink on top.
barsA bar chart standing on a baseline or hanging from it, one bar in the second ink with a black marker, dashed grid lines.
orbitA planet in the second ink with a tilted ring passing in front of it, stars and a black moon.
contourSeven contour lines around a wobbly centre, filled in alternating inks, and a dashed line to one corner.
checkerA rotated checkerboard in the main ink, a disc in the second ink with a black satellite.
cubesIsometric cubes in a stair or a cluster, faces in the two inks and paper, a dashed ground line.
crescentA crescent in the main ink over a halftone glow, a small disc in the second ink, scattered black dots.
dotsA dot grid that grows towards one corner, one black dot, crossed by a band in the second ink.
trianglesA triangle mesh with some cells filled in either ink or black, one triangle outlined, a black dot.

Eight of these abstract prints also own keywords, so data, finance, media and growth teams and the classic infrastructure disciplines land on them:

MotifKeywords
discsdbre, database, db, storage, postgres, mysql, sql
ringssre, observability, monitoring, reliability, o11y, telemetry
cubesk8s, kubernetes, containers, infrastructure, orchestration
latticeplatform, infra, core, foundation
barsanalytics, data, metrics, insights
chevronsgrowth, experimentation, acquisition, conversion, activation
tickerfinance, trading, markets, treasury, brokerage, investing
arcsmedia, content, video, audio, streaming, editorial

Ten motifs for infrastructure, platform and security teams (app/src/lib/motifs-infra.ts):

MotifWhat it drawsKeywords
shieldA heraldic shield in the main ink over a diagonal hatch, half of it in the second ink, with a black dot or a chevron on it and a dashed drop line.security, sec, secops, appsec, infosec, trust, compliance, privacy
keyA tilted key with two or three teeth in the main ink over a halftone glow, a keyhole in the second ink and a dashed axis.identity, iam, auth, sso, access, login, accounts, oauth
meshA hub in the second ink with six or eight nodes around it, spokes, a ring (solid or dashed) and a few black packets on the way.network, networking, edge, cdn, mesh, dns, vpc, connectivity
cloudA cloud in the main ink under a sun in the second ink, with dashed rain or a black lightning bolt below.cloud, aws, gcp, azure, hosting, cloudops, finops
pipelineA pipe in the main ink with a step in it, flow arrows, black flanges and a vertical bypass pipe in the second ink.ci, cd, cicd, devops, pipeline, release, delivery, build
gearA big gear with eight or twelve teeth in the main ink meshing with a small one in the second ink, three spokes, a dashed axis.devex, tooling, tools, internal, productivity, automation, enablement
rackA server rack of five or seven units with rails and screws, LEDs on one side and vents on the other, one unit in the second ink and one in black.backend, servers, compute, hardware, datacenter, baremetal, services
queueThree lanes of boxes on rails with black arrowheads at the end, one box black and one slot empty, and a large disc in the second ink at one edge.messaging, events, kafka, async, queue, pubsub, eventing
lensA magnifying glass in the main ink over lines of black text; the glass enlarges the text and highlights one line.search, discovery, relevance, indexing, ranking, browse, seo, sem
terminalA terminal window in the main ink with a drop shadow in the second ink, a black title bar, a prompt chevron, a cursor and lines of output.cli, ops, terminal, shell, console, scripts, oncall, runbooks

Ten motifs for product teams (app/src/lib/motifs-product.ts):

MotifWhat it drawsKeywords
browserA browser window in the main ink with a black title bar, a hero block in the second ink, columns of text lines and an optional sidebar.frontend, web, ui, webapp, website, fe
phoneA tilted phone with a black bezel, a grid of app tiles in paper and one large tile in the second ink.mobile, ios, android, app, apps, native
neuralThree or four layers of nodes fully connected by hairlines, one active path in the main ink, hot nodes in black.ml, ai, ds, datascience, science, llm, models, research, machine
cardA payment card in the main ink with a chip, embossed digits and a two-circle logo, over a second card in the second ink.payments, billing, fintech, checkout, subscriptions, wallet, card, acquiring, issuing
cartA shopping cart in the main ink, solid or wire, with goods in the second ink above the basket and black wheels.commerce, ecommerce, shop, store, catalog, orders, marketplace
penA nib in the main ink drawing a bezier curve, with its control handles and anchor points shown as in a vector editor.design, ux, brand, creative, illustration, visual
flaskA laboratory flask filled to one of two levels with liquid in the main ink, bubbles in the second ink and black vapour.qa, testing, test, lab, labs, quality, experiments
globeA tilted globe in the main ink with a graticule, a continent in the second ink and a black pin.i18n, l10n, localization, international, geo, maps, global
bellA swinging bell in the main ink with two or three sound arcs on each side and a badge in the second ink.notifications, notify, comms, alerts, push, email, deliverability
chatTwo speech bubbles, one in the main ink with typing dots and one in the second ink with lines of text.support, cx, customer, success, helpdesk, community

Twelve motifs for operations, data and hardware teams (app/src/lib/motifs-ops.ts):

MotifWhat it drawsKeywords
gaugeA speedometer dial in the main ink with tick marks, a black needle and a red-line sector in the second ink.performance, perf, latency, capacity, speed, scaling
sirenA beacon lamp in the main ink on a black base, with light rays around it.incident, incidents, response, escalation, outage, emergency
lifebuoyA life ring striped in the main ink and paper, floating on waves in the second ink.dr, backup, recovery, resilience, continuity, bcp
funnelA funnel in the main ink with blocks of data in the second ink and black drops falling in, and a stream leaving the spout.etl, ingestion, dataeng, elt, transform, dataops
lakeA sun over mountains reflected in a rippled lake, the water in the main ink.lakehouse, warehouse, lake, dwh, datalake, snowflake
chipA microchip in the main ink with black pins on every side and a die in the second ink.firmware, embedded, iot, devices, sensors, robotics
plugA plug with prongs in the main ink, black collars and a lightning mark, on a curling cable.api, apis, gateway, integrations, connectors, partners
hourglassAn hourglass in a black frame with sand in the main ink running from the top bulb to the bottom.scheduling, batch, cron, jobs, workflow, workflows
flagA waving flag in the main ink on a black pole, with a stripe in the second ink and a paper emblem.flags, feature, toggles, rollout, launch, launches
compassA compass rose in the main ink inside a ring of ticks, the needle in black and the second ink.architecture, arch, strategy, radar, principles, guild
chainThree interlocked chain links in the main ink, one link in the second ink.blockchain, crypto, web3, ledger, tokens, defi
eyeA wide eye with an iris in the main ink and, on some teams, black lashes.fraud, abuse, moderation, safety, integrity, risk

Twelve motifs for business teams (app/src/lib/motifs-business.ts):

MotifWhat it drawsKeywords
bookAn open book with black lines of text on its pages, a cover in the second ink and a ribbon in the main ink.docs, documentation, wiki, knowledge, writing, handbook
peopleTwo or three bust silhouettes in the main and second inks, overlapping like a team photo.hr, people, talent, recruiting, hiring, onboarding
scalesA black balance with pans in the main ink holding weights in the second ink, tipped to one side.legal, policy, governance, regulatory, ethics, contracts
magnetA horseshoe magnet in the main ink with paper tips, pulling small blocks in the second ink.sales, crm, revenue, leads, deals, quota
megaphoneA megaphone in the main ink with a rim in the second ink, a black handle and black sound arcs.ads, advertising, campaigns, pr, outreach, social, communications, press
gamepadA game controller in the main ink with a black d-pad and buttons.games, gaming, gameplay, esports, studio, play
truckA delivery truck with a cargo box in the main ink, a cab in the second ink and black wheels.logistics, shipping, fulfillment, supply, fleet, dispatch
coinsStacks of coins in the main ink beside one large coin with a star.cost, budget, procurement, spend, pricing, invoicing, monetization
sparkleA large four-pointed sparkle in the main ink with smaller ones in the second ink and black.recommendations, personalization, recs, feed, assist
a11yThe accessibility figure in paper inside a disc in the main ink.a11y, accessibility, inclusive, inclusion, assistive, wcag
trophyA trophy cup in the main ink on a black base, with confetti in the second ink.loyalty, rewards, gamification, achievements, engagement, retention
leafA leaf in the main ink with black veins, on some covers with a small twin in the second ink.sustainability, green, esg, climate, carbon, energy

Seven motifs for finance teams (app/src/lib/motifs-finance.ts):

MotifWhat it drawsKeywords
bankA columned bank building with a pediment and steps.banking, bank, neobank, deposits, branch, psd2
vaultA round safe door with a spoked handle and bolts.custody, vault, wealth, savings, assets, safekeeping
calculatorA desk calculator with a display and a keypad.tax, accounting, fp, bookkeeping, payroll, forecasting
percentA percent sign on a coin or a hanging price tag.lending, loans, credit, mortgage, underwriting, interest
passportAn identity document with a photo, lines of text and a round stamp.kyc, aml, kyb, sanctions, crime, verification
pieA pie or donut chart with one slice pulled out, over a small legend.bi, intelligence, reporting, dashboards, kpi, okr
handshakeTwo sleeved hands clasped across the card.partnerships, alliances, bizdev, business, solutions, presales

Eight motifs for engineering, product and company functions (app/src/lib/motifs-work.ts):

MotifWhat it drawsKeywords
wrenchA wrench crossed with a screwdriver.engineering, eng, production, maintenance, sustaining, craft
puzzleA grid of interlocking puzzle pieces with one piece being placed.product, pm, prd, mvp, roadmap, outcomes
bracketsLarge code braces around a cursor or dots, with lines of code.developer, devrel, dx, sdk, advocacy, relations
headsetA VR headset on its strap, with a beam of light below.ar, vr, xr, metaverse, spatial, immersive
clipboardA clipboard with a checklist, some items ticked.operations, bizops, program, project, pmo, process
capA graduation cap with a tassel.learning, training, education, academy, courses, mentoring
laptopAn open laptop with text on its screen and a keyboard.it, workplace, office, facilities, endpoints, desktop
targetA bullseye with an arrow in it.marketing, lifecycle, gtm, targeting, segments, positioning

The keywords are chosen so that the team names most companies use get a themed motif, and a test keeps it that way: about 130 names from Engineering, Platform and SRE through Payments, KYC and Treasury to Sales, Marketing, Legal and Workplace (app/src/lib/team-catalog.test.ts) must each get a matching motif, on their own and all together on one site. Camel case does not split after a single leading lowercase letter, so iOS and eCommerce stay one word while SecurityTools and dataAPI split.

The nine remaining abstract prints (waves, sunburst, bricks, orbit, contour, checker, crescent, dots, triangles) own no keywords.

Every motif takes two decisions per project (the side it leans to, the number of teeth or rays, the thickness of a line) and the rest per record, so a team’s set is recognisable while every card differs.

What varies per record#

A motif receives two seeded random generators. record is seeded by the record id (payments/adr/0003-stripe-as-primary-provider) and decides positions, sizes, rotations and the paper grain. project is seeded by the project id, so the choices drawn from it are the same on every cover of a team. The record id also picks the card stock:

StockChanceLook
paper3 in 5Ink printed on the paper token (--inkan-paper).
flood1 in 5The ink floods the card; motif in paper and key black.
black1 in 5Light on black; the motif screens over the dark stock.

Each ink has a fixed second ink for overprints (cyan with magenta, magenta with yellow, yellow with cyan, green with cyan, violet with magenta, orange with magenta), so the same two-plate misregistration appears across a team’s set.

A shared motif alone would make a team’s cards hard to tell apart in a grid, so the record id also frames the print. Every cover zooms into its own part of the motif (1.15 to 1.85 times) at its own angle (up to 18 degrees either way), and the paper and black stocks get one flat shape in the second ink laid over the print, a disc or a band in its own place. A team’s set keeps its ink and motif while neighbouring cards differ in composition and colour balance. Plugin motifs are framed the same way and need no changes.

Renaming a file changes its id and therefore its cover. Changing a record’s title or body does not.

Status treatments#

A record that no longer describes the current state has its print treated, as a sticker on an old release would be:

StatusTreatment
drafthalf grey, very pale
rejectedfull greyscale
withdrawngreyscale, half transparent
supersededmostly grey, dimmed
deprecatedsepia, slightly faded

Accepted, implemented, proposed and in-review covers print in full ink. Custom statuses get no treatment unless they reuse one of these ids.

Where covers appear#

  • On every photocard: the record index, the latest records on the landing page, team pages, the timeline and the flippable card in a record’s rail.
  • As the banner of each team card on the landing page, using the project’s newest record (or the project id when the project has no records yet).

Covers are written into the HTML, so they cost no requests. A page with many cards carries the SVG markup of each; the record index therefore renders the first 120 cards and mounts the rest as you scroll.

Recolouring#

The SVG uses classes that cover.css maps to tokens: --inkan-ink-<ink>, --inkan-ink-k, --inkan-paper, --inkan-bg-2 in Stage and the blend mode --inkan-blend. Override those tokens in customCss and every cover follows; see Theme with —inkan-* CSS tokens. There is no per-record cover setting and no way to supply an image instead.

Add your own motif#

A motif is a module whose default export is a MotifFn, wrapped in defineMotif. A plugin registers it under a name with the motifs key, and any _project.yml can then pin that name. The second argument of defineMotif takes keywords, so projects whose id or name contains one of them get the motif without pinning it. This is examples/company-decisions/plugins/stamp.ts:

plugins/stamp.ts
import { defineMotif } from '@tilaven/inkan';
import { circle, n, TAU } from '@tilaven/inkan/covers';
/** A rubber stamp: a ticked ring over a halftone cloud, with a black seal off to one side. */
export default defineMotif((c, record, project) => {
const { w, h } = c;
const side = project() < 0.5 ? 0.3 : 0.7;
const teeth = 24 + Math.floor(project() * 3) * 8;
const cx = w * (side + (record() - 0.5) * 0.12);
const cy = h * (0.3 + record() * 0.12);
const R = w * (0.26 + record() * 0.06);
const tilt = (record() - 0.5) * 0.6;
c.halftone({ spacing: 5, angle: 0.6, maxR: 2.6, cls: 'a', field: { cx, cy, r: w * 0.9, pow: 1.3, amp: 1.1 } });
c.add(circle(cx, cy, R, ' class="b x"'));
c.add(circle(cx, cy, R * 1.18, ` class="sa n x" stroke-width="${n(R * 0.14)}"`));
let ticks = '';
for (let i = 0; i < teeth; i++) {
const a = tilt + (i / teeth) * TAU;
const r0 = R * 1.3;
const r1 = R * (i % 4 === 0 ? 1.55 : 1.42);
ticks += `M${n(cx + Math.cos(a) * r0)} ${n(cy + Math.sin(a) * r0)}L${n(cx + Math.cos(a) * r1)} ${n(cy + Math.sin(a) * r1)}`;
}
c.add(`<path class="sk n" stroke-width="1.6" d="${ticks}"/>`);
c.add(circle(cx, cy, R * 0.62, ' class="sk n" stroke-width="2.4"'));
const seal = tilt + TAU * (0.6 + record() * 0.3);
c.add(circle(cx + Math.cos(seal) * R * 1.9, cy + Math.sin(seal) * R * 1.9, w * 0.05, ' class="k"'));
}, { keywords: ['audit', 'controls', 'sox'] });
plugins/acme.ts
import { definePlugin } from '@tilaven/inkan';
export default definePlugin({
name: 'acme',
motifs: { stamp: new URL('./stamp.ts', import.meta.url) },
});
decisions/platform/_project.yml
name: "Platform"
ink: y
motif: stamp

Rules for the motifs key:

  • A name matches /^[a-z][a-z0-9-]*$/. Anything else fails config validation: plugins.0.motifs.Stamp: motif names use lowercase letters, digits and dashes and start with a letter.
  • The value is an absolute path, a file: URL string or a URL. A URL with another scheme fails with plugin "acme" motif "stamp": expected a file path or file: URL, got ...; a file that does not exist fails with plugin "acme" motif "stamp": module not found: ....
  • A later plugin wins over an earlier one, and a plugin motif named like a built-in replaces it for the whole site.
  • The module’s default export must be a function, or the build fails with plugin motif "stamp" must default-export a function. An exception thrown while drawing fails with motif "stamp" failed for <record id>: ....

Rules for defineMotif(fn, options?):

  • Without options it returns fn unchanged. With { keywords } it attaches the list to the function, which is what the registry reads.
  • keywords must be an array of strings; anything else (a single string, numbers) throws inkan: defineMotif keywords must be an array of strings when the module loads.
  • Each keyword goes through the same word splitting as project names: ['Legal', 'data-science'] becomes legal, data, science. Keep keywords to single lowercase words.
  • A plugin keyword is not checked against the built-in lists. If stamp claimed legal, which scales owns, both would match and the hash would decide; audit, controls and sox are free today.
  • A plugin motif that replaces a built-in name and sets keywords replaces that motif’s keyword list too. One that sets none keeps the built-in keywords of the name it replaces.

The motif is called with (canvas, record, project). record and project are the two generators described above; each call returns a number in [0, 1). Draw with ink classes instead of colours, so one print works in Day and Stage and on every stock:

ClassPaints
aFill in the main ink
bFill in the alternate ink
kFill in key black
pFill in paper
saStroke in the main ink
skStroke in key black
nNo fill (for outlines, together with sa or sk)
xBlend with what is below (multiply in Day, screen in Stage)

The canvas is a MotifCanvas (types in lib/index.d.ts):

MemberWhat it does
w, hSize of the print. The default cover is 220 by 340 units.
add(markup)Appends SVG markup to the print.
id()A fresh id for <defs> entries such as patterns, unique across every cover on a page.
halftone(options)A riso halftone raster: dots grow where the field is strong.
clip(d)Registers a clip path from an SVG path and returns its id.

halftone takes HalftoneOptions: spacing between dots, the screen angle in radians, maxR (dot radius where the field is 1), cls (a, b, k or p), an optional clip id, and field, either one radial fall-off { cx, cy, r, pow, amp } or a list of linear ramps { x1, y1, x2, y2, amp? } that multiply.

@tilaven/inkan/covers exports four helpers for writing the markup: TAU (a full turn in radians), DEG (radians to degrees), n(v) (a number as an attribute value, one decimal, no -0) and circle(cx, cy, r, attrs?) (a <circle> element; attrs is raw attribute markup such as ' class="k"'). The built-in motifs in app/src/lib/motifs.ts, motifs-infra.ts and motifs-product.ts are written with the same API and are the best reference for more complex prints.

Consequences#

  • Zero assets to make or store; a thousand records have a thousand distinct covers on the first build.
  • Deterministic: the same repository builds the same covers anywhere, so screenshots and printouts stay consistent.
  • Teams stay distinct until 222 pairs are in use (more with plugin motifs); after that, pairs repeat.
  • Keyword matching depends on the folder name and name: renaming security to trust-and-safety keeps the shield (trust is a keyword), renaming it to blue-team loses it. Pin motif where the picture must survive a rename.
  • Adding a pin or a project that sorts early can move the covers of unpinned projects. Pin both fields where the look matters.
  • A plugin motif is code that runs at build time; a motif that throws stops the build. There is still no way to upload artwork per record.
  • Renaming a record file gives it a new cover, which is a reason (next to broken links) to keep file names stable.
  • Inline SVG on list pages adds to the HTML size; that is why the index streams cards in chunks.

History 1

  1. ImplementedInkan maintainers9e5e535 commit diff