crashappsec / crashappsec/react-registry

composite blocks: the seven a real consumer hand-rolls, with call-site counts

Open
#14 0 comments 0 reactions 0 assignees View on GitHub
Dominant language
TypeScript
Stars
1
Forks
0
PR merge metrics
No merged PRs in 30d

Description

The registry publishes 70 items, of which three are `registry:block` —
`stat-card-row`, `service-detail-header`, `service-table` — plus `empty` and
`item`. All five are recreated from the Crash Override console and are shaped
for *services*. crashappsec/compass tried to adopt them for wave D5 of its
restyle and adopted none of them; the reasoning is in
[docs/plans/940-composite-blocks.md](https://github.com/crashappsec/compass/blob/main/docs/plans/940-composite-blocks.md).

That is not a complaint about the three blocks — it is evidence about which
blocks are missing. The README names COMPASS as an in-scope consumer, so here
is what a real consumer hand-rolls instead, with call-site counts measured
across 15 pages and ~90 components.

## Ranked by call-site volume

| # | Proposed item | What COMPASS hand-rolls instead | Sites |
|---|---|---|---|
| 1 | `page-header` | 15 distinct heading blocks, no shared component | 15 |
| 2 | `empty-state` | 30 distinct empty/no-data states, no shared component | 30 |
| 3 | `section` (titled card) | `Card` + `H3` assembled by hand | ~59 |
| 4 | `stat-tile` | 5 competing local `StatCard`/`MetricCard` definitions | 40 raw sites |
| 5 | `field-label` | a copy-pasted `FIELD_LABEL` const, with 3 divergent values | 6 |
| 6 | `list-row` | the same "rank + name + status + metric" row re-implemented | 6 widgets |
| 7 | `sort-header` | a page-private sortable column header | 1 |

### 1. `page-header` — title, subtitle, optional icon and actions

The strongest candidate. COMPASS has no `PageHeader`; every page writes its
own. The 15 blocks disagree on **four different heading elements for the same
semantic role** (H1, H2, H3 and an H3-inside-a-component), and **11 of them
override the type ramp with a size class** — `text-xl`, `text-lg`, `text-base`,
`text-2xl`, `text-7xl`. That is the exact anti-pattern the `typography` item
fixed at the primitive level, recurring one level up because there is nothing
to compose. The subtitle is variously `

`, a raw `

`, or a bare
``.

A `` has zero domain knowledge.

### 2. `empty-state` — the `empty` item, generalised

`empty` exists and is close, but it is one fixed presentation: a dashed frame
with a centered icon chip. Real surfaces need more than one weight — a
dropdown's "No results" row cannot carry a 12-unit-tall dashed frame, and a
full-page "nothing scanned yet" wants exactly that.

More importantly, the 30 states COMPASS hand-rolls encode a distinction the
component has no way to express. These three are different things and must not
look alike:

- **empty** — the query ran and matched nothing ("No repositories match these filters")
- **unmeasured** — the value was never collected, which COMPASS renders as an em dash, never a `0`
- **error** — the fetch failed

COMPASS currently carries that distinction in ~30 code comments. A
`variant="empty" | "unmeasured" | "error"` prop plus a `size` would let it be a
type instead. Worth considering for `empty` directly rather than a new item.

### 3. `section` — titled card with an optional icon and actions

`Card` + `Group` + icon + `H3` + body, assembled by hand about 59 times. `Card`
alone appears 150 times across 35 files and `H3` 65 times, almost always
immediately inside one. The padding is chosen ad hoc per site — `gap-0 p-6`,
`p-6`, `gap-0 p-4`, `gap-0 p-3` — which is a spacing scale leaking because
there is no component to hold the decision.

### 4. `stat-tile` — one figure, one label

`stat-card-row` is close but takes the whole row. What repeats is the *cell*.
COMPASS has five separate definitions of it, two of which are byte-for-byte
identical to each other in different files, and the raw
`font-mono text-{lg,xl,2xl,4xl} font-bold` + colour-class pattern appears at 40
sites across 22 files.

Splitting `stat-card-row` into a `stat-tile` item that the row composes would
make both usable. Two notes from trying to adopt the row as published:

- The value is `React.ReactNode` and is printed verbatim, so there is no way to
say "this was never measured". COMPASS needs an em dash plus an
`aria-label="… not assessed"`, and cannot get there without wrapping.
- The row hardcodes `grid-cols-1 sm:grid-cols-2 lg:grid-cols-N`. Inside a
fixed-height dashboard tile the breakpoint has to be shared with the grid
that allocates the tile's row count, so the component cannot own it alone.

### 5. `field-label`

`field` exists and composes label + control + help/error, which is the right
shape for a full form. What COMPASS needed and did not find is just the label
voice — mono, uppercase, tracked — as something you can put above an arbitrary
control. It is now a copy-pasted `const FIELD_LABEL` in 6 files that has
already drifted into **three different values**, which is a live inconsistency
caused by the gap.

### 6 and 7. `list-row`, `sort-header`

`item` covers much of `list-row` already; what is missing is a leading rank or
index slot and a right-aligned metric column, which is why six COMPASS widgets
re-implemented the row rather than composing `item`. `sort-header` — a table
column header with an active-direction indicator — has no equivalent at all.

## One defect, separately

`status-badge`'s union is `healthy | needs_attention | at_risk | critical | stale`.
COMPASS's status vocabulary is `healthy | needs attention | at risk | stale | abandoned`
— no `critical`, and `abandoned` is required and guard-tested. So COMPASS
cannot use `status-badge`, and by extension cannot use `service-detail-header`
or `service-table`, both of which are typed against it. It deleted its vendored
copy rather than leave an incompatible duplicate in the tree.

If the union were a generic parameter, or the status→tone map an injectable
prop, all three items would become adoptable. That is probably the smallest
change in this issue with the widest effect.

## Context

- react-registry#11 — the `/registry.json` index. See the comment I have just
added: the index *is* published, at `/r/registry.json`. Without knowing that,
COMPASS probed item names by hand and got false 404s for `stat-card` and
`empty-state`, which are published as `stat-card-row` and `empty` — it
concluded the registry published almost no blocks. It publishes five.
- react-registry#13 — the `alert` icon contrast divergence.

Happy to send any of these upstream as PRs if the shapes look right.

Contributor guide

Open the contributing guide

Research direction

Begin with docs/plans/940-composite-blocks.md and the published /registry.json, then verify the COMPASS call-site evidence described here. Because seven candidates and a status-badge defect are in scope, first get agreement on one focused item; done means an agreed, tested component change that addresses its stated consumer gap.

Written by the indexing model from the issue text.

Assessment

Tech stack
react, tailwindcss, typescript
Domain
design, frontend
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.