Skip to content
sailscastshqPublic

Repository files navigation

Klean UI

Klean UI means Kelvin's Lean UI: source-owned, accessible UI for The Boring JavaScript Stack. It gives Vue, React, and Svelte applications framework-native components with neutral defaults, ordinary Tailwind styling, and Durable UI patterns. The source belongs to the application as soon as it is added.

Add a component

Run the same command in every standard Boring Stack application:

npx klean-ui add button
npx klean-ui add input
npx klean-ui add textarea
npx klean-ui add rich-text
npx klean-ui add checkbox
npx klean-ui add radio
npx klean-ui add switch
npx klean-ui add separator
npx klean-ui add popover
npx klean-ui add menu
npx klean-ui add select
npx klean-ui add dialog
npx klean-ui add toast
npx klean-ui add durable-ui

Klean detects Sails and the frontend framework from package.json and the conventional application entry. It then installs the selected framework-native registry item and its prerequisites. Button is one source file:

Vue      assets/js/components/ui/button/Button.vue
React    assets/js/components/ui/button/Button.jsx
Svelte   assets/js/components/ui/button/Button.svelte

Multi-file components remain conventional. Toast installs the detected renderer and one framework-neutral controller beside it:

Vue      assets/js/components/ui/toast/Toast.vue
React    assets/js/components/ui/toast/Toast.jsx
Svelte   assets/js/components/ui/toast/Toast.svelte
All      assets/js/components/ui/toast/toast.js

There is no init, klean-ui.json, framework questionnaire, alias questionnaire, generated cn.js, or Klean runtime dependency. Each installed component is ordinary application code. Its visual API is class in Vue and Svelte or className in React, and caller Tailwind classes win.

Preview the resolved work without changing the application:

npx klean-ui add button --dry-run

Nonstandard Boring Stack applications can use explicit path overrides:

npx klean-ui add button \
  --components-dir assets/js/design-system \
  --css assets/styles/app.css

Klean will not silently replace edited source. Re-running an unchanged installation is a no-op; edited files produce a useful conflict and require the deliberate --overwrite flag.

Read the complete installation guide.

Give your coding agent the Klean UI skill

Install the project-level skill from the Klean UI repository:

npx skills add sailscastshq/klean-ui --skill klean-ui

Then invoke $klean-ui when building, migrating, or reviewing an interface. The skill teaches the agent Klean's source-ownership, Tailwind, native-platform, accessibility, and Durable UI decisions; the canonical component APIs remain in the documentation. Add -g to the installation command only when you want the skill available across projects.

Add the resilience layer

Durable UI is Klean source, not a second runtime package. One zero-configuration command adds the framework-native utilities and their small browser core:

npx klean-ui add durable-ui

Vue and React receive idiomatic useStoredState, useQueryState, useFormDraft, useWizardDraft, useScrollRestoration, useOptimistic, and useSearch APIs. Svelte receives the equivalent create… rune APIs. Defaults are SSR-safe and use namespaced, versioned storage; URL defaults stay out of the address; drafts expire and restore deliberately; scroll is session-scoped; optimistic work rolls back; and stale searches are aborted. No provider, initializer, manifest, or Klean state runtime is introduced.

Update owned source safely

Copied source still belongs to the application. Klean therefore inspects before it writes:

npx klean-ui check
npx klean-ui diff button
npx klean-ui update button
npx klean-ui update --all

check distinguishes current source, safely replaceable historical Klean source, local edits, and untracked files. diff is read-only. update replaces only an exact known Klean revision and applies component files, direct dependency changes, and lockfile changes as one rollback-safe transaction. Locally edited or unknown source is never replaced unless the caller deliberately passes --overwrite.

No application manifest is created. Revision hashes and migration notes stay inside the versioned CLI registry. Read the update guide.

Native form controls

Klean installs Input and Textarea independently. The application writes the real label, help, and error elements so the form remains obvious HTML instead of a configuration API.

<div class="grid gap-2">
  <label for="email">Email address</label>
  <Input
    id="email"
    v-model="form.email"
    name="email"
    type="email"
    autocomplete="email"
    required
    :aria-invalid="Boolean(form.errors.email)"
    aria-describedby="email-help email-error"
  />
  <p id="email-help">We only use this for account messages.</p>
  <p id="email-error" class="empty:hidden text-sm text-red-700">
    {{ form.errors.email }}
  </p>
</div>

aria-invalid="false" is valid, and the stable empty error contributes no description. When an error appears, the same relationship becomes useful without conditional IDs or a Klean helper. Validation, IDs, messages, and values remain application-owned. Input and Textarea forward native attributes, while visual density, color, shape, and layout remain ordinary caller Tailwind classes. Read the canonical Input documentation.

Button contract

Button has behavioral inputs only: rendered element, native button type, and disabled semantics. Actions remain buttons; navigation renders a native anchor or the Boring Stack/Inertia Link component. Loading labels and product styling remain application concerns.

<script setup>
import Button from "@/components/ui/button/Button.vue";
</script>

<template>
  <Button
    type="submit"
    :disabled="form.processing"
    class="rounded-full bg-emerald-700 px-6 hover:bg-emerald-800"
  >
    {{ form.processing ? "Saving" : "Save changes" }}
  </Button>
</template>

There is intentionally no variant, size, tone, color, radius, or generated class-helper API. Repeated visual treatments become application-owned components styled with Tailwind.

Component workbench

npm install
npm run storybook

Storybook opens one neutral workbench at http://localhost:6006. Its sidebar composes three framework-native Storybooks:

6006  Klean UI workbench
6007  Vue
6008  React
6009  Svelte

Open Vue, React, or Svelte, then choose Components / Button, Input, Textarea, Checkbox, Radio, Switch, Spinner, Tooltip, Tabs, Popover, Menu, Select, Combobox, Command, Dialog, Separator, Slide, or Toast. The stories mount the actual registry source and share behavior expectations without sharing a runtime component abstraction. Controls stay limited to useful behavioral inputs; interaction stories and the accessibility addon verify the durable contract.

npm run build-storybook produces the same composition as one static artifact: the neutral shell at storybook-static, with each renderer nested below it.

Validate Klean UI

npm test
npm run build
npm run build-storybook
npm pack --dry-run

The package contains the CLI and its versioned registry. It does not publish the Storybook, development build, or framework runtimes.

Doctrine

Klean is the canonical implementation of our Durable UI practice. Durable behavior lives in the component, composable, or Boring Stack block that owns it; it does not turn every primitive into a state-management abstraction.

The canonical documentation lives at docs.sailscasts.com/klean-ui, including the doctrine, installation guide, Durable UI contract, theming convention, and every component page.

Flag

npx klean-ui add flag installs framework-native Flag and its local asset registry. country accepts a case-insensitive two-letter code (surrounding whitespace is ignored). A nonempty src takes precedence. Omitted alt derives a stable English country name: <Flag country="NG" /> has alt="Nigeria". An explicit string always wins, including alt="" for decorative flags beside country text or alt="Based in Nigeria" for application meaning. alt remains a string, not a Boolean switch. Native image attributes and error listeners pass through. Missing, invalid or failed images render a fallback span, hidden from assistive technology when decorative, named by alt when informative. Vue/Svelte slots or React children own fallback content. Changing the resolved source retries.

<Flag country="NG" class="w-5" />
<Flag country="KE" class="w-8" />
<Flag country="NG" alt="Based in Nigeria" />
<Flag country="NG" alt="" />
<!-- Decorative beside country text -->
<Flag country="GH" class="size-10 aspect-square rounded-full" />
<Flag src="/flags/organization.svg" alt="Organization" class="w-12" />

Default geometry is 24px wide, 3:2. Use w-5, w-6, w-8, w-10, w-12 for 20/24/32/40/48px. A circle uses aspect-square rounded-full and a width or size class. Klean deliberately has no size or circle presentation props. React uses className; Svelte uses class.

Assets, coverage and performance

Original component behavior; no Flux implementation source is used. Flag art comes from country-flag-icons 1.6.20, npm tarball SHA-1 aa6f36104568993f9cd43e7da283f9b7d0a802cc. The full MIT license travels in flags.js, including installed application source. The fixed registry has 257 entries: 249 ISO 3166-1 codes plus AC, EU, IC, TA, XA, XC, XK, XO supplied by this asset version. This is asset coverage, not an assertion of political status or a Unicode RGI registry. countries is exported from the installed flags.js; applications own the eligible picker list and localized country names. countryName(country) exports the fixed English lookup. Names are checked in alongside the asset registry, with no render-time Intl.DisplayNames, browser language lookup, or locale prop. The same code yields the same default name during SSR and hydration. Override alt with the application's localized label. Unknown or missing codes derive ""; a custom source without a known country must receive a meaningful explicit alt or alt="" for deliberate decoration. Any nonempty custom src disables inferred naming even when country is also supplied: the source could depict anything. Only an explicit alt labels a custom image. XA/XC/XO names follow this asset package (Abkhazia/Northern Cyprus/South Ossetia), not Unicode's unrelated pseudo-region assignments.

SVGs are encoded as data URLs in one application-owned module (~247KB before minification/compression), avoiding asset-host setup and all built-in flag network requests. The complete registry is eagerly included when Flag is imported. Lazy-load a country picker where appropriate; a product using only one flag can use a local src and trim its owned registry. Allow data: in CSP img-src. No asset dependency or tracker runs at render time. A custom src follows the application's network/CSP/privacy policy. Flag depicts an explicit chosen region; never infer ethnicity or nationality from identity.

Releases

Packages

Contributors

Languages