Skip to content

shadcn, taken from Herz — and three controls that were lying - #4

Merged
Navesz merged 3 commits into
mainfrom
shadcn
Aug 26, 2026
Merged

Navesz merged 3 commits into
mainfrom
shadcn

Conversation

@Navesz

@Navesz Navesz commented Aug 26, 2026

Copy link
Copy Markdown
Owner

The UI was built without shadcn even though it was in the plan from the third message of this project. This puts it in, takes the setup from Herz rather than inventing one, and rebuilds the price index on it.

Taken from Herz, not invented

Same style (base-nova), same primitives (Base UI, not Radix), same base colour, same icon library, same components.json. React 19.2.8, Tailwind 4.3.3, Vite 8.2, TypeScript ~6.0 on both sides — the setup transplants rather than adapts.

Also taken: the semantic state families (each meaning carries background, foreground and line, not one loose colour) and the fuller pointer-affordance block.

Not taken: Geist (IBM Plex stays — the mono has real tabular figures), TanStack Table (a Grupo is not a row: its key is the best price per distinct provider, which getGroupedRowModel cannot express), sidebar tokens, recharts.

Two things that were silently dead

Both measured in the emitted CSS, not reasoned about.

tw-animate-css was never imported. animate-in / animate-out are not Tailwind core, so the bundle contained zero rules for them. The tooltip's entry and exit simply did not happen.

shadcn/tailwind.css was never imported, and this one is subtler than it first looks. Tailwind 4 knows data-* natively, so data-active: compiled to [data-active] and the tabs would have looked fine. shadcn defines it as [data-state="active"], [data-active]:not([data-active="false"]). The difference is the ="false" case — a component writing the attribute with a falsy value would get the active styling. It worked by luck, and tabs.tsx uses data-active: eleven times.

The border split was already there, under other names

--color-edge (3.38:1, interactive, WCAG 1.4.11) and --color-line (decorative) map exactly onto shadcn's --input and --border — the generated button.tsx already writes border-border on one variant and dark:border-input on another. Verified in the browser: the outline button resolves to oklch(0.52 …), the interactive one.

Three controls that were lying

Control Was Now
Provider chip said "fal.ai 589", delivered 63 rows — it counted catalogue entries while the table shows groups already narrowed says 63, delivers 63
Column header a literal ↑ in the text and a hardcoded aria-sort="ascending" — an order nobody could change, announced forever three sortable columns, icon for current state, title for what the next click does, rows without a price always last
The premise invisible — every price is one 1024×1024 image at 25 steps, and without saying so "US$ 0,200 per thousand" read as absolute a Badge with a Tooltip beside the headline

Sorting by spread now answers the product's whole question in one click: FLUX.2 [pro] 2,0×, klein 4B 1,6×, klein 9B 1,4×.

Also

  • TableCell and TableHead ship whitespace-nowrap, which would have pushed the maker and task tags out of the Model column instead of stacking them — silently.
  • The max-h-[70vh] inner scroll is gone: two scrollbars in the same direction parked "O que ainda não está aqui" behind thousands of pixels, and that block is the most honest thing on the screen.
  • baseUrl removed — TypeScript 6 deprecates it and 7 stops honouring it.
  • 846 lines in one file became eight. The split is not tidiness: the rule deciding whether the product may claim "2,0× mais caro" was only exercisable by mounting React.

Verified

Gate green (instrucoes · formato · controle · segredo · elos · tipos · lint · fronteiras · testes · build). Checked in the browser at 1280px and 375px: no horizontal scroll, nothing overflowing, no touch target under 24px, table hidden below md, 60 cards.

Filter chips stay at 40px on purpose. shadcn's button scale tops out at 36px, which is desktop-dense — a thumb on a bus needs more than SC 2.5.8's 24px minimum.

Navesz added 3 commits August 25, 2026 21:08
shadcn was in the plan from the third message of this project and the UI was
built without it. This puts the base in, and takes it from Herz instead of
inventing it: same style (base-nova), same primitives (@base-ui/react, not
Radix), same base colour (neutral), same icon library (lucide), same components.json.
The versions line up almost exactly — React 19.2.8, Tailwind 4.3.3, Vite 8.2,
TypeScript ~6.0 on both sides — so the setup transplants rather than adapts.

In this commit: the dependencies, the `@/` alias in Vite and tsconfig, the cn()
helper, components.json, and nine components (button, table, badge, card, input,
skeleton, tooltip, separator, tabs). Nothing consumes them yet — the screens are
the next commit, and splitting it keeps a working tree between two large changes.

Two frictions worth recording:

  - `paths` was set with `baseUrl`, which TypeScript 6 reports as deprecated and
    TypeScript 7 stops honouring. Paths resolve relative to the tsconfig without
    it since 4.4, so `baseUrl` is gone.
  - The generator writes code Prettier disagrees with, so `formato` fails right
    after every `shadcn add`. These are our files once written; they get
    formatted like the rest rather than excluded from the gate.

Gate green: instrucoes, formato, controle, segredo, elos, tipos, lint,
fronteiras, testes, build.
…tly dead

TWO IMPORTS THAT WERE MISSING, BOTH MEASURED IN THE EMITTED CSS

`tw-animate-css` was not imported. `animate-in` and `animate-out` are not
Tailwind core, so the built stylesheet contained ZERO rules for them — checked
with grep on the bundle. The tooltip's entry and exit simply did not happen, and
nothing anywhere reported a problem.

`shadcn/tailwind.css` was not imported either, and this one is subtler than it
first looked. Tailwind 4 knows `data-*` natively, so `data-active:` compiled to
`[data-active]` and the tabs would have LOOKED fine. shadcn defines the same
variant as `[data-state="active"], [data-active]:not([data-active="false"])`.
The difference is the `="false"` case: without the import, a component that
writes the attribute with a falsy value gets the ACTIVE styling. It worked by
luck, and `tabs.tsx` uses `data-active:` eleven times.

THE BORDER SPLIT WAS ALREADY THERE, UNDER OTHER NAMES

`--color-edge` (3.38:1, interactive, WCAG 1.4.11) and `--color-line`
(decorative) map exactly onto shadcn's `--input` and `--border` — the generated
`button.tsx` already writes `border-border` on one variant and `dark:border-input`
on another. Nothing had to be adapted; the names changed and the measurement
survived.

STATE COLOURS BECAME FAMILIES

Six loose colours became six meanings with three tokens each — background,
FOREGROUND and LINE — which is the pattern Herz proves. Without the `-fg` a
component picks its text colour by eye and the contrast becomes a lottery.

VALUES LIVE IN `:root`, NOT IN `.dark`

Prumo is dark and that is settled, but hanging the whole palette on `.dark`
means the page has NO colour at all the day that class goes missing, rather than
being light. The class exists only so the components' `dark:` variants fire.

Also: the pointer-affordance block is now the fuller Herz one (menuitem, option,
tab, combobox, text cursor in editable fields, not-allowed when disabled), and
143 token references migrated across app.tsx, credentials.tsx and indice.tsx.

Verified in the browser: class applied, brass resolves as primary, IBM Plex
still loads, filter chips still measure 40px. Gate green.
846 lines in one file became eight, and the split is not tidiness: the rule that
decides whether the product may claim "2,0x more expensive" was only exercisable
by mounting React. `agrupar.ts` is now pure — Entry[] in, Grupo[] out.

THE FILTER COUNT WAS CONTRADICTING ITSELF IN ONE CLICK

The chip read "fal.ai 589" and delivered 63 rows. It counted catalogue ENTRIES
while the table shows GROUPS already narrowed by the other filters. It now counts
what the click actually delivers — measured in the browser: the chip says 63 and
the counter answers "63 modelos de 95".

SORTING WAS ANNOUNCED AND NOT IMPLEMENTED

The old header carried a literal arrow inside the text ("Mil imagens ↑") and a
hardcoded `aria-sort="ascending"` on the `<th>`. It advertised an order nobody
could change, and advertised the same one forever. There are now three sortable
columns; the icon shows the current state, the `title` says what the next click
does, and rows without a price always sink to the bottom in both directions —
absence is not "cheapest".

Sorting by spread puts the whole point of the product on top in one click:
FLUX.2 [pro] 2,0x, klein 4B 1,6x, klein 9B 1,4x.

THE PREMISE WAS INVISIBLE, WHICH MADE THE PRICE A HALF-TRUTH

Every price here is one 1024x1024 image at 25 steps. Providers charge per
megapixel, per step or per second, and without the shared premise the numbers do
not compare. It is a Badge with a Tooltip now, next to the headline.

Also: `TableCell` and `TableHead` ship `whitespace-nowrap`, which would have
pushed the maker and the task tags out of the Model column instead of stacking
them — silently. The `max-h-[70vh]` scroll went away, because two scrollbars in
the same direction parked "O que ainda não está aqui" behind thousands of pixels
of rows, and that block is the most honest thing on the screen. "Limpar tudo"
replaces the "todos" chip, which undid one filter out of four.

Verified in the browser at 1280 and at 375: no horizontal scroll, nothing
overflowing, no target under 24px, table hidden below md, 60 cards. Gate green.
@Navesz
Navesz merged commit 44fc9bd into main Aug 26, 2026
8 checks passed
@Navesz
Navesz deleted the shadcn branch August 26, 2026 00:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant