npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

bank20baht-ui

v0.1.2

Published

JSON-driven form + workflow engine. Send JSON in, get an interactive form out.

Readme

bahtui

JSON in, interactive form out. A framework-neutral form + workflow engine shipped as Web Components (Lit, light DOM) with a pure-TypeScript rules core.

npm i bank20baht-ui

Quick start — one form (Level 1)

<script type="module">
  import 'bank20baht-ui';            // registers <baht-form> / <baht-workflow>
</script>
<link rel="stylesheet" href="bank20baht-ui/tokens.css" />

<baht-form id="f"></baht-form>
<script>
  const el = document.getElementById('f');
  el.form = {
    formId: 'f1',
    formName: 'Person',
    field_data: [
      { order: 1, type: 'text',   key: 'firstName', label: 'First name', width: '1/2', required: true, value: '' },
      { order: 2, type: 'text',   key: 'lastName',  label: 'Last name',  width: '1/2', required: true, value: '' },
      { order: 3, type: 'number', key: 'age',       label: 'Age',        width: '1/4', min: 0, value: null }
    ]
  };
  el.addEventListener('baht-change', (e) => console.log(e.detail.data));
  // el.validate() / el.getData() / el.setValue({...}) / el.reset()
</script>

<baht-form> is a dumb controlled component: props in → UI → baht-change out. No buttons, no cross-field logic, no fetch. Your app owns the state (mutate the JSON, feed it back).

Views — show a subset of one form

Define named display projections on the schema (e.g. per user role); select one with the view attribute:

el.form = {
  ...form,
  views: [{ name: 'summary', fields: ['age', 'firstName'] }], // view order = display order
};
el.view = 'summary'; // or <baht-form view="summary">; unset = full form

Views are client-side display only: the view picks which fields render (and validate — the user can only fix what they see), but getData() still returns the full form and the server (bank20baht-validator) always validates the complete schema. In a workflow, set view: 'summary' on a step's form to pin the view that step renders.

Repeater — line items / repeating rows

{ order: 5, type: 'repeater', key: 'items', label: 'Line items', minRows: 1,
  fields: [
    { order: 1, type: 'text',   key: 'name', width: '1/2', required: true },
    { order: 2, type: 'number', key: 'qty',  width: '1/4', min: 1 },
  ] }

Value is an array of row objects (getData().items → [{ name, qty }, …]). Rows add/remove in the UI (minRows/maxRows bound the count), each row validates its sub-fields — errors come back as items[0].name, on both the client and bank20baht-validator. One level deep only.

Dependent / async dropdowns

// schema — functions never live in JSON, only registry names:
{ order: 2, type: 'dropdown', key: 'amphoe', optionsFrom: 'amphoeFor', optionsDeps: ['province'] }

// registry:
el.registry = {
  options: { amphoeFor: (data) => fetch(`/api/amphoe?p=${data.province}`).then(r => r.json()) },
};

Options resolve from the registry with the form's current data (sync or Promise). When a dep changes, options refetch and the dependent value clears — cascading down chains (จังหวัด → อำเภอ → ตำบล).

Cross-field validation

{ order: 2, type: 'date', key: 'end',
  validators: [{ type: 'compare', op: 'gte', field: 'start' }] }

compare checks against a sibling field (confirm email = op: 'eq'), numeric/date-aware, skips while either side is empty. Works inside repeater rows and on the server.

Feature flags — deploy dark, flip a JSON file

// schema: gate a field / workflow step / rule
{ order: 9, type: 'text', key: 'promoCode', flag: 'promo' }   // '!promo' = the old path

// host: flags from one static file — edit it, features flip
el.flags = await fetch('/flags.json').then((r) => r.json());  // { "promo": true }

Flag-off means not in the schema: no render, no validation (a dark required can't block), nothing in the payload. Missing keys count as off, so new features stay dark until the file turns them on. Server side: validateSubmission(applyFlags(schema, flags), data) with the same JSON — bank20baht-validator re-exports applyFlags.

File upload

// schema:
{ order: 6, type: 'file', key: 'doc', uploader: 'default', accept: '.pdf,image/*', maxSize: 10_485_760 }

// registry — File in, serializable ref out:
el.registry = {
  uploaders: { default: async (file) => {
    const fd = new FormData(); fd.append('file', file);
    return (await fetch('/api/uploads', { method: 'POST', body: fd })).json();
  } },
};

Picking a file uploads immediately; the value becomes { name, size, type, url } — plain JSON, chips with remove buttons in the UI, maxSize/accept validated on both client and server. Omit uploader for the old capture-only mode.

Drafts / autosave (host pattern)

The library is controlled — autosave is three lines in your app:

el.addEventListener('baht-change', (e) => localStorage.setItem(key, JSON.stringify(e.detail.data)));
// (workflow: listen to baht-data-change instead)
el.value = JSON.parse(localStorage.getItem(key) ?? '{}'); // restore on mount
// clear the key after the server accepts the submission

See apps/web's render page for a working restore/discard/clear-on-submit flow.

i18n — Thai (or any) built-in strings

All built-in strings — validation messages, Next/Submit/Back, the approval step — come from one catalog:

import { setLocale } from 'bank20baht-ui';

setLocale('th'); // built-ins: 'en' (default), 'th'
setLocale('th', { required: 'ห้ามว่าง' }); // locale + your own overrides

Call it once before mounting. Per-field message and label attributes (next-label, approval.approveLabel, …) still win over the catalog. The server can localize too: bank20baht-validator re-exports setLocale, so validation issues come back in the same language the client shows.

Quick start — wizard with rules (Level 2)

document.querySelector('baht-workflow').workflow = {
  workflowId: 'wf-1',
  forms: [/* {formId, formName, order, field_data}, ... */],
  rules: [
    { id: 'skip-person',
      when: { field: 'Intro.kind', op: 'eq', value: 'company' },
      then: [{ action: 'skipForm', target: { form: 'Person' } }] },
    { id: 'vat',
      when: { field: 'Company.hasVat', op: 'eq', value: true },
      then: [{ action: 'show', target: { form: 'Company', field: 'vatId' } },
             { action: 'required', target: { form: 'Company', field: 'vatId' } }],
      else: [{ action: 'hide', target: { form: 'Company', field: 'vatId' } }] }
  ]
};

<baht-workflow> walks the forms in order, evaluates the rules on every change (show/hide/enable/disable/readonly/required/setValue/skipForm…), renders the single Next→Submit button, and emits baht-next / baht-submit / baht-complete / baht-data-change / baht-step-change. The runtime navigates; your app acts.

Headless core (any framework, no DOM)

import { evaluate, validateField } from 'bank20baht-ui/engine';

const derived = evaluate(workflow, { Intro: { kind: 'company' } });
// derived.forms  -> { Person: { visible: false }, ... }
// derived.fields -> { 'Company.vatId': { visible, enabled, readonly, required, computed } }
// derived.data   -> nested output (hidden/skipped values cleared + excluded)

The engine is a pure function with fixpoint evaluation (cycle-guarded), per-rule priority conflict resolution, and clear-on-hide/skip semantics. Build a React/Vue renderer on top of it without touching the Web Components.

Functions never travel in JSON

Formatters and custom validators are referenced by name and resolved from a registry you pass as a prop:

el.registry = {
  formatters: { uppercase: (v) => String(v).toUpperCase() },
  validators: { idCardTH: (v) => isThaiId(v) ? null : 'บัตรประชาชนไม่ถูกต้อง' }
};
// JSON side: { formatter: 'uppercase', validators: [{ type: 'custom', fn: 'idCardTH' }] }

Design system — 20baht

Ships themed as the 20baht design system: banknote green (#1F7A4E), cream paper background, Bricolage Grotesque / Geist / Geist Mono, borders-over-shadows, "paper" easing. 35+ components:

<baht-button variant="secondary" size="sm">Cancel</baht-button>
<baht-badge tone="success" label="Running"></baht-badge>
<baht-table></baht-table>  <baht-modal></baht-modal>  <baht-toast></baht-toast>  …
  • Data (server-side paging)baht-table · baht-list · baht-pagination · baht-filter. items is one page; baht-page-change asks your app to fetch the next one.
  • Feedbackbaht-modal · baht-toast · baht-alert · baht-tooltip · baht-progress · baht-spinner · baht-skeleton
  • Inputsbaht-rating · baht-range · baht-otp (+ the 11 form field primitives)
  • Structure & navbaht-card · baht-fieldset · baht-timeline · baht-accordion · baht-link · baht-breadcrumbs

Theming — design tokens + light DOM

Everything renders in light DOM and reads --baht-* CSS custom properties — override at build time, at runtime, or inject your own sheet entirely:

.my-app {
  --baht-color-primary: #047857;
  --baht-radius-md: 6px;
}
.my-app .baht-label { text-transform: uppercase; }  /* your CSS reaches every node */

Styling API: .baht-form, .baht-field, .baht-field--w-1-2, .baht-field--hidden, .baht-label, .baht-control, .baht-error, .baht-button, .baht-badge (+ the token list in tokens.css). Full reference: docs/theming.md.

One install, typed everywhere

npm i bank20baht-ui ships the runtime and the metadata every editor/framework needs — no hand-written type declarations in your app:

| You use | What you get | Setup | |---|---|---| | JetBrains / WebStorm (any framework) | autocomplete + docs for every <baht-*> tag | none — web-types is auto-detected | | VS Code (HTML / Angular templates) | tag + attribute completion | one line, see below | | React (TSX) | full JSX typings, typed props per element | import 'bank20baht-ui/react' once | | Angular | template binding on properties/events | CUSTOM_ELEMENTS_SCHEMA, see below | | Vue 3 | keep Vue from treating baht-* as components | isCustomElement, see below | | Phoenix / Rails / plain HTML | it's just HTML — works as-is | none | | Tooling (Storybook, docgen, custom) | machine-readable API of all 36 elements | read dist/custom-elements.json (CEM standard) |

VS Code.vscode/settings.json:

{ "html.customData": ["./node_modules/bahtui/dist/vscode.html-custom-data.json"] }

React — once, in your app entry (or load it globally without a runtime import via "types": ["bank20baht-ui/react"] in tsconfig.json):

import 'bank20baht-ui/react'; // JSX typings for every <baht-*> element

React 19+ sets non-string JSX props as DOM properties, so object props (form, workflow, items, …) work inline; on React 18 set those via a ref.

Angular — the compiler must be told baht-* tags are custom elements:

@Component({ schemas: [CUSTOM_ELEMENTS_SCHEMA], /* … */ })

Vue 3vite.config.ts:

vue({ template: { compilerOptions: { isCustomElement: (tag) => tag.startsWith('baht-') } } })

Docs, Storybook, examples

  • Per-component docs: docs/ — one .md per component + framework guides (vanilla / React / Angular).
  • Storybook: pnpm --filter storybook-bahtui dev → http://localhost:6006.
  • Copy-out starters: examples/vanilla, examples/react, examples/angular at the repo root — same workflow JSON in three hosts.

Field types (12)

text · textarea · number · email · dropdown · multiselect · checkbox · radio · toggle · date · file (capture only, no upload) · label (static)

Each field descriptor: { order, type, key, label, placeholder, width: '1/4'|'1/2'|'3/4'|'full', value, disabled, required, options, maxLength, min, max, validators[], formatter }.

Visual Builder

The playground app ships a two-mode Visual Builder (Form mode: palette/WYSIWYG canvas/properties; Workflow mode: sequence + rules with a live preview running the real <baht-workflow>), with Import/Export JSON. Run it from the monorepo: pnpm --filter playground dev/builder.