← All Projects

yesid.dev-design: a shared visual foundation

One visual foundation, room for different products

A button, a heading and a small response to a click can make separate products feel related. Keeping those details consistent becomes more involved when each product has its own content, behaviour and release schedule.

I built yesid.dev-design to give my projects a common foundation while keeping those differences explicit. It brings together the visual rules, reusable controls and interaction behaviour behind the yesid brand. Transit and yesid.dev have both adopted releases from it. A separate component gallery shows how the shared pieces work together.

This is an ongoing infrastructure project with its own source, releases and responsibilities. Its purpose is to make shared decisions easier to maintain and changes easier to review.

The project at a glance#

The problem#

Several products need to look related without inheriting each other's decisions. Maintaining the same visual rules in separate places makes it harder to tell whether a difference is intentional or whether the copies have drifted.

What I built#

A shared foundation for design values, reusable interface controls and motion, with a gallery that makes their behaviour visible. It also includes common mechanics for quality checks, search metadata, analytics and language routing. Each part has a defined responsibility and a reviewed release path.

The products it serves#

Transit and yesid.dev have adopted the foundation. They keep their own pages, content, data and runtime choices. The internal gallery provides neutral examples for reviewing shared behaviour before each product checks its own use.

The main tradeoff#

Sharing more code also ties more decisions together. Transit requires flat cards; yesid.dev keeps a bevel and hover shadow. I keep those differences local, even when that leaves some duplication. Composed patterns become shared only when three independent products need the same behaviour and responsibilities.

Where it stands#

The September 2, 2026 register records both products at v0.13.2. A v0.13.3 tag exists, and the reviewed main branch includes later development. Products choose their updates deliberately. Those records describe source adoption; they do not establish which version every live page currently serves.

My role#

I direct the visual choices, architecture, ownership boundaries and acceptance of changes. AI assists with implementation. I review its output against the code and tests, and remain responsible for the decisions I accept.

Optional depth#

Explore any of the sections below for more about the decisions, concrete examples and technical contracts:

Origins · Design values · Component behaviour · Product boundaries · Releases and adoption · Role and status

The foundation began inside yesid.dev. The first task was to extract the reusable parts without quietly redesigning the site at the same time. If a colour, spacing rule or animation changed during that move, it would become harder to tell whether the difference was intentional or an extraction mistake.

That distinction shaped the first release. Version v0.1.0 preserves a specific reference from the original site, with the extraction differences documented. The recorded exceptions include rewritten import paths and keeping more specialised motion on the application side. Later changes receive new versions. The original reference stays available and its tag does not move.

This gave the project two separate questions to answer: did the extraction preserve the intended baseline, and is a later design change worth adopting? Moving code and changing the experience can both be useful, but they deserve their own review.

What the foundation serves#

Transit and yesid.dev need a recognisable visual relationship while doing different jobs. Their shared foundation includes colours, typography, interface controls and interaction mechanics. Each product still determines its page structure, navigation, content, data and operating environment. The foundation has no authority over Transit's data pipeline or the editorial choices on yesid.dev.

The repository also contains a private component gallery. It uses the packages directly and makes their behaviour visible with neutral examples. This is a useful place to examine a control before putting it in a product. It is an internal integration environment, with a different role from either external product and its own version boundary.

That distinction matters to the purpose of the project. A shared button can carry a common appearance and predictable behaviour. It cannot decide what an action means in a transit tool or which message belongs on a project page. I wanted the reusable decisions to have a clear home while leaving those product decisions where they could be understood in context.

The implementation is a Bun and Turborepo workspace, with related packages maintained together and the gallery under an application directory. That arrangement makes it possible to work across tokens, components and their examples in one repository. Distribution still happens through deliberate releases, so a workspace change does not become a product update merely because it works in the gallery.

The brand's visual language draws from infrastructure and wayfinding. Orange identifies interaction. Yellow provides markers and orientation. Reflective white and structural black establish contrast and character. Other semantic colours communicate states such as success or error. The intent is a recognisable vocabulary that can support several kinds of information.

Those decisions live in structured design values, usually called tokens. A token gives a colour, text size, space, radius or animation duration a name and an agreed meaning. Components refer to those values rather than independently choosing another almost-identical one. This makes the relationship between a design decision and its uses easier to inspect.

From one source to several outputs#

The editable token source is a DTCG-format JSON file. DTCG provides a structured way to describe design values. Generators turn that source into the formats needed by stylesheets, motion code and the design reference. Generated files are compared with that source to catch hand-edited copies that could become a competing version of the design.

The repository's build adapter makes its four output paths explicit:

code ts
export const artifactPaths = [
  'DESIGN.md',
  'apps/gallery/src/app.css',
  'packages/motion/src/tokens.ts',
  'packages/tokens/tokens.css',
] as const;

These outputs serve different readers: the design reference, the gallery stylesheet, the motion code and the shared token stylesheet. The token engine itself takes data and returns content. A repository adapter decides where to write it. A product using the engine keeps its own output paths and must identify its own source truthfully in generated headers.

That separation avoids building knowledge of a particular application's folder structure into the shared package. It also leaves room for a product to maintain reviewed local values or output arrangements without pretending those are universal brand choices.

A small change with a visible trail#

One repository test makes this concrete. In a temporary fixture, it changes the fast animation duration from 150 to 151 milliseconds. It expects the motion TypeScript and package CSS to change, and the gallery stylesheet to remain untouched. It then runs the generator in check mode and expects no further change. The one-millisecond edit is a test input, not a redesign or a measured improvement.

The example checks two useful properties. A source change must reach the outputs that use it, and repeated generation must settle on the same result. The writer compares existing content before saving, so unchanged files do not need to be rewritten.

The surrounding checks add other layers: committed artifacts are compared with generated content, the token CI command detects differences, and an optional local commit hook rejects generated outputs staged without a relevant source change. Line endings are deliberately kept consistent across operating systems because a byte comparison also notices invisible newline differences. These mechanisms help catch drift; each has a defined scope.

Responsive values and design-tool handoff#

Some type and spacing values are fluid. They vary with available width between a defined minimum and maximum. The source keeps all three parts of the calculation, including the preferred value, and the CSS generator emits a clamp expression. This preserves the underlying decision rather than recording only the size observed in one screenshot.

Different destinations have different capabilities. The Figma export carries the clamp expression as a string; it does not turn it into a native responsive formula. The design-reference typography uses the structured maximum where its format accepts a single dimension. The Figma round trip also remains manual: export the variables, apply them through the design tooling, then verify the returned export. That is a useful connection between code and design, with explicit translation limits.

The component gallery renders actual package code with demonstration content. It includes buttons, tabs, a searchable selector, collapsible content, scrollable areas and other controls. It also shows the brand-specific pieces: station markers, section labels, blueprint-style frames and a terminal cursor.

A static default state only answers part of the question. The gallery includes disabled controls, loading, errors, overflowing content and labels supplied by the caller. Light and dark themes make it possible to inspect the same shared pieces in different visual conditions. Browser scenarios cover rendered interactions, responsive containment and accessibility-related behaviour as well as screenshot comparisons.

A component's responsibility#

Here, a contract describes what a component should show, how it should behave, and which decisions stay with the product using it. That includes details that may not appear in the first screenshot.

The searchable selector is a useful example. Its shared mechanics cover the open state, selected value, typed query and clearing behaviour. The caller supplies the options, stable values, visible labels, search text and accessible copy. The reviewed contract also specifies that closing resets the temporary query and that a value changed externally updates the displayed label. Those are observable behaviours a product can rely on and test.

Some requirements cross the package boundary. The selector uses a minimum tap-target variable that the product must define as 44 pixels. A translated close label must come from the product when it uses a sheet. Reusing a control therefore includes setup obligations; the import alone does not establish that its surrounding page is usable.

The same principle applies to HTML semantics. A control acting as a link and one acting as a button expose the attributes and element reference appropriate to what they render. This helps keep the public API aligned with the actual interface instead of accepting combinations the component cannot meaningfully honour.

Motion and reduced motion#

Motion has its own rules. Larger or continuous effects are gated by the reduced-motion preference, while some brief interaction feedback remains. The policy distinguishes pointer-following, large scale changes, continuous movement and scroll effects from smaller responses such as a colour change or a short press reaction.

For example, when the page sets up the effect, a card's pointer-following movement is disabled if reduced motion is requested or the device does not support hovering. These three guards come from the card action:

code ts
	if (typeof window === 'undefined') return { destroy: () => {} };
	if (isPrefersReducedMotion()) return { destroy: () => {} };
	if (!window.matchMedia('(hover: hover)').matches) return { destroy: () => {} };

The first check also lets the action do nothing in an environment without a browser. If the conditions allow the effect, it attaches pointer listeners and updates the offsets used by the card's inner elements. Leaving the card resets those offsets, and destroying the action removes the listeners.

The timing of the checks is part of the behaviour. This synchronous preference check runs at initialization. A changed operating-system preference takes effect on the next initialization or navigation for this policy; it is not a promise that every attached action reacts immediately. Other behaviours have their own CSS rules, such as stopping the skeleton pulse or removing a collapsible transition under reduced motion.

Closed content also needs more than an animation. The shared collapsible content can remain mounted while being marked inert and hidden from assistive technology when closed. A product replacing a simpler wrapper must account for the extra structure and closed-state behaviour. The visual transition and the ability to interact with the content are separate things to review.

What the checks can establish#

Package tests inspect shared contracts. The gallery examines their rendered integration. Quality engines inspect things such as colour contrast, style patterns and SEO coverage, using policy supplied by the caller. A product chooses its palette, thresholds, exclusions and expected results; the engine returns findings.

This gives a failed check somewhere specific to lead. An incorrect shared interaction belongs in the package. A product's conflicting visual requirement belongs with its local adaptation. A gallery screenshot gives evidence about the gallery. Full-page accessibility and product behaviour still need review in their actual context.

The most consequential decisions concern what each product keeps. Reuse creates a maintenance relationship: once several products depend on the same behaviour, changing it asks all of them to consider the consequences.

The card conflict#

Transit and yesid.dev share a foundation, but their cards do not need identical treatment. Transit's documented contract requires a flat card without shadow or edge highlight. yesid.dev keeps a bevel and a hover shadow. These are explicit, incompatible requirements, with consumer tests supporting them.

The common surface can remain shared while a local wrapper or style owns the difference. The shared package does not check the application's name to decide which appearance to render. If a proposed abstraction requires that knowledge, the project's governance sends that behaviour back to the product.

This is more than a preference about code style. If the bevel were added to the shared card to solve one site's need, Transit would receive a change its own tests reject. If the package accumulated named exceptions for each application, its maintainers would need to understand more product policy each time they changed a common element.

What the rule of three actually covers#

Composed patterns follow a rule of three: three independent consumers, meaning products that use the component, must need the same contract before the pattern is promoted into a shared package through its own release. This is a project governance rule. It is not a count of clients, a deployment target or a claim that every primitive already has three production users.

CollapsibleSection illustrates the distinction. Two products can both show a heading, chevron and hidden content while differing in header composition, persistence, control signals and animation semantics. The lower-level collapsible primitives can be shared. The complete section controller stays local until there is evidence for one common contract.

Footer composition follows the same reasoning. Small shared elements can help build a footer, while its navigation, status information, attribution and arrangement remain product decisions. A familiar appearance alone does not settle who should own the whole composition.

This leaves some duplication in place. The cost is maintaining those local pieces. The benefit is that differences remain explicit while the real requirements are still different. Promotion then becomes a decision supported by use, rather than an assumption made from two similar files.

State, configuration and product authority#

A controlled view can render a state and offer callbacks without owning how that state is stored or what the action means. QuietModeButton, for example, shares two-button markup, icons and interaction geometry. The caller supplies the copy, current state, persistence and both actions. Its visual reuse does not move product control into the package.

Even configuration has a boundary. The UI class-merging vocabulary is fixed when the application starts, per loaded module graph. Equivalent initialization can repeat; conflicting initialization throws. It must not be derived from a request, user, tenant or current language. A browser bundle and a server-rendering bundle have their own initialization. This keeps a shared configuration mechanism from becoming a container for changing user-specific state.

Domains beyond the visible interface#

The foundation now includes mechanics outside styling. The SEO package builds structured metadata and sitemap entries, while the product owns page content, canonical routes, indexing choices and image templates. Locale routing shares path parsing and localization mechanics, while each product chooses its languages, published pages, fallback behaviour and framework integration.

Analytics follows a similar boundary. The shared package supplies consent-aware state and sending mechanics. The product supplies its domain, event catalogue, consent text, storage choices and transport. In the reviewed source, consent and domain checks happen before transport loading and again before sending. Using those mechanics does not itself decide the product's privacy policy.

These domains share a reason for being here: their repeatable mechanics can serve several products. Each still has a narrow owner. Navigation, data contracts, credentials, deployment adapters and product-specific decisions stay outside the foundation.

Each product adopts an exact version. A release of the foundation does not automatically update every site. The unit of change is a reviewed adoption in the receiving product, with its own generated files and acceptance checks.

Coordinated releases and a separate tooling line#

Seven packages move together: tokens, motion, gates, UI, SEO mechanics, analytics and locale routing. Coordinating them gives a release one known package set. It also means that a change affecting one part still needs review within that coordinated release. The private gallery is outside this version group, and shared compiler and task configuration has its own independent release line.

Packages ship TypeScript and Svelte source. Products import the component families they need and explicitly opt into shared stylesheets. That makes the build integration visible: a Tailwind consumer needs to scan the installed UI source, and its stylesheet must supply the application values required by the components.

Public-interface reports make changes to the supported API reviewable. Release fragments describe the consumer-visible change and any migration. The compatibility policy also distinguishes adding a capability, fixing a defect and removing an established contract. A stable removal requires both a documented waiting period of at least 90 days and an intervening minor release. Those are maintained rules, not a reason to assume an upgrade needs no inspection.

Knowing what was installed#

The adoption tool installs an exact release archive and records what it received in a manifest. The receipt identifies the tag, underlying commit, asset size and digest, included package set, adoption-tool identity and installed payload hash. In plain terms, it ties the installed files to a specific release and gives later checks something concrete to compare.

Offline check mode verifies the installed payload and its receipt. It does not contact the release service to prove upstream CI, and it does not establish that the product behaves correctly. Release provenance, local integrity and product acceptance answer different questions.

The one-direction rule follows from that structure. Products do not repair shared code by patching their installed copy. A shared defect is corrected in the foundation and delivered through a new release. A product-specific difference belongs in the product's adapter. Otherwise the version in the manifest would stop explaining the code the product actually runs.

Following a change into a product#

Consider a reviewed change to a shared animation duration. The source value changes upstream, its generated outputs are examined, and the affected package and gallery checks run. The release describes the change. Transit or yesid.dev can then choose that exact version, regenerate its own outputs and inspect where the duration appears in its interface.

A component change asks additional questions. Did the new markup alter a product selector? Does a local card still preserve its intended surface? Does closed content still behave correctly? Do translated labels fit? The adoption contract requires relevant product tests, type checks, a build and browser comparison across the affected themes, sizes and interaction states.

This adds work to delivery. It also makes the decision traceable: what changed upstream, which product accepted it and what was checked there. A passing gallery run cannot replace those answers because the gallery uses workspace packages and its own demo composition.

Rollback follows the same deliberate path. The product adopts a previously accepted exact release, reconciles dependencies with that release's package set, regenerates its outputs and repeats its checks. Older versions may contain fewer packages. Restoring files without reviewing that dependency relationship would leave part of the upgrade decision unresolved.

My role includes visual direction, architecture, responsibility boundaries and acceptance decisions. Implementation is AI-assisted. The documented workflow includes reviewing agent output, challenging assumptions and checking claims against source and tests. I remain responsible for what I choose to accept.

The recorded adoption baseline is v0.13.2 for Transit and yesid.dev, dated September 2, 2026. The foundation also has a v0.13.3 release tag, with subsequent hardening work on the main branch separate from that tagged release. The technical discussion here describes the reviewed source and its documented contracts. A version recorded in a product tells us which code it adopted; its deployed pages still need their own verification.

The project has made ownership a concrete part of the design. A colour has a source. A component has a responsibility. A product difference has an explicit place to live. A release has an identity, and accepting it requires work in the product that receives it.

For a prospective client, that is the useful part of this case study: how I approach an interface that needs to keep evolving after its first launch. The visual details matter, and so does leaving the next change understandable. I am continuing to develop that practice through the decisions, checks and tradeoffs this foundation makes visible.

A shared foundation for design values, interface controls and motion. How Transit and yesid.dev adopt it while keeping their own product responsibilities.

TypeScript
Private repo (for now)

Let's build somethingthat moves.

Database, pipeline, dashboard, website: wherever it's stuck, that's where we start.