Define the v1 component system and visual identity #36
Notifications
Due Date
No due date set.
Blocks
Depends on
#37 Build the clickable POC prototype for the internal demo
christian/helmdocs-proposal-system
#35 Assemble the v1 code layout
christian/helmdocs-proposal-system
Reference: christian/helmdocs-proposal-system#36
Reference in New Issue
Block a user
No description provided.
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Question
Brand settled by author direction: this is a HelmDocs product, a sibling to the existing
bidder-tools product rather than its own brand. So this ticket extends an existing design
system rather than authoring one - which makes it considerably cheaper than originally scoped.
Where the system already lives
packages/config/shadcn.cssinnovelpad-desktop-:root[data-brand="helmdocs"]and.dark[data-brand="helmdocs"]token blocks already exist, light and dark, with--primary: 24.6 95% 53.1%(orange). Multi-brand is real infrastructure, not a plan.packages/config/tailwind.config.ts- the shared config..interface-design/reference/design-system- existing reference material.What this ticket resolves
1. The capability distinction, re-derived inside the HelmDocs palette. The surface map
prototype coded Issuing as pine and Bidding as indigo. That was a proposal and it does not survive
an orange primary. The two capabilities still need to be distinguishable at a glance - a user must
always know which authorization context they are standing in - but the mechanism has to come from
the existing tokens rather than from two invented accents.
2. Density, designed against the evaluation matrix first. It is the hardest surface on the map:
column pinning, horizontal scroll, fractional scores that cannot round (a real award separated
71.81 from 70.89), and it must stay legible at 200 requirements. A system that works on the vendor
home and falls over here is the likely failure mode, and it is discovered late unless the matrix
leads.
3. State encodings, which are most of the actual work. These are information design, not
decoration, and they must read at a glance in a dense grid:
Semantic state colour must stay separate from the brand accent, or a gate failure and a primary
action will compete.
4. The component inventory the six surfaces actually need, and which existing shadcn primitives
carry over unchanged.
Carried consequence of the brand decision
Branding the venue as the bidder-tools company means the neutrality argument can no longer be
made by the brand, so it must be made by the product. A retailer may reasonably ask why the
company selling bid-writing AI is running their evaluation. The answers are mechanisms this map
already decided - sealed bidding until the deadline, the human commit that creates every score, the
bar on the model deciding gates or arithmetic - and they are demonstrable rather than rhetorical.
Design consequence: those mechanisms need to be visible in the retailer surfaces rather than
merely true. This is a design requirement, not a marketing note, and it is carried into
the POC prototype.
Prior art: the surface map prototype - https://claude.ai/code/artifact/91357d1c-96a3-43e0-8a3b-d2da6296b01a
Parent: #1
Audit: what the HelmDocs system already covers
Run against
novelpad-desktop. The gap is three items, one of which is a promotion rather than adesign job. Almost everything the six surfaces need already exists.
Carries over untouched
67 UI primitives in
packages/app/src/components/ui/. Every piece of ordinary furniture the sixsurfaces need is present:
sidebar(the shell),command+combobox+virtualized-combobox(the org x capability picker),
resizable(the two-pane compose surface),scroll-area,badge,tooltip,progress,skeleton,empty,card,tabs,separator,sheet,dialog.Table foundation exists.
@tanstack/react-table@8.21.3and@tanstack/react-virtual@3.13.18are already dependencies, with a 390-line
packages/app/src/components/table/data-table.tsx.Brand tokens exist, light and dark, at
packages/config/shadcn.css:181(
:root[data-brand="helmdocs"]).The reference system already publishes in Claude Design's format.
.interface-design/reference/design-system/preview/*.htmlare standalone preview pages, and thereare two kits (
ui_kits/app,ui_kits/marketing). This is the DesignSync card shape already.Gap 1 — semantic tokens are designed but not shipped, and warning collides with the brand
The reference system defines them, in
colors_and_type.css:But
packages/config/shadcn.cssships only--destructive. None of the other three aretokenized. So promoting them is a small PR, not a design exercise.
The genuine problem:
--np-warningis amber, hue ~38. The HelmDocs--primaryis24.6 95% 53.1%, hue ~25. Thirteen degrees apart. At status-dot size, a warning and a primaryaction are near-indistinguishable — which is exactly the "semantic state must stay clear of the
accent" constraint this ticket named, arriving as a concrete collision rather than a principle.
The eligibility panel's
expiring in 11 dayswarning and aSubmit bidbutton would read as thesame colour. This is the one real colour decision in the ticket.
Note also that
--tag-{amber,coral,cyan,dusk,pink,plum,sage}exists but is identity colour fortags. Reusing it for state would collide with tag semantics.
Gap 2 — nothing in the codebase pins a column
grep -rn "sticky" packages/app/src/components/table/*.tsxreturns nothing.data-table.tsxis aconventional sortable, paginated list table.
The evaluation matrix is transposed — requirements down, vendors across — and needs a pinned
first column with horizontal scroll. TanStack supports
columnPinningnatively, so it is buildableon dependencies already present, but it is a new component rather than a variant of data-table.
Gap 3 —
health-indicatoris a false friendpackages/app/src/components/common/health-indicator.tsxsounds like the eligibility panel and isnot: it reports db-worker, websocket and auth connection status. Nothing reusable. Flagging it
because the name will mislead whoever picks this up.
Revised scope
shadcn.css, and resolve the amber/orange collision.The only real colour decision.
Capability wayfinding may need nothing new.
sidebarandcommandexist, and #23 decided thenav tree swaps wholesale on context switch — so the mechanism can be structural rather than
chromatic, which sidesteps the palette problem entirely.
Consequence for the demo chain: this is roughly one colour decision, one component, and a
promotion PR. #36 is no longer the long pole — the corpus is.
Resolution
Prototype, rendered at shipping size on the Friendship PCS corpus: https://claude.ai/code/artifact/c69c2fa3-3b4d-42b8-902c-a1b62259e5ec
The audit's headline held up: the gap is small, and most of it is furniture that already ships.
What did not hold up was the framing. The audit called the amber/orange collision "the one real
colour decision in the ticket." It is not a colour decision at all — it is a boundary decision, and
the actual design problem is elsewhere: HelmDocs has no way to say "the document cannot tell
you," which is the state this product exists to represent.
Decision 1 — the brand accent does not enter data surfaces
The collision is real: semantic amber at hue 38, the HelmDocs primary at hue 24.6. Thirteen degrees.
Repainting amber was tried on paper and abandoned. Every hue with real distance from an orange
accent is already spoken for — red is
--destructive, green is success, and the gap between themis an olive yellow that fails contrast on white and reads cheap at any size. There is no good
caution hue available to a brand whose accent is orange. Accept that rather than fight it.
So the boundary moves instead of the colour:
All four semantic tokens promote into
packages/config/shadcn.cssunchanged — no hue moves.This is a strictly smaller change than the audit scoped.
And the rule that makes 13° survivable everywhere else:
That is checkable in review, which a palette you have to trust is not. It also means the eligibility
panel's expiry warning and a
Submit bidbutton can share a screen without competing: the button isthe only orange thing on it.
Decision 2 — verdicts carry hue, unknowns carry none
This is the encoding, and it is the substantive half of the ticket.
The axis is not good-versus-bad. It is settled versus unsettled — because that is this map's
thesis: the model may locate but never decide (#15), a human commit creates every score (#15), an
ambiguous gate escalates rather than fails (#16). If the encoding is built on good/bad, all of that
becomes invisible in the one surface where it matters.
answerednot_answeredindeterminateAn evaluator scanning two hundred rows sees colour where there is a finding and grey where a human
is still owed. Triage falls out of the palette instead of needing a filter.
indeterminateand "not evaluable" deliberately share a family. They are the same idea arrivingtwice — the artifact cannot say. One is recoverable by a human reading harder, the other never is,
so they take different glyph and fill inside one achromatic family: the eye groups them, the hand
can still separate them.
Decision 3 — density, designed on the matrix first
31 requirements, 12 criteria of which 10 are scoreable, three bidders across the binding gradient.
Four calls are load-bearing, and the corpus forced each one rather than taste choosing it:
Gates render as a pinned band above the scored body. Chesapeake is the cheapest bid in the field
and disqualified on two missing health documents. At row 40 of 200 you would miss that, and the
demo's whole point is that a price-ranked leaderboard puts a disqualified bid on top.
Roll-up parents get no committable cell.
METHOD(30) andEXPERIENCE(20) render as bandsshowing a derived total; their weighted children carry the cells.
ORG_CAPACITYandCOMMUNITYstay scoreable, because their sub-bullets carry no points. This is
isScoreable()fromsrc/scoring/routing.tsappearing on screen, and it is why the rule keys on weight rather than onnesting — Friendship carries both shapes in one document.
Totals are tabular and never rounded. 82.15 against 81.90. A display that rounded to whole
numbers would show a tie in a contest decided by a quarter point.
A disqualified column keeps its scores, struck and greyed, and shows no total. Hiding them would
be easier and worse — the evaluator has to be able to see what was thrown away, and the absent total
is #10's ban on one composite number doing its job in the one place it is tempting to break it.
Geometry: pinned requirement column 360px, vendor columns 132px, base row height 34px with
single-line clamp and click-to-expand, sticky header and sticky footer total.
Decision 4 — capability is stated in words, not in hue
The surface map coded Issuing as pine and Bidding as indigo. That does not survive an orange primary
— and it was the wrong mechanism regardless.
This is an authorization boundary. A dual-role company's vendor vault is invisible to its own
issuing side, so a user who is confused about which context they are standing in reads a boundary as
missing data and files a bug. Colour alone cannot carry a boundary that consequential, and it
excludes anyone who cannot separate the two hues.
Two signals, neither chromatic:
Acting as {Org} — {Capability}strip at the top of the sidebar. "Acting as"phrases it as an authorization statement rather than a label: you are not viewing an org, you
are acting as one.
#23 already swaps the nav tree wholesale. What was missing was a persistent statement of the actor,
because a nav that quietly changed is a nav you do not notice changed.
The audit's guess was right for the wrong reason: capability wayfinding needs no new colour, but
it does need a new (composed) component.
Build list
Four components.
<EvaluationMatrix>(TanStackcolumnPinning+react-virtual, both alreadydependencies — a new component, not a
data-tablevariant),<CoverageMark>(the seven-statevocabulary above; one component, one enum),
<GateBand>,<ContextStrip>.One promotion PR:
--np-success,--np-warning,--np-infointoshadcn.cssbeside the--destructivethat already ships. No hue changes.67 primitives carry over untouched —
sidebar,command,combobox,resizable,badge,tooltip,progress,skeleton,empty,card,tabs,sheet,dialog,separator,scroll-area, anddata-tablefor list surfaces.Do not reach for:
health-indicator(false friend — it reports db-worker/websocket/authconnection status),
--tag-*(identity colour; reusing it for state collides with tag semantics),or a second accent.
Carried into #37
The brand decision means neutrality cannot be argued by the brand, so it must be argued by
mechanism. Three of the four decisions above are where those mechanisms become visible rather
than merely true, and they are the ones to put in front of a retailer:
Commit scoresas the primary action — the human commit is the loudest control on the surfaceindeterminatemarks that route to a personinstead of resolving themselves
Side effect: one drift closed
Pulling corpus data into the prototype surfaced a name drift — the truth files used
determinableFromResponseDocumentwhile the schema column andgateEvaluableFromResponse()usedeterminableFromResponse. Read through the default, R-B6C ("two bid copies submitted") would havestarted failing bidders over a fact no response document can establish. Renamed, and
validate-truth.pynow mirrors the gate rule so it cannot recur silently —a36956b:What this does not settle
No component is built. This ticket fixes the vocabulary and the geometry; the first real
<EvaluationMatrix>will find things a static page cannot — column resize behaviour under 10+vendors, virtualisation against a sticky footer, and how the expand-in-place row interacts with
horizontal scroll. Those belong to #37, not here.