@kapable/remix-kit (0.3.0)

Published 2026-07-26 16:24:38 +00:00 by kapable

Installation

@kapable:registry=
npm install @kapable/remix-kit@0.3.0
"@kapable/remix-kit": "0.3.0"

About this package

@kapable/remix-kit

See it live: https://remix-kit-showcase.kapable.kapable.run/ — a curated component-family directory at /, focused docs at /components/:slug, every island actually running at /live, and the exhaustive audit sheet at /all.

Shared app-tier library for Remix 3 (beta.5) apps on the Kapable platform: petrol design tokens, AA-by-construction UI components, leak-proof SSE realtime, sqlite data glue, validated startup configuration, request correlation, and a router test harness. TypeScript source consumed directly (no build step), remix@3.0.0-beta.5 exact-pinned as a peer, Node ≥ 24.3. Typechecked on TypeScript 7 (the Go-native compiler) — the same toolchain new consumer apps resolve.

Remix 3 is not React: components are (handle) => () => JSX, state is closure variables + explicit handle.update(), styling is the css() mixin. Read docs/SKILL-remix3-epoch2.md before touching anything; read skills/SKILL-remix-kit.md to build an app on this kit. Start a new app by copying template/ (every wiring trap pre-solved); showcase/ is the live site's source; demo/ is the deploy-log reference app the gate audits.

Why this exists

A four-model benchmark (docs/BENCH-RESULTS.md) showed that every defect cheap models ship lives in exactly the code this kit owns: leaked SSE heartbeats, dead EventSource wiring, unreadable contrast. Taste frozen into tested code beats taste described in prose — every failure pattern this kit absorbs is one no agent can ship again.

Install

npm install @kapable/remix-kit   # exact pin in package.json — no ^

Surface

Subpath Contents
/gallery renderGalleryHtml(options) — the full component gallery as one self-contained HTML document; serves the published artifact and the demo's chrome-free /kit route
/tokens Petrol theme (kapable-theme.css v0.7.1, lockstep-tested): themeStylesheet to inline in a Document, themeTokens css()-object for scoped embedding, typed t.* var refs, semantic status (pass/warn/fail/pending), fontFacesCss, opt-in printStylesheet (ink-on-paper; hides toasts/skip-link/dialogs), graphite second theme (graphiteStylesheet/graphiteTokens — same token contract, brand cobalt-indigo accent, AA-walked), signet third theme (signetStylesheet/signetTokens — warm-dark industrial, zero-radius, Archivo + IBM Plex Mono; the first theme to vary shape as well as color, via each theme's optional shape field and the exported ShapeTokens/SchemeTokens types)
/ui Card Btn IconButton SplitButton Badge Callout Field DateField DatePicker InputOTP FormSection FormActions SearchField TextareaField NumberField RangeField CheckboxGroup ToggleGroup ErrorSummary Dropzone UploadList TagInput PasswordField Checkbox Switch RadioGroup SegmentedControl ToggleButton Progress Spinner Tabs/TabList/Tab/TabPanel Accordion/AccordionItem Disclosure Breadcrumbs Pagination Dialog AlertDialog Drawer Popover Tooltip Toaster/toast Avatar AvatarGroup Identity Separator SideNav Toolbar Skeleton Sparkline Stat DescriptionList Timeline Time Steps Masthead PageShell CommandK EmptyState LivePill Kbd Table DataTable Select/Option Combobox/ComboboxOption ComboboxMulti Menu/MenuItem/Submenu/ContextMenu CommandPalette CopyButton CodeBlock DiffBlock ExternalLink SectionLabel + the xxxCss mixin tier and tested helpers (containerCss, lineClampCss, safeAreaPaddingCss, scrollbarGutterCss, textWrapCss, truncateCss, userInvalidCss, paginationRange, parseDiff, formatRelativeTime, formatCount, formatBytes, formatDuration, avatarHue…). Both color schemes, visible focus, reduced-motion, WCAG AA by construction
/realtime createLiveFeed() version-counter, sseResponse(feed, request) (retry hint, heartbeat, one shared cleanup on cancel+abort, enqueue-after-close guard), liveReloadIsland / LivePill client wiring
/data openDatabase(path|':memory:') (node:sqlite + migrations), loadDatabase(db) typed middleware, done(request, href) content-negotiated 303/204
/testing testRouter(router), formBody(fields), readSse(response) (cannot leak: every exit path cancels), expectExit() wedge guard
/config env field parsers and loadConfig(definition, source) — validates once at startup, aggregates redacted issues, separates and freezes public/server output
/request-context createAppContext(config) — typed Remix middleware providing a snapshotted plain-data context.config, per-request context.requestInfo, and generated x-request-id response headers

Sixty seconds of kit

// startup — parse once; only the public section may be serialized
import { env, loadConfig } from '@kapable/remix-kit/config'
const config = loadConfig({
  public: { environment: env.required('APP_ENV') },
  server: { port: env.default('PORT', env.integer({ min: 1, max: 65_535 }), 4470) },
}, process.env)

// request lifecycle — first router middleware; generated IDs are server-owned
import { createAppContext } from '@kapable/remix-kit/request-context'
const appContext = createAppContext(config)

// theme — one line in your Document <head>, both schemes, focus ring, reduced motion
import { themeStylesheet } from '@kapable/remix-kit/tokens'
<style>{themeStylesheet}</style>

// realtime — the whole SSE pattern
import { createLiveFeed, sseResponse } from '@kapable/remix-kit/realtime'
export const live = createLiveFeed()
//   in every mutation action:   live.bump(); return done(context.request, routes.home.href())
//   in the events action:       return sseResponse(live, context.request)
//   in the page:                <LivePill eventsHref={routes.x.events.href()} frame="board" />

// ui — one primary per view, machine-register badges, wired-for-you fields
<Btn type="submit" variant="primary">Record deploy</Btn>
<Badge tone="pass">pass</Badge>
<Field label="App" name="app" required error={error}>
  {(attrs) => <select id={attrs.id} name={attrs.name} mix={inputCss()}></select>}
</Field>

// tests — drive the real router; the runner is guaranteed to exit
const app = testRouter(router)
assert.equal((await app.post(routes.x.create.href(), { title: 'hi' })).status, 204)
after(() => expectExit())

Demo

Native-first component families

Family Public runtime exports Contract
Confirmation AlertDialog Native modal alert dialog; Cancel receives initial focus and app-owned action forms remain siblings.
Pending state Spinner CSS-only indeterminate indicator; pass label when it is the status announcement, omit it when decorative.
Glyph action IconButton Native square button with a required accessible label; supports external pending state.
Date input DateField, DatePicker Native date/datetime-local controls with Field wiring; DatePicker is the date convenience form, not a custom calendar.
Control strip Toolbar Named ARIA toolbar; native tab order without JS, roving arrow-key focus when rendered in an island.
Server table DataTable Data-driven Table; sorting stays in URLs and selection stays in native form fields.
Upload readout UploadList Presentational file status/progress list; transfer state and live updates remain app-owned.
Joined action SplitButton Native primary action joined to the kit Menu; the menu needs an island.
Joined choice ToggleGroup Native radio or checkbox fieldset for single or multiple form values; works without JS.
One-time code InputOTP One native text input, preserving paste, autofill, leading zeroes, and mobile keyboard hints.

demo/ is a real app built entirely from the kit — a deploy log with a validated form, a realtime table, status badges, and an empty state. It works with JavaScript disabled (blocking frames + 303 form flow) and syncs across tabs via SSE with JavaScript on.

cd demo && npm install && npm run dev   # http://localhost:4430  (PORT env to change)
npm test                                # router tests, in-memory sqlite

Gates

npm run typecheck && npm test           # kit — suite must be green AND exit
cd demo && npm run typecheck && npm test
node scripts/verify-browser.mjs         # full gate: axe, contrast, reflow, focus, forced colors, JS-off, SSE, and island behavior
node --import remix/node-tsx scripts/verify-nontext-boundary.tsx   # WCAG 1.4.11 control boundaries, RENDERED

The non-text contrast gate (verify-nontext-boundary.tsx)

WCAG 1.4.11: a boundary you must perceive to operate a control needs 3:1. This gate renders the kit's own components under all three themes in real Chromium and measures what the cascade actually resolves — 3 themes × 2 schemes × 10 controls × rest/hover = 120 cells. It exits non-zero on any failure. Not part of npm test (it boots a browser), same convention as verify-browser.mjs. Playwright resolves from the workspace-level node_modules; nothing to install.

Why it exists: the token matrix in tests/tokens.test.ts compares two token strings and cannot see which token lands on which element. It was green while five controls were invisible in every theme — including signet, whose own test is named "signet has ZERO non-text failures".

Reading its output:

  • border / fill / none — the mechanism that actually draws the edge. fill means the control has no border and its own background is its boundary (Switch). none means nothing is drawn at rest (ToggleButton's ghost variant) and is excluded from pass/fail, with a note.
  • NOT MEASURED by this instrument — printed every run. Read it. A control listed there has no measured number, which is not the same as passing.
  • Colour resolution is canvas pixel readback, not string parsing, so rgba() boundaries are measured as painted and the gate survives a move to OKLCH. It calibrates itself (white-on-white 1.00, black-on-white 21.00) before every sweep and throws on an unresolvable colour rather than skipping a row.

Adding a control: add a specimen to the specimenTree with a data-control="…" wrapper, then an entry in CONTROLS. EXPECTED_TOTAL derives from CONTROLS.length, so the pinned cell count follows automatically.

UA shadow pseudo-elements cannot be measured this way. Chromium does not expose ::-webkit-slider-runnable-track and friends to getComputedStyle — it silently returns the host element's styles, so a plainly-bordered control reports none and the gate prints PASSED having measured nothing. Author pseudos (::before) resolve fine. Anything hitting this belongs in UNMEASURABLE with a source-level assertion instead; sliderCss is the worked example.

Control-boundary tokens

Three OPTIONAL tokens, each with a CSS fallback, so a theme that omits them renders byte-identically:

token falls back to use
--kp-border-control --kp-border resting edge of an interactive control
--kp-border-control-hover --kp-border-strong the same edge on hover
--kp-control-track --kp-border-strong the FILL of a control whose body is its boundary (Switch track)

All three themes set all three. Reach for them on buttons, inputs, checkboxes, radios, selects, segmented controls, chips and sliders — never on card, table, grid, divider or badge borders, which are deliberately near-invisible and outside 1.4.11's scope.

Deriving a value for a new theme: seed from that theme's own --kp-border (or --kp-border-strong for a track), preserve hue and saturation exactly, step lightness only, until the worst contrast against --kp-bg, --kp-surface, --kp-surface-hi and --kp-surface-sunken clears 3:1. Hover applies the theme's own border → border-strong lightness delta, so light schemes step darker and dark schemes step lighter. ⚠ --kp-surface-sunken is darker than --kp-surface-hi in light schemes and is often the binding surface — checking only the first three silently ships a 2.96.

Docs

  • skills/SKILL-remix-kit.md — build an app on the kit (start here)
  • skills/SKILL-remix-kit-maintainer.md — evolve the kit without breaking frozen apps
  • docs/design/UI-API.md — the reviewed component contract
  • docs/BRAND.md, docs/reference/ — brand doctrine and judged reference code

Do not publish to npm; the anchor publishes after review.

Dependencies

Dependencies

ID Version
typescript5 npm:typescript@5.9.3

Development dependencies

ID Version
@types/node ^26.1.1
axe-core 4.12.1
playwright ^1.61.1
remix 3.0.0-beta.5
typescript 7.0.2

Peer dependencies

ID Version
remix 3.0.0-beta.5
Details
npm
2026-07-26 16:24:38 +00:00
4
UNLICENSED
latest
197 KiB
Assets (1)
Versions (3) View all
0.3.0 2026-07-26
0.1.1 2026-07-21
0.1.0 2026-07-21