@tabcommerceio/tabui
v0.1.5
Published
Standalone admin UI component library, demoed in Storybook.
Readme
@tabcommerceio/tabui
A standalone admin UI component library, demoed in Storybook.
It was forked from apps/irodia/packages/web/console-ui and is now maintained
independently: there is no shared code, no shared build, and no sync process
back to the original. Changing one side never changes the other.
What is in here
src/components/*— components with their own CSS, tests and storiessrc/screens/*— whole-page specimens composed from those componentssrc/styles/*— design tokens (tokens.css), the CSS entry (globals.css) and the Tailwind preset (tabuiPreset)src/lib/cn.ts— thecn()class merger
Development
pnpm install
pnpm storybook # http://localhost:6007Port 6007 keeps Storybook clear of console-ui's 6006 if both run at once.
Scripts
| script | what it does |
| --- | --- |
| pnpm storybook | Storybook dev server on 6007 |
| pnpm build-storybook | static Storybook into storybook-static/ |
| pnpm build | library build into dist/ via tsup (cjs + esm + d.ts) |
| pnpm dev | tsup in watch mode |
| pnpm test | vitest run |
| pnpm typecheck | tsc --noEmit over src and .storybook |
Consuming it
Install the library and its React and Tailwind CSS peers:
npm install @tabcommerceio/tabui react react-dom tailwindcssimport { Button, Card, Page } from "@tabcommerceio/tabui"
import "@tabcommerceio/tabui/styles.css"Tailwind consumers extend the preset:
import { tabuiPreset } from "@tabcommerceio/tabui/preset"
export default { presets: [tabuiPreset], content: [...] }Design reference
The visual language tracks the Shopify admin. The current values were read off a live admin session (Polaris 25.98, September 2026) rather than copied from documentation, so they include the 2025-26 redesign:
| | before | now |
| --- | --- | --- |
| app frame | light canvas, full-width top bar | near-black #0a0a0a chrome, no top bar, content on an inset white sheet at 16px |
| navigation | 240px light column | 220px dark column holding brand, search, nav, Settings and the account row |
| fields | 1px border + 2px offset outline on focus | no border, no outline — one box-shadow stack per state |
| buttons | 8px radius, bevelled | pill, flat secondary, lit primary |
| cards | 12px radius + 1px ring | 20px, four-layer soft shadow |
| controls | 8px radius | 12px (inputs, selects, menu items, nav) |
| popovers / dialogs | 12px / 16px | 16px / 24px |
| text | #303030 / #616161 | #101010 / #4a4a4a |
| borders | #8a8a8a / #e3e3e3 | a single hairline #dddddd |
| link / focus | #005bd3 | #1650d0 / #5083f0 |
| status | muted teal + amber | lime success #b3f782, orange warning |
| page titles | 18px semibold | 13px / 500 — the admin de-emphasised them |
| weights | optical 450 / 550 / 650 | plain 400 / 500 / 600 (no 700) |
Shell anatomy
ConsoleShell mirrors the admin's own frame. There is no utility bar: the
navigation column owns everything except the page.
.tabui-Shell grid, 220px + 1fr, background #0a0a0a
├── .tabui-Sidebar grid: header / search / nav / footer
│ ├── .tabui-Sidebar__Header brand + collapse control 48px
│ ├── .tabui-Sidebar__Search a button dressed as a field 28px
│ ├── .tabui-Sidebar__Nav the only part that scrolls
│ └── .tabui-Sidebar__Footer Settings, then the account row
│ └── .tabui-Sidebar__Account store switcher + help + notifications
└── .tabui-Workspace the sheet: white, 16px, inset 4px
├── .tabui-Workspace__Chrome save-bar slot; 0px until a page claims it
└── .tabui-Workspace__Content the scrollportThe nav group with id: "system" is pinned into the footer rather than
scrolling with the rest, which is where the admin keeps Settings. Below 768px
the column becomes a drawer and the sheet goes full bleed, growing a header
strip for the drawer's trigger.
Page still reaches the chrome strip through useConsoleChromeSlot(); only
the node's position moved, so the save bar now floats at the top of the sheet
instead of in a bar above it.
Floating surfaces
Popovers, dropdown menus, selection menus, the column menu, the store menu and the date range picker all draw the same surface, because the admin does:
white · 16px radius · no border · 4px padding · --p-shadow-popover
items 12px radius · 4px 6px · 32px min-height · 13/20 regular
hover #f2f2f2 · pressed / selected #ebebebReach for --p-shadow-popover rather than writing a shadow; the one-pixel
ring is the shadow's last layer, so nothing needs a border as well. (Before
this pass there were four different treatments, and the dropdown drew both a
border and a ring.)
Fields
A field is drawn entirely with box-shadow — no border and no outline. That
is what the admin does, and it matters: the box never changes size between
rest, hover, focus and error, and the focus halo can bleed outside the
control instead of being clipped to an outline rectangle. The stack lives in
--p-shadow-input{,-hover,-focus,-error,-error-focus,-disabled}.
Hover is scoped with :hover:not(:focus-within). A field is hovered and
focused the instant you click into it, and the hover selector carries more
classes than the focus one, so without that guard hover wins the cascade and
the focus ring only appears once the pointer leaves.
Date range
DateRangeSelect is the admin's reporting range control: a preset rail beside
a two-month calendar, with the staged range summarised above Cancel / Apply.
Nothing is reported until Apply, so dismissing the popover always leaves the
merchant where they were, and the trigger keeps naming the committed range
while a draft is in progress. Presets are data — pass your own presets, each
with a resolve(today) — and the trigger names the preset whenever the
committed range matches one.
Use variant="filled" to give the trigger a surface background when it sits
directly on the page canvas; the default plain trigger stays transparent.
The rail ends with Custom range, which is a mode rather than a preset: touching the calendar or typing in either endpoint switches to it, and it stays custom even if the dates drift back onto a preset. In custom mode the range summary gives way to two endpoint fields.
Those fields read as dates and edit as dates. At rest they show
August 3, 2026; on focus they swap to 2026-08-10, which is the only shape
they accept — the admin does the same, and it is what lets the field be strict
about input. Anything that is not a digit or a dash is dropped as it is typed,
the value is capped at ten characters, and on commit the date must survive a
round trip (so 2026-02-31 is refused rather than rolling into March) and fall
inside the allowed range. A rejected edit leaves the committed date alone.
Tokens live in two layers in src/styles/tokens.css:
--p-*— the Polaris surface the components already consume, so a rule written asvar(--p-color-text)picks up Shopify's colour untouched.--tabui-*— the public, renameable surface the Tailwind preset and the shell read; it resolves through layer 1.
Every hard-coded fallback in the components matches the token beside it, so
the library still renders correctly if tokens.css is never imported.
Naming
The design-token and class prefix is tabui, not console:
- CSS variables —
--tabui-color-surface,--tabui-radius-md,--tabui-font-sans - component classes —
tabui-Page,tabui-CartEditor,tabui-Sidebar__Nav - Tailwind colors —
bg-tabui-surface,text-tabui-text-subdued
The React API is deliberately unchanged from the fork point, so component
names, props and exports still match console-ui one-for-one — including
ConsoleShell and CONSOLE_NAV. Renaming those is a separate pass.
src/screens/* are Irodia-flavoured page specimens carried over with the fork.
They are demo material, not product truth; delete or rewrite them freely.
