Skip to content

feat(ui): status tones, chips, lists and a preview stylesheet, so a solution page is built from the kit (Part of #1001) - #1005

Merged
antoinetoussaint-byte merged 1 commit into
mainfrom
issue-1001-codefly-dev-ui-the-kit-elements-a-solution
Oct 4, 2026
Merged

antoinetoussaint-byte merged 1 commit into
mainfrom
issue-1001-codefly-dev-ui-the-kit-elements-a-solution

Conversation

@antoinetoussaint-byte

@antoinetoussaint-byte antoinetoussaint-byte commented Oct 4, 2026 •

Copy link
Copy Markdown
Contributor

Part of #1001: items 1–7. Items 8 (Markdown source offsets and resolveLink) and 9 (Board) are #1010. Issue #1001 stays open until both land.

A consuming solution measured where it reached past @codefly-dev/ui: about a third of one 332-line page was hand-laid markup standing in for missing kit pieces, and the page could not be rendered at all without a running host. This PR adds the seven pieces from its gap list. It also makes saas-ui's two data-source surfaces use them, so the kit eats its own components.

Classification

Fix, every part. The kit lacked the primitives, and the token layer behind them lived in the wrong package. Each change is made where that behaviour is owned:

  • the status colours are contract tokens;
  • the components are in the kit;
  • the token→utility layer moved from the host into the kit.

There is no stopgap and no special case for one consumer. Nothing here names a solution.

What changed

  1. Status tones. success, warning and info are new appearance tokens beside destructive, defined in the contract, with light and dark defaults, and listed in TOKENS.md. @codefly/saas-plugin-contract goes 2.3.0 → 2.4.0, and every exact pin moves with it, as Carry a design system in four layers: scale, roles, slots and rules #850 and feat: a DateField whose shape the skin chooses, and the explorer adapter that shows it #881 did.
  2. Chip / ChipGroup.
    • href renders a link, onClick a button, and render swaps in a router link.
    • icon leads and meta trails after a separator: meta="unread" reads "· unread".
    • onRemove with a required removeLabel adds a remove control. It sits beside the action, never inside a link.
    • The chip clips nothing, so the keyboard focus ring on the action and on the remove control is never cropped (WCAG 2.4.7). A test walks every element from each control up to the chip and fails if any clips overflow.
    • ChipGroup is a named, wrapping ul of chips.
  3. Card description. feat: a solution declares the source it is built on, and saas-ui asks only for the credential (Closes #997) #998 shipped this. This PR leaves it unchanged; it's in the stories and the docs here.
  4. List / ListItem and DescriptionList.
    • ListItem takes icon, description, meta and actions. variant="divided" rules a line between rows.
    • DescriptionList is term/value pairs, inline in two baseline-aligned columns or stacked. Rows are keyed by index unless item.key is given, since two rows may share a term.
  5. Banner tone.
    • neutral is the default and keeps today's look.
    • Each status tone adds its own glyph, so the kind of banner is told by shape and not by colour alone.
    • danger is role="alert"; the rest stay role="status".
  6. EmptyState reference. It holds a pointer elsewhere on the page, separate from the explanation (description) and from the action (children). Links inside it are underlined.
  7. The standalone stylesheet. Covered in the next section.

Every new surface has its own type slot (badge-lg, chip, chip-meta, list-item, list-item-description, description-term, description-details, empty-state-reference), so a skin can restyle each one independently. The raw-type baseline is honoured: new files carry zero raw type utilities, and datasources-panel.tsx drops from 17 to 15.

The kit eats its components:

  • DatasourcesPanel
    • Statuses are tones with a dot: paused is warning, no longer secondary; degraded is danger; unknown is neutral.
    • Non-conformant is warning, and collection read access is success / neutral.
    • The hand-laid destructive error card is now Banner tone="danger".
    • The info and warning banners carry tones.
  • DeclaredSourceCard
    • Connected, Set up and Error are success, neutral and danger.
    • Paused is now warning, labelled "Paused", the same as in the panel; it still offers Reconnect. Before, it read "Error · paused" in danger.
    • Two sources matching one declaration is now warning, not destructive: both keep ingesting and nothing is broken, the card just can't say which one is the solution's.
    • The hand-laid <dl> is now DescriptionList, and the ambiguous-match <ul> is now List.

Item 7: the preview stylesheet, and what it may be used for

The move. The @theme colour mapping, the light/dark binding to --appearance-*, and the shadcn custom variants lived in the host's globals.css. So a class a kit component wrote meant nothing outside the host.

  • They are now @codefly-dev/ui/theme.css, and globals.css imports it. src/app/shadcn.css is gone.
  • I compiled the host stylesheet before and after the move and diffed them. The output is byte-identical except for the new tokens.
  • The projection of a skin onto custom properties (appearanceStyleProperties) moved with it into @codefly-dev/ui/skin. src/lib/appearance.ts re-exports it.

What @codefly-dev/ui/preview.css is. The kit's own source compiled with that layer and with the default skin's values at :root, as plain CSS that needs no Tailwind.

It is committed (src/skin/preview.generated.css) and checked for staleness, not built at publish. This changed after review, which found that a dist build was invisible to the kit-version gate:

  • Its bytes depend on the contract's default skin and on the tailwindcss, @tailwindcss/postcss, Lightning CSS and tw-animate-css versions in package-lock.json. None of those is inside the kit's folder.
  • So a later contract or lockfile change would rebuild it under the same version, and publish-frontend-kit.mjs would refuse the release as "already published with different contents" — the v0.0.67 failure.
  • I chose committing over widening the gate to the whole lockfile: widening would demand a kit bump for every unrelated dependency change.
  • The host test runs the generator's --check, and a companion test proves --check refuses a copy one byte off.
  • So any of those inputs moving fails CI until the file is regenerated. A regenerated file is a changed kit file, which the gate then sees.
  • Cost: a PR that bumps Tailwind, including a dependabot PR, now has to regenerate it and bump the kit. That is the honest price of those bytes shipping.

A preview build may use it for: rendering, reviewing and screenshotting a solution's pages and stories locally, and visual tests against them. Dark mode is class="dark" on an ancestor.

A preview build may not use it for:

  • Anything a host loads. Import it only from the preview harness's own entry, never from a module the remote exposes. Bundled into a remote, it would ship a second Tailwind preflight and the default skin's values into the host's document.
  • Evidence of how a deployment looks. It is the default skin, so a screenshot shows structure and states, not the product's appearance.
  • Styling anything but kit components.
    • It contains only classes the kit's own source writes, so a utility a solution writes by hand renders unstyled in preview. That is deliberate: hand-laid markup is what the kit replaces.
    • @codefly-dev/saas-ui is not covered either. Its components render inside the host's stylesheet. Whatever classes they share with the kit will paint; the rest won't.

The guard. src/lib/__tests__/preview-stylesheet.test.tsx renders every kit story and fails if any class one of them carries is missing from preview.css. That keeps "built from kit components" and "painted in preview" the same claim. The class has to match as a whole selector: .border is not satisfied by .border-border, nor .p-4 by .p-4\.5.

test-published-ui.mjs packs the kit, installs it into a scratch consumer, and checks that @codefly-dev/ui/preview.css and theme.css resolve through the package's exports and arrive compiled.

Arbitrary values. DescriptionList's grid-cols-[minmax(0,max-content)_minmax(0,1fr)] is the one structural arbitrary utility. It's fine here because the host compiles this kit. The README now says that a kit element meant to render from a remote carries its structure inline, because the host compiles nothing a remote or another module's kit ships.

That test names one exclusion, file:type-input. The bug predates this PR, and I am reporting it rather than fixing it: the type-slot utilities wrap their rule in :where(&), so behind the file: variant they become :where(.x::file-selector-button). A pseudo-element inside :where() is invalid, so browsers drop that rule in the host stylesheet too, and the preview compile removes it. The input's file-button type has never been styled.

Docs updated

  • packages/codefly-ui/README.md:
  • packages/codefly-ui/TOKENS.md: three colour rows, the new slots, and the name→colour chain, which now runs through theme.css and preview.css.
  • packages/codefly-ui/CATALOG.md: the layout tier, and the sentence on host CSS authority.
  • packages/codefly-ui/README.md, also: why preview.css is committed, and that structure travels inline for anything the host doesn't compile.
  • module/services/frontend/AGENTS.md, "The published client kit": the kit owns the token layer, what preview.css is, and when to regenerate it.

Verification

Run locally on the rebased branch (on top of #998):

  • prepare:frontend, tsc --noEmit and eslint (0 errors).
  • The full pure Vitest project: 300 files, 3,742 tests.
    • One earlier full run showed 7 failures, in kit-sourcemaps, audit-page, onboarding-reminder and content, none of which this PR touches. All four files passed in isolation, and a full rerun was green.
  • scripts/ci kit-version and release-gates tests: 127 passing. The frontend scripts/ tests: 32 passing.
  • kit-version.mjs check after committing and fetching tags: 0.12.0 against v0.0.89.
  • The naming gate, check-boundaries.sh, test:published-ui (now including the stylesheet check), and the inventory test.

Each new component has tests beside the existing ones, covering every variant and state. Each guard I added was checked to bite:

  • the theme test fails with --color-success removed;
  • the focus test fails with overflow-hidden restored on the chip;
  • the whole-class matcher rejects .border against .border-border;
  • --check refuses a stale copy.

I also server-rendered the new stories with only preview.css and screenshotted them in headless Chromium, in light and dark mode. Every component painted.

Kit 0.11.5 → 0.12.0, with ui and saas-ui co-versioned. saas-ui's peer range moves to ^0.12.0 because it now uses tone.

Not verified

  • Nothing against a running host. The host's compiled CSS was diffed, not looked at, and no admin page was opened with the new tones.
  • The chip's focus ring was not checked by eye. The test proves nothing clips it; I did not tab through it in a real browser.
  • The freshness check across platforms. I generated preview.css on macOS. CI regenerates it on Linux and compares; if Tailwind or Lightning CSS output differs by platform, that test will show it.
  • The pipeline Vitest project needs a codefly graph.
  • codefly ci run was not run.
  • No consuming solution has built against preview.css yet. My render harness was a stand-in for one.
  • The new tokens' contrast was not measured. It was chosen to match destructive's contrast, not checked against a WCAG figure.
  • Not checked in the explorer under a real skin. The platform-* skins don't declare the new tokens, so they inherit the defaults.
  • Not published. Consumers get none of this until a deploy-counter tag publishes 0.12.0.

🤖 Generated with Claude Code

…olution page is built from the kit (Part of #1001)

A consuming solution measured where it reached past @codefly-dev/ui: about a
third of one 332-line page was hand-laid markup standing in for missing kit
pieces, and the page could not be rendered at all without a running host. This
delivers items 1-7 of #1001; items 8 and 9 (Markdown source offsets and link
resolution, Board) are #1010.

Status tones. `success`, `warning` and `info` join `destructive` as appearance
tokens (contract 2.4.0, defaults for both modes, TOKENS.md), each one colour
read as text and as a tint of itself, the shape `destructive` already had.
`StatusTone` (neutral | success | warning | danger | info) is shared by `Badge
tone`, `Chip tone` and `Banner tone`. A tone replaces a variant's colours
rather than stacking on them. `Badge` also takes `size` (sm | default | lg) and
keeps #998's decorative `dot`, now sized with the badge.

New components: `Chip` / `ChipGroup` (link, button or router link; leading
icon, trailing meta, a remove control beside the action, never inside it; no
overflow clipping, so the focus ring is never cropped), `List` / `ListItem` and
`DescriptionList`. `EmptyState reference` is a pointer elsewhere on the page,
distinct from the explanation and from the action. Each gets its own type
slots, so a skin can restyle them independently.

The token layer moves into the kit. The `@theme` mapping, the light/dark
binding and the shadcn custom variants lived in the host's globals.css, so a
class a kit component wrote meant nothing outside the host. They are now
`@codefly-dev/ui/theme.css`, which globals.css imports; the host's compiled
stylesheet is byte-identical apart from the new tokens. The projection of a
skin onto custom properties moves with it to `@codefly-dev/ui/skin`, and
src/lib/appearance.ts re-exports it.

`@codefly-dev/ui/preview.css` is the kit's source compiled with that layer and
the default skin. It is committed and checked for staleness rather than built
at publish: its bytes depend on the contract's defaults and on the Tailwind
toolchain in the lockfile, neither inside the kit's folder, so the kit-version
gate could not see them move and the publish would refuse the rebuilt bytes
under an unchanged version. It is for a solution's local preview, stories and
screenshots only: never loaded by a host or bundled into a remote, not
evidence of a deployment's look, and it styles kit components only. A host
test renders every kit story and fails if any class one of them carries is not
defined there, matched as a whole class.

The kit eats its own components: DatasourcesPanel and DeclaredSourceCard read
their statuses from tones (paused is a warning in both, not destructive), the
panel's hand-laid error card becomes a danger Banner, and the card's
declaration and ambiguous-match list become DescriptionList and List.

Kit 0.11.5 -> 0.12.0 (ui and saas-ui co-versioned; saas-ui now needs ^0.12.0);
@codefly/saas-plugin-contract 2.3.0 -> 2.4.0 with every exact pin.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@antoinetoussaint-byte antoinetoussaint-byte changed the title feat(ui): status tones, chips, lists and a preview stylesheet, so a solution page is built from the kit (Closes #1001) feat(ui): status tones, chips, lists and a preview stylesheet, so a solution page is built from the kit (Part of #1001) Oct 4, 2026
@antoinetoussaint-byte
antoinetoussaint-byte force-pushed the issue-1001-codefly-dev-ui-the-kit-elements-a-solution branch from 9e9a713 to 8e525d1 Compare October 4, 2026 16:07
@antoinetoussaint-byte
antoinetoussaint-byte added this pull request to the merge queue Oct 4, 2026
Merged via the queue into main with commit 56eaab2 Oct 4, 2026
24 checks passed
@antoinetoussaint-byte
antoinetoussaint-byte deleted the issue-1001-codefly-dev-ui-the-kit-elements-a-solution branch October 25, 2026 17:07
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