@kapable/remix-kit (0.3.0)
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.fillmeans the control has no border and its own background is its boundary (Switch).nonemeans 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 appsdocs/design/UI-API.md— the reviewed component contractdocs/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 |