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▾
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";ComponentDescription
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
TermDescription
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
slice
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
You passDescription
"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
You passDescription
"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
refnames a model getsData.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. WithoutonSelectevery tile is plain. queryKeykeeps 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 asproductInOrgthrows. Narrow the list withqueryinstead. - Give each action its slot.
renderTemplatefills the edit modal and brings the New button;renderViewmounts the view modal. Model.AdminPaneldoes this wiring for you. It takes the module'sUnit,TemplateandViewnamespaces and fills these slots.


Data.* is for admin screens. It reads root slices, which are Admin-guarded, and ships a toolbar, a query maker and a data export: a lot of client JS for a list a visitor only reads. A product screen composes Load.Units with the module's own Unit and Zone instead.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
| breakUnit | Relative label while |
|---|---|
| Not set | Always 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
| Case | Shows |
|---|---|
| Past the break, same day | HH:mm |
| Past the break, same year | MM-DD |
| Past the break, another year | YYYY-MM-DD |
Past the break, with format="full" | YY-MM-DD HH:mm |
| Tooltip | YYYY-MM-DD HH:mm |
Tooltip, with breakUnit="second" | YYYY-MM-DD HH:mm:ss |
Epoch placeholder (0 or -1) | --:-- |
Relative wording
| relative | Output 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". breakUnitis 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 waiting | Use |
|---|---|
| Content will appear in this spot | Loading.Skeleton |
| A control or a small area is working | Loading.Spin |
| The work has a known end, such as an upload | Loading.ProgressBar |
| A whole panel is busy | Loading.Area |
| A button or field is not rendered yet | Loading.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 defaulttext-primary/70vanishes on abg-infobadge or a primary button. Atext-*inclassNamebeats every tone. - A replacement icon needs no spin class. An
indicatorkeeps its own colour, and the wrapper spins an SVG icon for you, so leave outanimate-spin. - Covers need a positioned parent.
Loading.AreaandLoading.Spin isCenterareabsolute inset-0, so put them inside arelativeelement. - Each member is its own override slot.
LoadingSkeletoncan be re-skinned without touchingLoadingSpin, 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 skipcn().
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.Unitsalready draws<Empty />for a slice with no rows, andTabledraws one at 160 px. Pass your own only to change it. - One override restyles them all.
Emptyis an override slot, so a route's_overrides.tsxchanges 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
Tabledoes not slice the rows. It draws all ofdataSource, so hand it the current page, as above.paginationforwards four fields only.currentPage,total,itemsPerPageandonPageSelectreach the pager. Forprev,nextorempty, setpagination={false}and render aPaginationinfooter.
Pagination
A standalone page-number control for page state you hold yourself. When the state belongs to a model slice, use
Data.Pagination.Props
currentPagenumber
The current page, counted from 1.
totalnumber
The total item count. At 0 the pager renders
empty, or nothing.itemsPerPagenumber
Items per page. The page count is
total / itemsPerPage, rounded up.onPageSelect(page: number) => void
Called with the chosen page, counted from 1.
prevReactNode
The mark inside the previous-page button. The button itself stays the framework's.
nextReactNode
The mark inside the next-page button.
ellipsisReactNode
The mark standing in for the pages a long pager skips.
emptyReactNode
The placeholder for a pager with no pages. Replaces the deprecated
renderEmpty.classNames{ className?, activePageNumClassName?, pageNumClassName? }
Classes for the wrapper, the current page button and the other page buttons.
Example
A photo grid that shows twelve items a page:
apps/koyo/ui/PagedGrid.tsx
- Long pagers fold. Past ten pages it shows the first and last page, five pages around the current one, and
ellipsisfor the rest. - It follows the button recipe. The page buttons use the route's
buttonrecipe slot, andPaginationitself is an override slot.