Skip to content

Headless core ​

Every stateful component is a thin binding over a framework-agnostic state machine in @thl-ui/core. You can use them directly to build your own components — in any framework, or none.

Store ​

All machines expose a tiny observable store:

ts
interface Store<T> {
  get(): T;
  set(next: T | ((prev: T) => T)): void;
  subscribe(fn: (value: T, prev: T) => void): () => void;
}

Bridging is one line per framework:

ts
// React
const state = useStore(machine.state);              // exported by @thl-ui/react
// Vue
const state = useStore(machine.state);              // exported by @thl-ui/vue → Ref<T>
// Angular
readonly state = storeSignal(machine.state);         // exported by @thl-ui/angular → Signal<T>

Machines ​

FactoryHighlights
createTable({ data, columns, pageSize, rowKey, manual })sort, search, filters, paging, selection, inline edit, server mode
createSelect({ options, multiple, searchable, creatable })keyboard navigation, groups, creatable tags
createTabs({ items, active })roving tabindex, disabled skip
createSteps({ count, current, linear })status per step
createCarousel({ count, interval, loop })autoplay, pause
createDisclosure() / createAccordion()collapse, panels
createSidebar({ breakpoint })mini / mobile / user-mini state, resize aware
createToastManager({ duration, max })positions, sticky, max queue
createUpload({ accept, maxSize, request })validation (i18n), progress, previews
createIdleTimer({ warnAfter, expireAfter })activity events
paginate({ page, total, pageSize, maxPages })page items with ellipsis
monthGrid(), weekdayNames(), presetRanges()Intl-aware calendars
rules.*, validate(), validateAll()returns i18n keys

Example: a custom table in vanilla TS ​

ts
import { createTable, createThl } from '@thl-ui/core';

const thl = createThl({ locale: 'en' });
const table = createTable({
  data: employees,
  columns: [{ key: 'name', sortable: true }, { key: 'salary', sortable: true }],
  pageSize: 5,
  rowKey: 'id',
});

table.state.subscribe(render);
table.toggleSort('salary');
table.setSearch('tokyo');

function render() {
  const { rows, total, page, pageCount } = table.view();
  footer.textContent = thl.t('pagination.showing', { from: (page - 1) * 5 + 1, to: Math.min(page * 5, total), total });
  // …render rows.map(r => r.row)
}

Validation rules ​

ts
import { rules, validate, validateAll } from '@thl-ui/core';

validate('', [rules.required()]);                       // { key: 'validation.required' }
validate('ab', [rules.minLength(3)]);                   // { key: 'validation.minLength', params: { min: 3 } }
validateAll({ pw: 'x', confirm: 'y' }, { confirm: [rules.match('pw')] });

Available: required, email, minLength, maxLength, min, max, number, pattern(re), match(field). A rule is just (value, allValues) => { key, params } | null, so custom rules are trivial — and their messages are translated like built-ins.

Utilities ​

bem(block, prefix) · cx(...classes) · uid() · focusTrap(el) · scrollLock() · clickOutside(els, fn) · computePosition(anchor, float, placement) · breakpoints · skins · palette.