YouBothAgent▾
You — Business rules and flows you own. Read these yourself.
Both — Know the idea; your agent follows the details.
Agent — Conventions and references your agent follows. Look up as needed.
CLI Reference▾
AkanJS Reference▾
UI Reference▾

Display UI

Components that show data: model listings, timestamps, loading and empty states, status pills, tables and pagers. All of them come from akanjs/ui.import { Badge, Data, Empty, Loading, Pagination, RecentTime, Table } from "akanjs/ui";
Data
The admin listing screen and its parts, bound to one model's slice.
RecentTime
A time shown as "3 minutes ago", with the exact date in a tooltip.
Loading
Six waiting indicators: spinner, skeleton, progress bar and three placeholders.
Badge
A status pill: a <span> styled by badgeRecipe.
Empty
The "no data" placeholder, with room for a follow-up action below it.
Table
Rows you already hold, drawn as a table with an optional pager.
Pagination
A page-number control driven entirely by props.
Words used on this page
slice
Metadata naming one model list and the store keys it fills. Get it from fetch.slice.<model>.
store
The client state generated per model. st.use reads it and st.do changes it.
insight
Aggregates a slice returns beside its rows, such as count.
override slot
A component name a route's _overrides.tsx can swap for the app's own version.
Store-bound or prop-bound
The pager and the table each come in two versions, and swapping them is the usual mistake. Use the Data.* one for a model slice, and the plain one for values you already hold:
Component
Store
Props
Pager
Data.Pagination
✓
Reads a slice's page, limit and count from the store.
Pagination
✓
Takes currentPage, total and itemsPerPage as props.
Table
Data.TableList
✓
A listing wired to a model: rows, pager and modals all come from the slice.
Table
✓
Rows you already hold, passed in as dataSource.
✓Where its values come fromNot used
Related pages

Data

The admin listing screen, split into parts. Data.ListContainer is the whole screen; every other member is one piece of it, exported so a different layout can compose instead of fork.
Every member takes the same slice, which tells it the model and the store keys to read.
Members
Data.ListContainer{ slice, type?, query?, columns?, actions?, tools?, render…? }
The whole admin listing: toolbar, dashboard, rows or cards, and the CRUD modals.
Data.TableList{ slice, columns, init?, queryArgs?, actions?, renderView?, renderTemplate?, renderTitle?, onItemClick? }
The listing as rows, with its own edit and view modals. queryArgs makes it load on mount.
Data.CardList{ slice, columns, renderItem, init?, actions?, renderView?, renderTemplate?, renderLoading? }
The listing as cards. Each renderItem result sits in a Data.Item with the row actions.
Data.Item{ slice, model, title?, actions?, columns?, onClick?, children? }
One card: children (or title) on top, then the listed columns and the action buttons.
Data.Pagination{ slice, className? }
The pager. It reads page state from the slice's store, not from props.
Data.Dashboard{ slice, summary, columns?, presents?, hidePresents?, queryMap?, summaryRefName?, onSelect?, queryKey? }
Summary tiles above the list. A tile that knows its filter narrows the list on click.
Data.Insight{ slice, insight, columns? }
Tiles for the slice's insight values. The total count is already in the header.
Data.QueryMaker{ slice, query?, onApply? }
Picks a declared filter and fills its args. onApply defaults to the slice's store.
Data.RefPicker{ refName, value, onChange }
Picks a row of another model for a filter arg whose ref names it, such as an owner id.
Data.ListContainer props
sliceSliceMeta
The model's root slice, fetch.slice.<model>.
type"card" | "list"default "card"
The first rendering. The toolbar toggle switches between cards and rows.
queryQuerySetting
Fixes the filter. The query maker and the dashboard are then not drawn.
queryMap{ [column]: QuerySetting }
The filter per summary column. ?filter=<column> opens the list on that filter.
initFetchInitForm
The first fetch: page, limit, sort, and the defaults a new model starts from.
columnsDataColumn[]default ["id", "createdAt", "updatedAt"]
Fields shown in each row and card, and written by the CSV export.
actionsDataAction[] | (item, idx) => DataAction[]default ["remove", "edit", "view"]
Row buttons. A function decides them per row.
toolsDataTool[] | (list) => DataTool[]default []
Extra entries in the toolbar's more menu, beside CSV and JSON export.
createbooleandefault true
Shows the New button, as long as renderTemplate is given.
titleReactNode
The heading. Defaults to the model's name from its dictionary.
sortsort key
The initial sort. The toolbar offers the model's other sort keys.
classNamestring
Classes for the whole container.
cardListClassNamestring
Classes for the card grid.
Render slots
renderItem(props) => ReactNode
The card body in card mode. It gets { [model]: item, slice, actions, columns, idx }.
renderTemplate(props) => ReactNode
The form inside the edit and new modals. Without it there is no New button.
renderView(model) => ReactNode
The body of the view modal. Without it the view button opens nothing.
renderTitle(model) => ReactNode
The modal title. Defaults to the model name and the id.
renderDashboard({ summary, onSelect, queryKey, hidePresents }) => ReactNode
The area above the list, usually a Data.Dashboard. It needs the app's summary state.
renderInsight({ insight }) => ReactNode
The insight area above the list, usually a Data.Insight.
renderQueryMaker() => ReactNode
Replaces the filter-argument form under the toolbar.
renderLoading() => ReactNode
One placeholder card, repeated while the cards load.
Columns
"name"
A field name. The header label comes from the model's dictionary.
"createdAt""updatedAt""startAt"
Date fields with these and a few similar names are drawn as RecentTime.
"status""role"
A name containing status or role is drawn as a coloured badge.
{ key, title?, render?, value?, responsive? }
Your own label and cell. value is what the CSV export writes instead of render.
{ key, responsive: true }
Shows the column from md up and hides it on smaller screens.
Actions and tools
"view""edit""remove"
Icon buttons wired to the store. remove asks for confirmation first.
<YourButton />
Your own element. Rows put it in an Actions column, cards in the more menu.
(item, idx) => DataAction[]
Decides the buttons per row, for example by status.
{ key, render }
A tools entry. A tools function receives the loaded list.
Filters and dashboard tiles
  • The query maker lists the model's declared filters. A filter with a model-typed arg is skipped, and an id arg whose ref names a model gets Data.RefPicker.
  • It waits for required args. Nothing is sent until each required arg has a value, and typing is debounced.
  • A tile filters when it knows its query. queryMap[column] wins; otherwise the summary field's .meta({ refName, queryKey, queryArgs }) is used when it names this model. Without onSelect every tile is plain.
  • queryKey keeps the active tile honest. It is the filter the list shows, so a tile stops looking active once the toolbar moves off it.
Example
An admin product list that opens as rows and wires every row action to a modal:
apps/koyo/lib/product/Product.Zone.tsx
  • Root slice only. Pass fetch.slice.product; a named slice such as productInOrg throws. Narrow the list with query instead.
  • Give each action its slot. renderTemplate fills the edit modal and brings the New button; renderView mounts the view modal.
  • Model.AdminPanel does this wiring for you. It takes the module's Unit, Template and View namespaces and fills these slots.

RecentTime

Shows a time as a relative label such as "3 minutes ago", in the page's language, with the exact date in a tooltip. Past breakUnit it prints a date instead.
Props
dateDate | Dayjs | null
The time to show. null renders nothing.
breakUnitIntl.RelativeTimeFormatUnit
Where relative labels stop. Unset, they never switch to a date. See the table below.
format"auto" | "full"default "auto"
How a date past the break is printed. See the table below.
relative"fromNow" | "always" | "auto" | (ctx) => stringdefault "fromNow"
The wording of the relative label. See the table below.
classNamestring
Classes for the label itself.
Where relative labels stop
breakUnitRelative label while
Not setAlways relative, never a date
"second"Never relative, always a date
"minute"Under 60 seconds
"hour"Under 60 minutes
"day"Under 24 hours
"week"Under 7 days
"month"Under 4 weeks
"year"Under 12 months
What it prints
CaseShows
Past the break, same dayHH:mm
Past the break, same yearMM-DD
Past the break, another yearYYYY-MM-DD
Past the break, with format="full"YY-MM-DD HH:mm
TooltipYYYY-MM-DD HH:mm
Tooltip, with breakUnit="second"YYYY-MM-DD HH:mm:ss
Epoch placeholder (0 or -1)--:--
Relative wording
relativeOutput for one day ago
↳ Wording from
"fromNow"a day ago
dayjs locale strings. The default.
"always"1 day ago
Intl.RelativeTimeFormat, always as a number.
"auto"yesterday
Intl.RelativeTimeFormat, with words like yesterday where the language has them.
(ctx) => string…
Your own wording from { unit, count, date, now, defaultLabel }.
Example
A story byline that says "yesterday" rather than "a day ago", and a date after a week:
apps/koyo/lib/story/Story.View.tsx
  • It works in a server component. The View above carries no "use client".
  • breakUnit is the first unit printed as a date. With "week", anything under a week stays relative and anything older prints a date.

Loading

Six indicators, one per shape of thing that is waiting. Pick by what the reader is looking at:
What is waitingUse
Content will appear in this spotLoading.Skeleton
A control or a small area is workingLoading.Spin
The work has a known end, such as an uploadLoading.ProgressBar
A whole panel is busyLoading.Area
A button or field is not rendered yetLoading.Button · Loading.Input
Members
Loading.Spin{ className?, indicator?, isCenter?, size?: "sm" | "md" | "lg" | number, tone? }default size "md", tone "primary"
The spinner. size is a step or pixels; tone is "primary", "current" or "muted".
Loading.Skeleton{ className?, active?, style? }default active true
Four grey text lines, pulsing while active. A good fallback for Load.Stream.
Loading.ProgressBar{ className?, value, max }
A determinate bar that animates to value / max. Use it when both numbers are real.
Loading.Button{ className?, active?, style? }default active true
A button-shaped placeholder for a control not there yet. Not a spinner inside a button.
Loading.Input{ className?, active?, style? }default active true
The same placeholder, shaped like an input field.
Loading.Area{ className?, indicator?, children? }
A blurred absolute inset-0 cover with a spinner and a message (default: processing).
Example
An upload row with a spinner and a progress bar:
apps/koyo/ui/UploadProgress.tsx
  • On a filled surface, use tone="current". The default text-primary/70 vanishes on a bg-info badge or a primary button. A text-* in className beats every tone.
  • A replacement icon needs no spin class. An indicator keeps its own colour, and the wrapper spins an SVG icon for you, so leave out animate-spin.
  • Covers need a positioned parent. Loading.Area and Loading.Spin isCenter are absolute inset-0, so put them inside a relative element.
  • Each member is its own override slot. LoadingSkeleton can be re-skinned without touching LoadingSpin, and likewise for the other four.

Badge

The status pill: a <span> with the badgeRecipe variants and nothing else. Every other attribute passes through, so title, aria-* and a click handler all work.
Props
variant"default" | "primary" | "secondary" | "accent" | "neutral" | "success" | "warning" | "info" | "error" | "outline"default "default"
The colour. Map a model enum to it through a module-scope as const table.
size"xs" | "sm" | "md" | "lg"default "md"
Height and text size.
outlineboolean
Draws the variant's colour as an outline. variant="outline" is the plain, uncoloured one.
...HTMLAttributes<HTMLSpanElement>attributes
Everything a <span> takes. className is merged last and wins over the variant.
Example
A job status badge. The enum maps to a variant through a module-scope table:
apps/koyo/lib/job/Job.Unit.tsx
  • Restyle every badge at once. Bind recipes: { badge } in a route's _overrides.tsx; no call site changes.
  • Need only the classes? Call badgeRecipe. badgeRecipe(variants, className) gives a badge look to an <a> or a <button>. It is server-safe and takes an array as the second argument, so skip cn().

Empty

The standard "no data" state: an icon, a translated message, and room for a follow-up action below.
Props
descriptionReactNodedefault l("base.noData")
The empty-state text. The default is the translated no-data label.
iconReactNode
The mark above the text. Defaults to an inbox icon.
minHeightnumberdefault 300
Minimum height of the empty body, in pixels.
classNamestring
Classes for the empty body. children sit outside it.
childrenReactNode
Content under the empty body, such as a create button.
Example
A product list whose empty state offers a create button, passed through Load.Units:
apps/koyo/lib/product/Product.Zone.tsx
  • You rarely mount it yourself. Load.Units already draws <Empty /> for a slice with no rows, and Table draws one at 160 px. Pass your own only to change it.
  • One override restyles them all. Empty is an override slot, so a route's _overrides.tsx changes every empty state under it.

Table

A responsive table for rows you already hold. For a model listing wired to the store, use Data.TableList instead.
Props
columns{ key?, title, dataIndex, render?, responsive? }[]
One header and cell per column. responsive lists the breakpoints where it shows.
dataSourceany[]
The rows to draw, all of them. Slice it to the current page yourself.
rowKey(row) => string
The React key per row. Defaults to the row index.
loadingboolean
Dims the rows and draws loadingIndicator over them.
loadingIndicatorReactNode
The mark shown over the rows while loading. Defaults to a spinner.
paginationPaginationProps | false
Draws a Pagination under the table. Unset or false draws none.
onRow(record, index) => { onClick }
Row events such as click-to-open. Rows then show a pointer cursor.
rowClassNamestring | (record, index) => string
Classes for every row, or per row.
size"small" | "middle"
"small" tightens the cell padding.
borderedboolean
Draws a rounded border around the table.
showHeaderboolean | Responsive[]default true
Hides the header, or shows it only at the listed breakpoints.
headerReactNode
Content drawn above the table.
footerReactNode
Content drawn below the table, under the pager.
emptyReactNodedefault <Empty minHeight={160} />
The placeholder for a table with no rows.
Example
An invoice table that pages locally and hides the amount on small screens:
apps/koyo/ui/InvoiceTable.tsx
  • Table does not slice the rows. It draws all of dataSource, so hand it the current page, as above.
  • pagination forwards four fields only. currentPage, total, itemsPerPage and onPageSelect reach the pager. For prev, next or empty, set pagination={false} and render a Pagination in footer.

Released under the MIT License

Connect your AI to these docs

MCPhttps://akanjs.com/mcp
Copyright © 2026 Akan.js All rights reserved.System managed bybassman