@fikar/design-react
v1.4.0
Published
React components for the Fikar Ink design language. AppShell renders the canonical app shell from @fikar/design.
Maintainers
Readme
@fikar/design-react
React components for the Fikar Ink design language. It provides AppShell, NavItem, NavHeading, BrandLogo, AppLauncher, ThemeMenu, UserMenu, UserMenuItem, userInitials, SearchBox, SearchResults, FilterChip, EntityBadge, DetailPanel, DetailSection, ListRow, DaySeparator, StatusBadge, RelativeTime, formatRelative, formatExact, useShortcut and useAppShell. AppShell renders the canonical app shell from @fikar/design (recipes/shell.html), or the no-sidebar shell (recipes/shell-no-sidebar.html) with sidebar={false}, and owns the drawer, the desktop collapse and the rail tooltips.
The package ships no CSS. All styling comes from the class names in @fikar/design/components.css, so import that too. It releases at the same version as @fikar/design. What a version number promises, and how things are deprecated and removed, is in the "Versioning" and "Retiring something" sections of the root README (@fikar/design).
Peer ranges: @fikar/design >=1.0.0 <2 (optional, because this package publishes after the CSS package), react and react-dom ^18.3.0, @radix-ui/react-tooltip >=1.1.6 <2.
npm install @fikar/design-react @fikar/design @radix-ui/react-tooltip react react-dom@import '@fikar/design/tokens.css';
@import '@fikar/design/components.css';import { AppShell, NavHeading, NavItem } from '@fikar/design-react';
import { Link } from 'react-router-dom';
<AppShell
brand={<img src={logo} alt="" />}
brandName="FIKAR"
nav={
<>
<NavHeading>Workspace</NavHeading>
<NavItem component={Link} to="/" icon={<HomeIcon />} label="Home" active={pathname === '/'} />
<NavItem component={Link} to="/settings" icon={<GearIcon />} label="Settings" badge="New" active={pathname === '/settings'} />
</>
}
footer={<AccountBlock />}
headerActions={<><AppLauncher apps={apps} currentId="platform" /><ThemeMenu value={theme} onChange={setTheme} /><UserMenu email={user.email} signOut={{ href: signOutUrl }} /></>}
>
<Outlet />
</AppShell>API
AppShell props: brand, brandName, navLabel (default "Main"), nav, navRail, navScroll, footer, headerStart, headerActions, children, fill (default false), sidebar (default true), collapsed, defaultCollapsed, onCollapsedChange.
The props are a union on sidebar. With the sidebar (the default) nav is required. With sidebar={false} the shell is a top bar and the content, and nav, navRail, navScroll, footer, navLabel, collapsed, defaultCollapsed and onCollapsedChange are never, so passing one is a type error. sidebar must be the literal true or false. In that mode the component reads and writes no storage, renders no drawer, and useAppShell() returns collapsed: false, drawerOpen: false, isRail: false with setters that do nothing. headerStart is worth using there: it sits between the brand and the cluster, and scrolls sideways inside itself when the bar is short of room, so the page does not widen and no link is lost.
<AppShell sidebar={false} brand={<BrandLogo lightSrc={light} darkSrc={dark} />} brandName="FIKAR" headerStart={<AdminLinks />} headerActions={<Cluster />}>
<Outlet />
</AppShell>footer is optional. Without it AppShell renders no .side-foot, so there is no bare divider above the collapse control. fill makes the shell one viewport tall at every width with a padless content column, for a page that scrolls inside itself (a conversation with a docked composer).
BrandLogo props: lightSrc, darkSrc (both required strings), alt (default "", given to both images) and height (number of px, inline style on both, absent by default so the package's 18px applies). It renders <img class="logo-light"> and <img class="logo-dark">; the package CSS shows the one that matches the theme (.dark or [data-theme='dark'] on an ancestor), and the classes only take effect inside .brand, so use it in the brand slot. brand itself stays a free slot: if you render your own logo, put logo-light and logo-dark on the two elements, <img> or inline <svg>. The old wrapper spans are not needed.
NavItem props: label, icon, badge, active, variant ('default' | 'back', default 'default'), component (default a), plus every prop the component accepts except className and children. variant="back" adds nav-item-back, the quieter link that leads out of this surface back to where the person came from; put it first. There is no className passthrough on purpose (KAN-311). It always renders a plain string className. Pass the router's Link as component and compute active in the app; NavLink manages its own class and aria-current, which fights the prop. A click closes the drawer.
NavHeading takes children and renders a group heading between nav items.
useAppShell() returns { collapsed, setCollapsed, drawerOpen, setDrawerOpen, isRail } for content inside the shell. isRail is true only while the rail is showing (collapsed and at least 1024px wide). brandName is separate from brand because the wordmark must sit inside .brand for the rail to hide it. headerStart fills the top bar left of the cluster, and navScroll makes a long nav scroll inside the sidebar.
The collapsed choice is stored in localStorage under fikar.shell.collapsed ("1" collapsed, "0" or absent open) unless you control it with collapsed. @fikar/design/shell.js writes the same values since 0.8.0.
Also new in 0.8.0 in the CSS, no React API needed: .rail-hidden and .rail-only for content that shows only open or only in the rail (.rail-hidden also leaves the tab order and the accessibility tree, so a list of links can go in it), and a fix that keeps a closed drawer out of the tab order. Upgrading from 0.7: the footer identity rule now matches direct children of .meta, so a plan pill in a nested span no longer needs a forced colour; the top bar and main area may shrink below their content; and an omitted footer renders no .side-foot. Full guide: the "React apps" section of the @fikar/design README.
Theme menu (since 1.2.0)
ThemeMenu is the sun or moon button and the panel of Light, Dark and System, for the second slot of the shell cluster. It renders recipes/theme-menu.html and needs @fikar/design 1.2.0 or later for its CSS, so install both at the same version. It keeps no state and stores nothing. The app passes the theme it has and a handler, and applies and saves the choice in its own way.
import { ThemeMenu } from '@fikar/design-react';
<ThemeMenu value={theme} onChange={(next) => { applyTheme(next); saveTheme(next); }} />value is 'light', 'dark' or 'system', and the chosen row follows it. onChange is called with the row the person chose, unless it is the one already chosen. The sun or moon on the button comes from the theme on the root (.dark or [data-theme='dark']) in the CSS, so it is right on first paint and the component takes no theme for it. It opens on click and closes on Escape, on a press outside and on choosing a row, and the arrow keys move between the rows. The markup and behaviour are documented under "Theme menu" in the @fikar/design README.
User menu (since 1.1.0)
UserMenu is the avatar and account panel for the last slot of the shell cluster. It renders recipes/user-menu.html and needs @fikar/design 1.1.0 or later for its CSS, so install both at the same version. It opens on click and closes on Escape, on a press outside and on choosing a row, with no extra dependency.
import { UserMenu, UserMenuItem } from '@fikar/design-react';
import { Link } from 'react-router-dom';
<UserMenu
name={user.name}
email={user.email}
avatarUrl={user.avatarUrl}
accountSettings={{ component: Link, to: '/account' }}
signOut={{ onSelect: () => { clearLocalState(); signOut(); } }}
>
<UserMenuItem component={Link} to="/workspace">Workspace settings</UserMenuItem>
</UserMenu>signOut takes { href } or { onSelect }. Leave out accountSettings to omit that row. userInitials(name, email) is the rule for the initials. The props, the markup and the behaviour are documented under "User menu" in the @fikar/design README.
App launcher (since 1.2.0)
AppLauncher is the grid button and app list for the first slot of the shell cluster. It renders recipes/launcher.html and needs @fikar/design 1.2.0 or later for the focus ring on its button, so install both at the same version. It opens on click and closes on Escape, on a press outside and on choosing an app, with no extra dependency. It fetches nothing, and it renders nothing when apps is empty.
import { AppLauncher } from '@fikar/design-react';
<AppLauncher
apps={[
{ id: 'chat', name: 'Chat', url: 'https://chat.fikar.ai', hint: "Ask across your organization's memory" },
{ id: 'platform', name: 'Platform', url: 'https://platform.fikar.ai' },
]}
currentId="chat"
/>Each app has id, name, url and an optional hint. The app whose id is currentId gets .current and aria-current="page". The rows are plain links, because they leave the app. The props, the markup and the behaviour are documented under "App launcher recipe" in the @fikar/design README.
Search
SearchBox (a controlled search input with an icon, a clear button and a shortcut hint, in a 'bar' or 'panel' size), SearchResults (the typeahead listbox, which hears ArrowDown, ArrowUp, Enter and Escape on the input), FilterChip (a toggle pill with pressed and onToggle), EntityBadge (type is 'PERSON', 'ORGANIZATION', 'EVENT' or 'GEO') and useShortcut(keys, handler) ('/' and 'mod+k', the React counterpart of what shell.js binds). Each renders a recipe from @fikar/design (search.html, search-results.html, filter-chip.html, entity-badge.html) and adds no styling of its own. SearchBox forwards its ref to the input and never renders data-fk-search, so the app registers the shortcut with useShortcut. The props, an example and the drift check rule are in the root README under "Search".
Detail panel, list rows, status badges and relative time
DetailPanel (a type line with a dot, a serif title, a description and an optional close button) with DetailSection (a heading over rows or chips), ListRow (a title and a muted line, a button by default and a link with component) with DaySeparator, StatusBadge (status is 'success', 'warning', 'destructive' or 'muted', and the word is the children) and RelativeTime (a <time> that says "4 h ago" and keeps the exact time in its title, refreshing itself while mounted). formatRelative(value, now?) and formatExact(value, now?) are the same wording as FikarShell.formatRelative in shell.js. Each component renders a recipe from @fikar/design (detail-panel.html, list-row.html, status-badge.html, time.html) and adds no styling of its own. The props, the wording rules, the placement of the panel and what each app changes to adopt them are in the root README under "Detail panel, list row, status badge and relative time".
