@violet-sorrengail/ui
v0.1.3
Published
Company standard React UI framework: application shell, layout and core components.
Readme
@violet-sorrengail/ui
The company's standard React UI framework. It provides the application shell — header, sidebar, navigation, page layout — plus the core components every product needs, so new applications start with the company look and spend their code on business logic.
import { AppShell, Page, Button, DataTable } from '@violet-sorrengail/ui'
import '@violet-sorrengail/ui/styles.css'
function App() {
return (
<AppShell brand={{ title: 'Quote Manager' }} nav={navigation} activePath="/quotes">
<Page title="Quote History" actions={<Button>Create Quote</Button>}>
<DataTable columns={columns} rows={rows} />
</Page>
</AppShell>
)
}Repository layout
src/lib/ The published package (@violet-sorrengail/ui)
components/ One folder per component: .tsx + scoped .css + barrel
styles/ Design tokens and the scoped reset
theme/ ThemeProvider and token overrides
hooks/ Shared behaviour hooks
types.ts NavItem / NavSection / LinkComponent / BrandConfig
index.ts The package's public surface — nothing else is exported
src/playground/ Demo application, written exactly like a consuming productThe playground is not published. It imports @violet-sorrengail/ui by its real
specifier (aliased to source in vite.config.ts), so anything that works there
works for a consumer.
Application integration
The published package owns the reusable shell and components. Each product owns its routes, labels, logo assets, permissions and business screens. A product can render the ready-made template immediately:
import { AppShell } from '@violet-sorrengail/ui'
import '@violet-sorrengail/ui/styles.css'
import type { ReactNode } from 'react'
export function ProductShell({ children }: { children: ReactNode }) {
return <AppShell>{children}</AppShell>
}This includes the template logo, sidebar navigation, About action, and account
dropdown. A product then supplies its own logo, navigation and labels through
props, while the header actions stay the template's. A product normally creates
one small wrapper around AppShell and uses it in every route:
import { AppShell, type NavItem } from '@violet-sorrengail/ui'
import { useLocation } from 'react-router-dom'
import type { ReactNode } from 'react'
import logo from './assets/product-logo.svg'
const navigation: NavItem[] = [
{ key: 'home', label: 'Home', icon: 'dashboard', href: '/' },
{ key: 'reports', label: 'Reports', icon: 'table', href: '/reports' },
]
export function ProductShell({ children }: { children: ReactNode }) {
const { pathname } = useLocation()
return (
<AppShell
brand={{ logo: <img src={logo} alt="Product" />, title: 'Product', href: '/' }}
nav={navigation}
activePath={pathname}
>
{children}
</AppShell>
)
}The framework does not hard-code a product's navigation or branding: replace the
navigation data, brand.logo and brand.title per application, and the
defaults apply only where a prop is omitted. Supply linkComponent when using a
client-side router; otherwise the shell uses normal anchors. Keep
product-specific code outside src/lib so the package remains reusable.
The header actions are fixed
The header actions are the one part of the template a product does not own. The
About link and the account dropdown are always rendered by the framework, in the
same order and the same style, so every company application presents the same
affordances. A product supplies their data through headerConfig — never
their markup:
<AppShell
brand={{ title: 'Quote Manager', logo: <img src={logo} alt="Quote Manager" /> }}
nav={[
{ key: 'dashboard', label: 'Dashboard', icon: 'dashboard', href: '/' },
{ key: 'quotes', label: 'Quotes', icon: 'table', href: '/quotes' },
]}
headerConfig={{
about: { label: 'Help', href: '/help' },
account: {
name: 'Jordan Smith',
email: '[email protected]',
items: [
{ key: 'settings', label: 'Settings', href: '/settings' },
{ key: 'signout', label: 'Sign out', tone: 'danger' },
],
},
}}
>
{children}
</AppShell>Every field is optional. Omit headerConfig entirely and the template renders
About plus the placeholder account.
| headerConfig field | Effect |
| -------------------- | ------ |
| updates | Adds the "Latest Updates" action ahead of About. Omitted by default; {} accepts every default (label, /updates, green status dot). |
| about | Overrides the About label and href. null removes the action. |
| account.name / .email / .avatarSrc | The signed-in user, shown in the avatar and the menu's summary row. |
| account.items | Replaces the menu entries below the summary row. The summary row itself is always drawn by the template. |
| account | null removes the dropdown — for an unauthenticated shell. |
| extraActions | Product-specific affordances, appended after About and before the account menu. |
Product affordances are added, not substituted:
<AppShell
headerConfig={{
account: { name: 'Jordan Smith' },
extraActions: (
<HeaderAction icon={<Icon name="table" />} href="/exports">
Exports
</HeaderAction>
),
}}
/>There is no headerActions prop: a product cannot replace the standard actions
with its own. A screen that is genuinely not an application shell — a login
page, an embedded view — opts out of the header as a whole with
header={<MyHeader />}.
The footer
The shell closes the content column with a quiet footer: the copyright line on
the left, a short link list on the right. It is on by default with the
template's copyright and Privacy / Terms of Use / Support links, and takes its
content from footerConfig.
<AppShell
footerConfig={{
copyright: '© 2026 IGT. All rights reserved.',
links: [
{ key: 'privacy', label: 'Privacy', href: '/privacy' },
{ key: 'status', label: 'Status', href: 'https://status.example.com', external: true },
],
}}
>
{children}
</AppShell>| footerConfig field | Effect |
| -------------------- | ------ |
| copyright | The left-hand line. Defaults to the current year and the company name. |
| links | The right-hand links. href routes through linkComponent; external opens a new tab and adds the external-link glyph. Pass [] for none. |
The two fields merge over the defaults independently, so naming only
copyright keeps the standard links. hideFooter drops the footer for
embedded or full-bleed screens, and footer={<MyFooter />} replaces it
outright.
Clear the page margin
The framework's reset is scoped to .cui-root so it never fights a host
stylesheet, which leaves the page itself carrying the browser default
body { margin: 8px }. AppShell is a full-viewport layout with a
viewport-fixed header, so that margin shows as a white gutter down the sides
and under the footer. Applications without their own reset add:
html,
body {
margin: 0;
}
#root {
min-height: 100vh;
}Scripts
| Command | What it does |
| -------------------------- | --------------------------------------------------------- |
| npm run dev | Runs the playground with HMR on the framework source |
| npm run build | Builds the package: dist/company-ui.js + .css + types |
| npm run build:playground | Builds the demo app into dist-playground/ |
| npm run typecheck | Type-checks framework and playground |
| npm run lint | oxlint |
Consuming the framework
npm install @violet-sorrengail/uiReact 18 or 19 is a peer dependency; the package ships ES modules and one stylesheet.
import '@violet-sorrengail/ui/styles.css' // once, at the application entry point1. Describe your navigation
Navigation is plain data, so it can come from config, a feature flag, or an API.
import type { NavSection } from '@violet-sorrengail/ui'
const navigation: NavSection[] = [
{
key: 'workspace',
items: [
{ key: 'home', label: 'Dashboard', icon: 'dashboard', href: '/' },
{ key: 'quotes', label: 'Quotes', icon: 'table', href: '/quotes' },
{
key: 'labs',
label: 'Engineering Labs',
icon: 'flask',
items: [{ key: 'reports', label: 'Reports', href: '/labs/reports' }],
},
],
},
{
key: 'admin',
label: 'Administration',
items: [
{ key: 'roles', label: 'Roles & Permissions', icon: 'userCheck', tone: 'accent', href: '/roles' },
],
},
]Sections are separated by a rule in the sidebar and may carry a label. Pass a
flat NavItem[] instead if you don't need groups. tone: 'accent' renders the
icon in the brand accent colour.
2. Mount the shell
import { AppShell } from '@violet-sorrengail/ui'
import { Link, useLocation } from 'react-router-dom'
const RouterLink = ({ href, ...rest }) => <Link to={href} {...rest} />
export function Shell({ children }) {
const { pathname } = useLocation()
return (
<AppShell
brand={{ logo: <img src={logo} alt="Company" />, title: 'Quote Manager', href: '/' }}
nav={navigation}
activePath={pathname}
linkComponent={RouterLink}
headerConfig={{
updates: { href: '/updates' },
account: { name: 'Jordan Smith', email: '[email protected]' },
}}
sidebarFooter={<span>v1.4.0</span>}
>
{children}
</AppShell>
)
}The header actions come from the template — see The header actions are fixed. An application names its user and its links; it does not rebuild them.
The shell is router-agnostic. Pass linkComponent and every link the framework
renders (sidebar, breadcrumbs, cards, header actions) goes through your router.
Without it, links fall back to plain anchors — or supply onNavigate and the
shell will intercept clicks and hand them to you.
useAppShell() exposes shell state (collapsed, toggleCollapsed, isMobile,
…) anywhere inside the shell.
3. Build screens
<Page
breadcrumbs={[{ label: 'Home', href: '/' }, { label: 'Quote History' }]}
title="Quote History"
subtitle="Every quote raised by your team."
actions={<Button leftIcon={<Icon name="plus" size={16} />}>Create Quote</Button>}
toolbar={<Input placeholder="Search" leftIcon={<Icon name="search" size={16} />} />}
>
<DataTable
columns={columns}
rows={rows}
rowKey={(row) => row.id}
defaultSort={{ key: 'updated', direction: 'desc' }}
pageSize={20}
striped
/>
</Page>Page also supports width (default | narrow | full), align="center"
for hero screens, and bleed to drop the canvas padding.
Components
| Area | Exports |
| --------- | -------------------------------------------------------------- |
| Shell | AppShell, Header, HeaderAction, HeaderSeparator, Sidebar, Footer, useAppShell |
| Layout | Page, PageSection, CardGrid |
| Content | Card, DataTable, Badge, Avatar, Icon |
| Controls | Button, Input, Dropdown, Spinner |
| Theming | ThemeProvider, themeToStyle |
| Hooks | useMediaQuery, useControllableState |
Theming
Every visual decision resolves to a CSS custom property (--cui-*) declared in
src/lib/styles/tokens.css. Products theme the framework by overriding tokens —
never by targeting internal class names, which are not a public API.
<ThemeProvider theme={{ brand: '#004b8d', accent: '#f5a623', sidebarWidth: '280px' }}>
<App />
</ThemeProvider>or globally:
:root {
--cui-color-brand: #004b8d;
--cui-color-accent: #f5a623;
}Defaults come from the reference design: brand #0055b9, sidebar #133366,
accent #ff7600, canvas #f5f6fa.
Behaviour built in
- Collapsible sidebar — orange edge toggle, icon-only rail with tooltips, auto-expands when a nested route is active.
- Responsive — below 900px the sidebar becomes a drawer opened by the header hamburger, and closes on navigation.
- Accessible — landmark regions, a skip link,
aria-currenton the active nav item,aria-sorton sortable columns, keyboard-operable rows and menus, andprefers-reduced-motionsupport. - Scoped styles — the reset applies to
.cui-root, so the framework can be dropped into an existing application without fighting its stylesheet.
Changing the logo
Brand artwork lives in src/playground/assets/ and is listed in one file,
src/playground/brand.ts:
export const BRAND_MARKS: BrandMark[] = [
{ src: igtEveriLogo, alt: 'IGT and Everi', height: 36 },
]To swap a logo, replace the file in assets/ keeping the same name — no code
change at all — or point src at a new file. Add or remove an entry and the
header lockup follows, dividers included. height is the rendered height;
the width follows the artwork's own aspect ratio.
igt-everi-logo.png is the combined IGT + Everi lockup, so it is a single mark
rather than two — it draws its own rule between the companies.
Dark surfaces
The header uses the brand token as its background, and logo artwork sits directly on that surface. The bundled lockup is the reversed (white) artwork drawn for exactly that, which is why it is not legible on a light surface — a login screen or printed view would need a full-colour export.
Applications theming the shell should likewise supply a logo with enough contrast against their own brand token:
<AppShell brand={{ logo: <img src={logoOnBrand} alt="Product" />, title: 'Product' }} />Adding a component
- Create
src/lib/components/MyThing/withMyThing.tsx,myThing.css, andindex.ts. - Style only with
--cui-*tokens, and prefix class namescui-my-thing__…. - Re-export from
src/lib/index.ts— if it isn't exported there, it isn't part of the package.
Publishing
npm run build emits dist/company-ui.js, dist/company-ui.css and
dist/types/. package.json already declares the exports map, files, and
React as a peer dependency; point publishConfig.registry at the internal
registry and npm publish when the package name is claimed.
