The kit
A copy-paste component kit for Tuimark - design tokens, k- component classes and 21 small documents you copy into your project and own, with a gallery app that shows them all.
The kit is a set of ready-made components for Tuimark: buttons, cards, alerts, a data table, a command palette, dialogs, toasts, and more. It follows the shadcn way: you do not install it, you copy what you need into your project and own it. There is nothing to import and nothing to keep in sync; if a card should look different, you edit your copy.
The kit adds no tag, attribute, or CSS property to the language. A component is ordinary .tui markup plus class names, ids, and action names that follow a few conventions, styled by two stylesheets. It lives in the repository's kit/ directory.
| File | Holds |
|---|---|
kit/theme.tcss | the design tokens, dark and light, and the base typography |
kit/kit.tcss | the component classes |
kit/components/NAME.tui | one small, valid document per component, with the markup to copy and a NAME.sample.json beside it when it binds data |
examples/kit-gallery | every component in one app, on five tabs, with a Go host |
Using it
Copy theme.tcss and kit.tcss into your project and load them first, then your own sheet last. There is no @import in TCSS, so each document lists the sheets it uses:
<tui theme="auto">
<style src="theme.tcss"/>
<style src="kit.tcss"/>
<style src="app.tcss"/>
<screen>
<col width="1fr" height="1fr" pad="1">
<box class="k-card" title="Deploy">
<text class="k-card-title">api-gateway 2.14.0</text>
<row class="k-card-actions">
<button id="deploy__go" class="k-btn primary" label="Deploy" on:click="button_deploy"/>
<button id="deploy__skip" class="k-btn ghost" label="Not now" on:click="button_skip"/>
</row>
</box>
</col>
</screen>
</tui>The sheets are cascaded together, and where two rules tie the later one wins. That is how you rebrand the kit: every color is a token in :root, so your own sheet only re-declares the ones you want to change.
/* app.tcss, loaded after theme.tcss and kit.tcss */
:root { --accent: #e8590c; --focus: #e8590c; }
@media (theme: light) {
:root { --accent: #c2410c; --focus: #c2410c; }
}Every document in the kit validates on its own, so you can try a component before copying it:
tuimark validate kit/components/table.tui --data kit/components/table.sample.json
tuimark run kit/components/table.tui --data kit/components/table.sample.jsonEach component file starts with a contract, an XML comment that says what it needs from your store and your host:
k-NAME · Contract: <store paths it reads and writes> · Actions: <named actions> · Keys: <keys> · Requires: <other kit classes>Tokens
All color lives in :root of theme.tcss, read as $name. The first ten names are the runtime's own required tokens (the same names its built-in themes define); the kit adds the rest.
| Token | Use |
|---|---|
bg fg muted border panel | the screen, body text, secondary text, idle borders, cards and modals |
accent focus | the brand color, and the border or fill of the focused thing |
ok warn danger info | status colors |
subtle raised sel | tertiary text, raised or inset fills, the cursor row of an unfocused list |
on-accent on-ok on-warn on-danger | text drawn on a filled color |
code | inline code |
Selection and focus never rely on a background color alone. A cursor row is also bold, a focused list or table gets a double border and a solid accent bar, and a focused button is inverted and underlined, so all of it still reads under 16 colors.
Conventions
Classes. Every kit class starts with k-: k-card for a component and k-card-title for a part of it. (TCSS has no descendant combinator, only >, so a part cannot be styled as "text inside a card"; each part carries its own class.) A modifier is a second, short class on the same node, such as primary, ok, warn, danger, and it is only ever used next to its component class: k-btn danger, k-badge ok, k-alert warn. Pick a modifier from your data with class:NAME="path" guards.
Ids are instance__part, with two underscores: confirm__dialog, confirm__yes, confirm__no, profile__email. A node that is the whole component keeps the instance name (svc for a table), and so do the singletons of your app (app, status, nav).
Actions are component_verb, a plain identifier: dialog_confirm, palette_run, toast_show, form_submit, table_select. Generic names such as cancel or save are avoided. An action does not say which instance fired it; a host routes by the prefix of the event's source before the last __, so confirm__yes belongs to the instance confirm, and one handler serves every dialog.
Overlays (dialogs, the palette, the help) are modals that close on esc, write their own size, and declare their keys in a keymap scoped to their own class, listed before any global keymap:
<keymap when=".k-dialog">
<bind keys="y" action="dialog_confirm" label="confirm"/>
<bind keys="n" action="dialog_cancel" label="cancel"/>
</keymap>Sizes are explicit. A component that fills a space says so in its class, and a tabs writes focusable, so nothing depends on the default size of a bare row, col, or modal.
Components
| Component | Class | In one line |
|---|---|---|
| badge | k-badge | a short status label, with a modifier for its color |
| button | k-btn | primary, secondary, danger and ghost, inverted when focused |
| card | k-card | a bordered panel with a title that takes the focus color while it holds focus |
| alert | k-alert | a callout with an icon, in four status colors |
| app bar | k-appbar | a one-row bar docked to the top |
| status bar | k-statusbar | a one-row bar docked to the bottom, with filled segments |
| key hints | k-hints | the keymap's labeled rows as a row of hints |
| confirm dialog | k-dialog | a modal that puts the cursor on the safe button |
| form field | k-field | a label, an input, help text and an error line, with invalid styling |
| empty state | k-empty | an icon, a title, a line of help and a call to action, centered |
| data table | k-table | a table with a header, right-aligned numbers and columns that hide by priority |
| menu | k-menu | a navigation list with icons, shortcuts and optional check marks |
| tabs | k-tabs | a tab strip bound to a path |
| labeled progress | k-meter | a label, a value and a bar in eighths |
| stat | k-stat | a KPI card with a change and a sparkline |
| toast | k-toast | a one-row notice that never moves the page |
| command palette | k-palette | a modal with an input over a filtered list |
| help overlay | k-help | a modal that lists every labeled key |
| split pane | k-split | a list beside a detail card, stacked on a narrow terminal |
| log panel | k-log | a scrolling log that follows its end |
| breadcrumb | k-breadcrumb | the current path, down to the last crumb on a narrow terminal |
Markup has no expressions, so a few components lean on the host: the palette's filtered list and the split pane's detail are computed with Derive, and a toast is cleared by an Every tick. The contract comment of each file says which, and the helpers below do the common ones for you.
Helpers
A component is markup, and markup has no expressions. Expiring a toast, filtering the palette, validating a form and routing a dialog's answer have to happen in the host. The kit ships that code as helpers: small files in Go and in TypeScript that write the store paths the components read. Like the components, you copy them and own them. They are not a package: kit/go is a nested module named tuimarkkit, with no dot in its name so that go get cannot fetch it, and kit/ts has no published name.
To use one, copy its file into your project. In Go, change package kit to your package; a helper imports only github.com/abdul-hamid-achik/tuimark. In TypeScript, copy the .ts file next to your parent; it imports only types from @thelacanians/tuimark. A file that needs another says so in its header: dialog and form need route, and palette needs filter. Each helper has a test beside it. The Go tests of toasts, form, dialog, palette and sortpage load the real component documents and drive them; the others, and the TypeScript tests, run against the store alone.
| Helper | What it does | Writes |
|---|---|---|
route | Instance(source) and Part(source): the two halves of an instance__part id | nothing |
toasts | Push(kind, text, ttl) into a capped queue that shows one toast at a time and expires them | toast.show, .text, .is_ok, .is_warn, .is_danger |
filter | substring or fuzzy match over an array of objects, by named fields | {items, any, count, total} at a path you choose |
sortpage | cycle the sort of a column (with an arrow in its title) and paginate (1–50 of 340) | the sorted rows of the page, and a meta object |
form | per-field rules (required, pattern, length, your own function) on change and on submit | forms.<form>.<field>.error, forms.<form>.valid |
dialog | Confirm(instance, title, body, onConfirm) opens a k-dialog and routes dialog_confirm / dialog_cancel by instance | <instance>.open, .title, .message, .result |
palette | a registry of commands (title, key hint, function) behind the palette | palette.commands, found, palette.open, .cur, .last |
async | Load(path, fn) runs slow work off the handler | <path>.loading, .error, .data |
format | Bytes, Duration, Relative, Percent, Thousands | strings ready for {path} |
The Go API has Derive and Every and the TypeScript host does not, so two things differ. In Go, filter and sortpage are derived values that follow every store write, and a toast expires on an Every tick. In TypeScript you call recompute() after a write, the palette's input fires palette_query on every change so the parent can, and toasts.start() runs the tick on a timer.
A toast queue, in Go:
ts, _ := kit.NewToasts(app, kit.ToastOptions{Max: 5, TTL: 3 * time.Second})
ts.Register() // toast_dismiss (esc)
stop := ts.Start(time.Second) // app.Every(time.Second, ts.Tick)
defer stop()
ts.Push("ok", "Settings saved", 0) // info, ok, warn or danger; 0 is the default time to liveThe same, in TypeScript:
const toasts = createToasts(ui, { max: 5, ttl: 3000 });
toasts.register();
const stop = toasts.start(500);
await toasts.push("ok", "Settings saved");A form named by the id prefix of its inputs (profile__name, profile__email), in Go:
forms := kit.NewForms(app)
forms.Add("profile").
Field("name", kit.Required("Name is required.")).
Field("email", kit.Required("Email is required."), kit.Pattern(emailRE, "Enter a full address.")).
OnSubmit(func(v map[string]string) error { return save(v) }).
Reset(nil) // creates forms.profile.* in the store
forms.Register() // form_validate, form_submitA rule is a function from the value to the message to show ("" when it is fine), so a custom rule is a plain function. Submitting shows every error and moves focus to the first invalid input.
Sorting by clicking a column header (<column on:click="table_sort">, with title="{svc_meta.titles.name}" so the arrow shows) needs the mouse on (<tui mouse="…">); for the keyboard, bind keymap rows to your own action that cycles the column.
The gallery
examples/kit-gallery shows every component together, with a Go host that implements the actions and routes them by instance. See it on the Examples page, or run the view alone with fixed answers for every action:
tuimark run examples/kit-gallery/app.tui --data examples/kit-gallery/sample.json --mock examples/kit-gallery/mock.jsonThe mock opens and closes the dialogs and shows toasts; filtering the palette, validating the form and appending to the log need the Go host (go run ./examples/kit-gallery).
Widgets
Tuimark's built-in widgets - list, input, button, progress, sparkline, table, tabs, hints and modal - with rendered examples, their events and the keys they take.
Dumps and tools
The Tuimark dump format and the CLI tools around it - dump, --cells, --styles, play, inspect, validate, preview, fmt, ir - plus the TUIMARK_* environment variables and the session log.