Inventory before abstraction

Repeated interface shapes became useful only after separating accidental variation, shared contracts, and behavior that belonged to a domain.

The fastest way to invent the wrong abstraction is to begin with the examples that already look alike.

After my first component library failed to enter its own applications, I stopped drawing a cleaner component tree and made an inventory. I expected clerical work: collect screenshots, count buttons, note colors, and find the most common dimensions.

The inventory became an architectural tool instead.

It showed three kinds of repetition that my screenshots had collapsed into one. Some differences were accidental and should disappear. Some implementations shared a behavioral contract even when their visuals differed. Others only looked similar while encoding domain rules that should remain local.

Those categories required different responses. A color value could become a semantic token. A focusable modal could become a maintained dialog contract. A panel that happened to have a header and border might remain inside its application because its state and meaning belonged there.

J05N began to become useful when abstraction followed that classification rather than preceding it.

Screenshots made every difference visual

I started with a contact sheet of common screens from C62Y, T04P, X8B6, M31V, and R7K1. I placed apparently related elements in rows: controls, fields, panels, notifications, navigation, and overlays.

The contact sheet revealed obvious drift. There were several near-identical grays, four corner radii, inconsistent focus outlines, and small spacing differences that had no product explanation. It was good evidence for cleanup.

It was poor evidence for component boundaries.

A screenshot could not show whether a button submitted a form or opened a link. It could not show where focus moved when a dialog closed. It could not reveal whether a status meant “failed” or “outcome unknown.” It could not show that two identical-looking fields validated at different moments because one edited a local draft and the other triggered a server query.

The visual inventory privileged what remained after interaction stopped.

I kept the screenshots but added a behavior sheet beside them. Each candidate pattern received a route, purpose, underlying element, state owner, keyboard behavior, failure behavior, and dependencies. A simple-looking control often occupied more rows in the behavior sheet than a complex chart.

This changed the comparison from “Do these things look alike?” to “Which promises do these things make?”

The inventory used instances, not component names

Existing names carried assumptions. If I searched for files named Modal, I would miss overlays implemented inside route code and accept that everything called Modal belonged together.

I inventoried rendered instances instead.

For each route, I followed its primary tasks and recorded the interface objects encountered. The initial fields were:

instance identity
application and route
user intention
semantic element or role
visible presentation
states and transitions
keyboard and focus behavior
data and operation dependencies
current owner
tests and known failures

Instance identity was deliberately independent of source filename. lantern-preview-retry could point to a control implemented inline. pressroom-delete-confirmation could point to a generic overlay component configured with dangerous behavior.

The inventory was not intended to remain a perfect live database. It was a snapshot sufficient to make a design decision. I recorded the source commit and review date so later drift would not masquerade as original evidence.

This prevented names in the old code from becoming votes for the new architecture.

The first category was variation with no meaningful owner or consequence.

Examples included:

  • Three muted text colors chosen from nearby grays.
  • Similar gaps expressed as 11, 12, and 13 pixels.
  • Focus outlines removed in two controls and customized differently in three others.
  • Button labels using different font weights without a content reason.
  • Border colors copied from old mockups after their surrounding surfaces had changed.

I asked a simple question: if these instances became identical tomorrow, would any task, hierarchy, or domain meaning be lost?

When the answer was no and the change remained accessible, the variation was a consolidation candidate. The response did not always require a React component. A semantic CSS variable, a shared focus rule, or a documented spacing decision was often sufficient.

This category produced the easiest migrations because its current behavior was shallow. It also required restraint. A value used twice was not automatically accidental, and visual uniformity was not valuable if it erased hierarchy. The inventory recorded why a difference existed when I could find a reason.

“No defender” did not mean nobody liked it. It meant the difference carried no maintained decision I was prepared to preserve.

Shared contracts sometimes wore different clothes

The second category was more interesting. Several interfaces looked different but promised the same behavior.

R7K1's build status appeared as a compact row. X8B6 displayed an operation receipt in a bordered panel. M31V used an inline publishing message. Their shapes differed, but all represented a durable operation whose state could be:

accepted -> pending -> succeeded
                    -> failed
                    -> outcome unknown

The critical distinction was between failed and unknown. A timeout did not prove that the external effect had not occurred. Each interface needed an operation identity, last observed evidence, safe next action, and language that did not encourage a duplicate attempt.

Centralizing the whole presentation would have made the applications less appropriate to their contexts. Centralizing the state vocabulary and accessibility announcement rules removed a dangerous inconsistency.

This became a J05N pattern contract with multiple renderings. It defined state meanings, allowed transitions, minimum visible evidence, and when a live update should be announced. Each application supplied its own layout.

The inventory had found reuse that a screenshot grouping would never propose.

Visual similarity often concealed domain ownership

The third category contained the abstraction traps.

C62Y had a rules panel explaining which responsive condition currently matched. R7K1 had a preview details panel showing artifact, environment, and expiry. X8B6 had a transaction details panel with account, operation history, and reconciliation state.

Each used a bordered surface, heading, metadata rows, and actions. My original catalogue called all three Card.

Their behavior did not belong to a shared card. C62Y's panel interpreted a rule engine. R7K1's panel followed an environment lifecycle. X8B6's panel guarded financial operation state. Their empty states, actions, update timing, and error handling came from those domains.

J05N could provide low-level composition: surface, stack, metadata list, action group. It should not provide a DetailsCard with enough props to encode every domain.

I marked these instances locally owned, composition eligible. They could reuse primitives and tokens while retaining their own component and state names.

This category prevented the design system from becoming a parallel domain layer. The fact that applications share a border does not mean a package should own what happens inside it.

Dialogs failed all three categories at first

The overlay inventory contained four instances:

  1. A delete confirmation in M31V.
  2. A help panel in C62Y.
  3. An image viewer in T04P.
  4. A preview log drawer in R7K1.

They all dimmed the page. Only the first was a modal dialog in the behavioral sense.

The delete confirmation interrupted the current task, required a decision, kept focus inside while open, supported Escape under policy, and returned focus to the initiating control. The help panel was complementary content and did not need to trap focus. The image viewer behaved like a lightbox but had navigation within a set. The log drawer allowed interaction with parts of the underlying page and was therefore not modal.

My original Modal component made all four use the same backdrop and focus trap. That would convert visual similarity into incorrect interaction.

The inventory split them into a true dialog contract, non-modal disclosure, lightbox-specific behavior, and application-local drawer. Some could share a surface primitive and dismissal utility. They did not share one top-level abstraction.

This analysis also exposed existing accessibility faults. The delete confirmation lacked a reliable name, and the log drawer stole focus despite allowing background interaction. Inventory was not a neutral act; observing contracts revealed where no coherent contract existed.

State ownership decided the abstraction level

When two instances seemed reusable, I asked who could legitimately decide their state transitions.

A primitive button could own visual pressed, disabled, and focus presentation. It could not decide whether an operation was safe to retry. A field could own label and error relationships. It could not decide when a domain value was valid. A dialog could own open focus entry and return. It could not decide whether unsaved data allowed dismissal.

This produced a boundary test:

If the shared package cannot decide a transition
without understanding the application's domain,
the transition does not belong in the shared component.

The package could expose a controlled state and require the application to decide. It could provide a state machine for a genuinely general interaction such as open, closing, and closed. It could document callbacks as proposals rather than assuming they had completed domain work.

The test reduced “smart” components. It also made application code more explicit. A consumer could see where domain authority lived instead of passing a callback into an all-purpose component and trusting internal timing.

State ownership was a stronger predictor of the right abstraction than repeated markup.

Frequency was not sufficient evidence

Buttons and fields appeared everywhere, which made careful primitives worthwhile. A rare dialog could still deserve a shared contract because focus behavior was difficult and consequential. A frequently repeated product row might remain local because every instance shared one domain module already.

I scored candidates across several dimensions rather than count alone:

frequency across applications
behavioral similarity
cost of inconsistency
accessibility complexity
migration feasibility
stability of the proposed contract
maintenance capacity

The score was not an automated formula. It forced the reasons into view.

A focus ring had high frequency, high inconsistency cost, and a stable platform contract; it belonged in a shared style. The operation outcome vocabulary had lower visual similarity but high semantic consequence; it belonged in a shared model. A specific ledger reconciliation table had complex behavior and only one domain owner; it stayed local.

Maintenance capacity mattered because every central decision became an obligation. A theoretically reusable component was a bad addition if I could not test its variations or support migrations from existing uses.

The inventory did not ask what could be abstracted. Almost anything can. It asked what was worth maintaining as shared infrastructure.

A matrix made disagreement visible

For the most uncertain patterns, I created a comparison matrix. Rows were contract questions; columns were existing instances.

For fields, the rows included:

native element
label source
description relationship
error source and timing
required semantics
value owner
blur behavior
submission behavior
focus after failed submission
read-only and disabled distinction

Cells contained observations, not a proposed API. Looking across a row exposed agreement and disagreement directly.

Three fields shared label, description, and error relationships. They differed in value and validation ownership. That suggested a presentational field structure with application-controlled state, not a J05N form engine.

The matrix also prevented the loudest instance from becoming the default. Without it, M31V's most recently edited form would shape the abstraction because I remembered it best. Older projects and less common states remained evidence rather than anecdotes.

When the matrix showed no stable center, I postponed centralization. “Not yet understood” became an acceptable inventory outcome.

Delay was cheaper than a shared API whose only consistency was that everyone worked around it.

Polite content makes almost any component look reusable. I added stress cases from the applications:

  • Long document and account names.
  • Empty histories and partially known operation outcomes.
  • Validation messages containing links.
  • Controls whose labels changed after an asynchronous state transition.
  • Right-to-left text mixed with identifiers.
  • Zoomed layouts with narrow effective widths.
  • Keyboard sequences interrupted by route updates.
  • Old browser fallbacks without newer layout capabilities.

These examples tested whether a proposed contract was real or merely fit to the catalogue.

A metadata row abstraction failed when values needed paragraphs, status controls, or nested lists. It became a simpler definition-list recipe rather than a restrictive component. A segmented control failed when labels wrapped; its intended behavior fit a radio group, but its compact visual arrangement was not universal. A notification region needed a queue policy once several operations changed at once.

Hostile examples did not all become permanent demos. They were design probes. They found the point where the abstraction's assumptions stopped matching content and interaction.

The system learned more from one difficult label than from ten copies of “Save.”

Inventory required fixing some sources first

An abstraction should not standardize a defect merely because the defect is common.

Several applications removed native outlines and replaced them inconsistently. The modal examples had unreliable focus return. Form errors were often visible but not associated with their fields. If I treated frequency as desired behavior, J05N would make those failures reusable.

I added an assessment field:

current behavior: observed
contract status: acceptable | defective | uncertain
desired correction: if known

Defective instances still informed migration difficulty, but they did not vote on the destination. Before centralizing a pattern, I tested its semantic and keyboard behavior against platform expectations and the actual task.

Sometimes the correction happened locally before abstraction. That separated “fix the user-facing defect” from “move the implementation into J05N.” An urgent focus bug should not wait for a package release strategy.

This order also provided evidence. Once the corrected local behavior survived use, its contract was less speculative.

Shared infrastructure should consolidate learning, not turn the average of existing mistakes into policy.

The abstraction proposal cited its evidence

Every J05N candidate gained a short proposal linked to inventory instances. It stated:

  • The repeated decision being centralized.
  • The known consumers and outliers.
  • What remains application-owned.
  • Which variation will intentionally disappear.
  • Which variation remains supported and why.
  • The first migration and its rollback path.
  • Conditions that would invalidate the contract.

For the field structure, the proposal did not claim to solve forms. It centralized semantic label, description, error, required, disabled, and read-only relationships. Applications retained value state, validation policy, and submission.

For dialogs, the proposal centralized naming, initial focus strategy, containment, Escape policy, backdrop interaction, and focus return. The application retained whether an operation permitted dismissal and what confirmation meant.

Linking proposals to real instances made later changes accountable. If a new consumer required a different contract, I could see whether it revealed a general omission or belonged to a distinct pattern.

An abstraction without evidence tends to expand through intuition. An evidence trail gives it a boundary that can be challenged.

Inventory continued after the first release

The initial snapshot did not freeze the applications. New behavior appeared, old routes disappeared, and migrations changed the remaining population.

I kept the maintenance process lightweight. Each J05N change named affected consumers. Deprecations listed known remaining instances. A new abstraction proposal updated the relevant matrix rather than re-inventorying every pixel. Twice a year, I sampled the main routes and looked for drift or local patterns that had earned reconsideration.

The most useful signal was retirement. When an old focus implementation or color role had no remaining consumers, its wrapper and documentation could leave. The inventory made deletion a fact rather than a hope.

It also revealed when J05N itself had become the source of variation. Two components sometimes encoded nearly identical spacing because they were designed at different times. Shared infrastructure does not become exempt from inventory merely by being shared.

The system remained a maintained map of decisions, not a one-time archaeology project.

Abstraction became the last step

Before the inventory, I began by naming a component and then searched for examples that fit it. Afterward, the sequence reversed:

observe instances
-> record behavior and ownership
-> classify variation
-> correct known defects
-> compare contracts
-> choose the narrow shared decision
-> test one migration
-> name the abstraction

Naming came late because names create gravity. Once a package exports Card, every bordered surface starts to look like its consumer. Once it exports Form, application validation begins bending around its assumptions.

The later name could be smaller and more accurate. FieldMessage instead of a form framework. DialogFocusScope instead of a universal overlay. Semantic color roles instead of a themed application shell.

The inventory did not eliminate design judgment. It gave judgment concrete material and made uncertainty visible.

Reuse followed the maintained decision

J05N's early progress was not a dramatic catalogue launch. It was a series of narrower outcomes.

Accidental color and focus variation moved into shared tokens and rules. Field relationships gained one accessible structure while form policy stayed local. True modal dialogs acquired a focus contract. Operation status shared a state vocabulary without sharing one presentation. Domain panels kept their own names and logic while using low-level composition.

The applications became more coherent without pretending to become one application.

That was the principle the inventory uncovered: reuse is strongest around a maintained decision, not a repeated rectangle.

Some decisions are visual. Some are semantic, temporal, or operational. Some repetition should remain because the local domain is the rightful owner. The job is not to centralize the largest possible surface. It is to place each decision where it can be understood, tested, changed, and retired.

By beginning with inventory, abstraction stopped being an act of optimism. It became a conclusion supported by the software that would have to live with it.