What's new in Akira UI 3.1.0
Spanish labels, badges on sidebar items, a pinned actions column, and the fix for table filters that broke on Inertia 3.
Today I tagged 3.1.0 of Akira UI. If you haven’t come across it, it’s a React component library built on shadcn primitives. Next to the usual buttons, dialogs and tables, it ships app shells (sidebar, header, user menu), ready-made blocks, label bundles per language and an Inertia adapter.
This is a minor release with no breaking changes, so upgrading from 3.0.0 is a version bump. I cut it now so Nos-Ferry/support 1.0.0 could depend on a published version instead of the main-dist branch.
Badges on sidebar items
Say you have a refunds queue and you want whoever runs it to see how many are waiting without opening the page. A nav item now takes a badge and a badgeLabel:
const mainNavItems: NavItem[] = [
{ title: 'Dashboard', href: '/dashboard', icon: LayoutGrid },
{
title: 'Refunds',
href: '/refunds',
icon: Receipt,
badge: openRefundsCount,
badgeLabel: `${openRefundsCount} refunds awaiting review`,
},
];
NavMain renders the count right-aligned on the row. A zero, an empty value or a boolean renders nothing, so a quiet queue leaves the row clean. From 1 to 99 you see the number, and above that you get 99+. When the sidebar collapses to its icon rail, the badge becomes a dot on the icon and the exact figure moves into the tooltip.
badgeLabel is there for screen readers. A bare “7” doesn’t say what it counts, so the label becomes the row’s accessible name and the number itself is hidden from assistive technology.
The sidebar header and the user menu
AppSidebarHeader takes an actions node, rendered at the right edge after the search button. That’s where a notifications bell or a user menu goes, and the Inertia adapter forwards it too:
<AppSidebarHeader
breadcrumbs={breadcrumbs}
linkComponent={Link}
onSearchClick={openCommandMenu}
actions={<NotificationBell />}
/>
The user menu in AppSidebar used to show “Settings” and “Log out” in English unless you composed the sidebar yourself. Those entries now come from the locale provider, and userMenuLabels on AppSidebar overrides them when one app needs different wording.
Labels beyond English
Locales got most of the work in this release. There’s a new Spanish bundle, written in the Spanish spoken in Spain, with the same shape as the French one:
import { UiLocaleProvider } from '@akira-io/ui';
import { esLabels } from '@akira-io/ui/locales/es';
<UiLocaleProvider labels={esLabels}>
<App />
</UiLocaleProvider>;
The Portuguese bundle had drifted. It mixed pre-reform spellings like “Seleccione” with Brazilian and post-reform ones like “Selecione” and “navegador”. It targets Portugal and Cabo Verde, so every string now uses one European spelling, the pre-reform one (“Seleccione”, “actual”, “acção”), and a test rejects the Brazilian and post-reform forms.
The provider also reaches further than before. EmptyState, DangerZone, the form overlays and the editor read their labels from it, and so does the screen-reader text of the shadcn primitives: “Close” on Dialog and Sheet, “Go to previous page” on Pagination, “Toggle Sidebar”, “Previous slide”. An app under ptLabels used to have those read out in English. A coverage test now compares every shipped locale against the Portuguese one key by key, so a label added to one bundle can’t go missing from another.
Tables and detail fields
The last column of a data table, where the row menu lives, now stays pinned while the table scrolls sideways. On a narrow screen you reach the menu without scrolling to the end of the row, and the shadow over the hidden columns only appears while a column is scrolled behind it.
In the info field block, a long value such as an email address could push the copy button past the edge of a narrow column. The button now sits next to the label, and the value truncates with an ellipsis. If you prefer the old layout, copyPlacement="value" brings it back.
Three fixes
The first one broke table filters on Inertia 3. The filters hook passed only: current.only on every visit, even when no only was set. Inertia 3 reads visit.only.length, so an explicit undefined overrode the router default and the visit threw before any request went out. The fix is one line:
// src/inertia-table-filters.ts
router.visit(current.url, {
data: toQuery({ ...current.filters, ...changes }),
...(current.only ? { only: current.only } : {}),
// …
});
The second was a DatePicker showing two containers: the popover, and inside it the calendar with its own ring and shadow. The library has a nested-surface variant to remove that second surface, but it recognised a parent by its data-slot. Renaming a component with slotName replaces data-slot, and the date picker renames its popover to calendar-popover. ConfirmDialog, FormDialog and a few others lost the reset the same way. Cards and overlay contents now carry a separate data-surface attribute, and the variant matches that instead:
/* theme.css */
@custom-variant nested-surface (
&:is(
[data-surface]:not([data-inset]) *,
[data-slot='sidebar'] *,
[data-slot='data-table'] *
)
);
The floating sheet panel got the marker as well, and a test opens every marked surface with a card inside, so dropping the attribute from any of them fails that test.
The third one shows up on macOS. Rubber-band overscroll reveals the background of the html element, which nothing painted, so a band of a different colour appeared above the inset sidebar. A new root-background.css, imported by theme.css, paints it:
:root {
background-color: var(--background);
}
:root:has([data-slot='sidebar-wrapper'] [data-variant='inset']) {
background-color: var(--sidebar);
}
In closing
Most of 3.1.0 is for apps whose interface isn’t in English and for the sidebar they tend to share. If you’re on 3.0.0, bump the version and you’re done. The full list is in the release notes, the package is on npm, and the documentation lives in the docs folder of the repository.