chore: initial package split from monorepo

Package: @lilith/admin-shell
Split from: lilith/ui.git or lilith/build.git
Publish workflow: calls lilith/workflows/.forgejo/workflows/publish-npm.yml@main
This commit is contained in:
autocommit 2026-04-20 01:10:56 -07:00
commit c2cdd2c97d
32 changed files with 671 additions and 0 deletions

View file

@ -0,0 +1,10 @@
name: Publish
on:
push:
branches: [main, master]
workflow_dispatch:
jobs:
publish:
uses: lilith/workflows/.forgejo/workflows/publish-npm.yml@main
secrets: inherit

24
dist/AdminShell.d.ts vendored Normal file
View file

@ -0,0 +1,24 @@
import type { AdminShellProps } from './types';
/**
* AdminShell provides a consistent layout for admin dashboards.
*
* Features:
* - Responsive sidebar with navigation sections
* - Configurable logo with optional badge
* - Theme-aware styling with accent color support
* - Scrollable main content area
*
* @example
* ```tsx
* <AdminShell
* logo={{ title: 'Platform Admin', subtitle: 'Lilith Platform' }}
* navigation={NAVIGATION_SECTIONS}
* accentColor="primary"
* footerText="Lilith Platform v0.1.0"
* >
* <Routes>...</Routes>
* </AdminShell>
* ```
*/
export declare function AdminShell({ logo, navigation, footerText, accentColor, children, }: AdminShellProps): import("react/jsx-runtime").JSX.Element;
//# sourceMappingURL=AdminShell.d.ts.map

1
dist/AdminShell.d.ts.map vendored Normal file
View file

@ -0,0 +1 @@
{"version":3,"file":"AdminShell.d.ts","sourceRoot":"","sources":["../src/AdminShell.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAmB/C;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,UAAU,CAAC,EACzB,IAAI,EACJ,UAAU,EACV,UAAU,EACV,WAAuB,EACvB,QAAQ,GACT,EAAE,eAAe,2CAYjB"}

38
dist/AdminShell.js vendored Normal file
View file

@ -0,0 +1,38 @@
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
import { styled } from '@lilith/ui-styled-components';
import { Sidebar } from './components/Sidebar';
const AppContainer = styled.div `
height: 100vh;
display: flex;
overflow: hidden;
`;
const MainContent = styled.main `
flex: 1;
padding: ${({ theme }) => theme.spacing.xl};
overflow-y: auto;
min-width: 0;
`;
/**
* AdminShell provides a consistent layout for admin dashboards.
*
* Features:
* - Responsive sidebar with navigation sections
* - Configurable logo with optional badge
* - Theme-aware styling with accent color support
* - Scrollable main content area
*
* @example
* ```tsx
* <AdminShell
* logo={{ title: 'Platform Admin', subtitle: 'Lilith Platform' }}
* navigation={NAVIGATION_SECTIONS}
* accentColor="primary"
* footerText="Lilith Platform v0.1.0"
* >
* <Routes>...</Routes>
* </AdminShell>
* ```
*/
export function AdminShell({ logo, navigation, footerText, accentColor = 'primary', children, }) {
return (_jsxs(AppContainer, { children: [_jsx(Sidebar, { logo: logo, navigation: navigation, footerText: footerText, accentColor: accentColor }), _jsx(MainContent, { children: children })] }));
}

9
dist/components/Sidebar.d.ts vendored Normal file
View file

@ -0,0 +1,9 @@
import type { NavSection, LogoConfig } from '../types';
export interface SidebarProps {
logo: LogoConfig;
navigation: NavSection[];
footerText?: string;
accentColor?: 'primary' | 'accent';
}
export declare function Sidebar({ logo, navigation, footerText, accentColor }: SidebarProps): import("react/jsx-runtime").JSX.Element;
//# sourceMappingURL=Sidebar.d.ts.map

1
dist/components/Sidebar.d.ts.map vendored Normal file
View file

@ -0,0 +1 @@
{"version":3,"file":"Sidebar.d.ts","sourceRoot":"","sources":["../../src/components/Sidebar.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAoHvD,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,UAAU,CAAC;IACjB,UAAU,EAAE,UAAU,EAAE,CAAC;IACzB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,SAAS,GAAG,QAAQ,CAAC;CACpC;AAED,wBAAgB,OAAO,CAAC,EAAE,IAAI,EAAE,UAAU,EAAE,UAAU,EAAE,WAAuB,EAAE,EAAE,YAAY,2CAoC9F"}

106
dist/components/Sidebar.js vendored Normal file
View file

@ -0,0 +1,106 @@
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
import { NavLink } from '@lilith/ui-router';
import { styled } from '@lilith/ui-styled-components';
const SidebarContainer = styled.nav `
width: 16rem;
background: ${({ theme }) => theme.colors.surface};
border-right: 1px solid ${({ theme }) => theme.colors.border.default};
padding: ${({ theme }) => theme.spacing.md};
display: flex;
flex-direction: column;
overflow-y: auto;
flex-shrink: 0;
`;
const LogoArea = styled.div `
margin-bottom: ${({ theme }) => theme.spacing.xl};
`;
const LogoTitle = styled.h1 `
font-size: ${({ theme }) => theme.typography.fontSize.xl};
font-weight: ${({ theme }) => theme.typography.fontWeight.bold};
color: ${({ theme, $accentColor }) => $accentColor === 'accent' ? theme.colors.accent.main : theme.colors.primary.main};
`;
const LogoSubtitle = styled.p `
font-size: ${({ theme }) => theme.typography.fontSize.sm};
color: ${({ theme }) => theme.colors.text.muted};
`;
const Badge = styled.span `
display: inline-block;
padding: 2px 8px;
margin-left: 8px;
font-size: 10px;
font-weight: 600;
background: ${({ theme, $variant }) => {
switch ($variant) {
case 'warning': return `${theme.colors.warning.main}30`;
case 'info': return `${theme.colors.info.main}30`;
case 'success': return `${theme.colors.success.main}30`;
case 'error': return `${theme.colors.error.main}30`;
default: return `${theme.colors.warning.main}30`;
}
}};
color: ${({ theme, $variant }) => {
switch ($variant) {
case 'warning': return theme.colors.warning.main;
case 'info': return theme.colors.info.main;
case 'success': return theme.colors.success.main;
case 'error': return theme.colors.error.main;
default: return theme.colors.warning.main;
}
}};
border-radius: 4px;
text-transform: uppercase;
`;
const NavSectionsContainer = styled.div `
flex: 1;
display: flex;
flex-direction: column;
gap: ${({ theme }) => theme.spacing.lg};
`;
const NavSectionContainer = styled.div ``;
const NavSectionTitle = styled.h2 `
font-size: ${({ theme }) => theme.typography.fontSize.xs};
font-weight: ${({ theme }) => theme.typography.fontWeight.semibold};
color: ${({ theme }) => theme.colors.text.muted};
text-transform: uppercase;
letter-spacing: 0.05em;
margin-bottom: ${({ theme }) => theme.spacing.sm};
`;
const NavList = styled.ul `
display: flex;
flex-direction: column;
gap: ${({ theme }) => theme.spacing.xs};
list-style: none;
margin: 0;
padding: 0;
`;
const StyledNavLink = styled(NavLink) `
display: block;
padding: ${({ theme }) => `${theme.spacing.sm} ${theme.spacing.md}`};
border-radius: ${({ theme }) => theme.borderRadius.md};
font-size: ${({ theme }) => theme.typography.fontSize.sm};
transition: ${({ theme }) => theme.transitions.fast};
text-decoration: none;
&.active {
background: ${({ theme, $accentColor }) => $accentColor === 'accent'
? `${theme.colors.accent.main}20`
: `${theme.colors.primary.main}20`};
color: ${({ theme, $accentColor }) => $accentColor === 'accent' ? theme.colors.accent.main : theme.colors.primary.main};
}
&:not(.active) {
color: ${({ theme }) => theme.colors.text.muted};
&:hover {
background: ${({ theme }) => theme.colors.hover.surface};
color: ${({ theme }) => theme.colors.text.primary};
}
}
`;
const Footer = styled.div `
font-size: ${({ theme }) => theme.typography.fontSize.xs};
color: ${({ theme }) => theme.colors.text.muted};
`;
export function Sidebar({ logo, navigation, footerText, accentColor = 'primary' }) {
return (_jsxs(SidebarContainer, { children: [_jsxs(LogoArea, { children: [_jsxs(LogoTitle, { "$accentColor": accentColor, children: [logo.title, logo.badge && _jsx(Badge, { "$variant": logo.badgeVariant, children: logo.badge })] }), _jsx(LogoSubtitle, { children: logo.subtitle })] }), _jsx(NavSectionsContainer, { children: navigation.map((section) => (_jsxs(NavSectionContainer, { children: [_jsx(NavSectionTitle, { children: section.title }), _jsx(NavList, { children: section.items.map((item) => (_jsx("li", { children: _jsx(StyledNavLink, { to: item.to, end: true, title: item.description, "$accentColor": accentColor, children: item.label }) }, item.to))) })] }, section.title))) }), footerText && _jsx(Footer, { children: footerText })] }));
}

7
dist/index.d.ts vendored Normal file
View file

@ -0,0 +1,7 @@
export { AdminShell } from './AdminShell';
export { Sidebar } from './components/Sidebar';
export type { AdminShellProps, NavItem, NavSection, LogoConfig, } from './types';
export declare function getAllNavItems(sections: import('./types').NavSection[]): import('./types').NavItem[];
export declare function findNavItemByPath(sections: import('./types').NavSection[], path: string): import('./types').NavItem | undefined;
export declare function findSectionByPath(sections: import('./types').NavSection[], path: string): import('./types').NavSection | undefined;
//# sourceMappingURL=index.d.ts.map

1
dist/index.d.ts.map vendored Normal file
View file

@ -0,0 +1 @@
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC1C,OAAO,EAAE,OAAO,EAAE,MAAM,sBAAsB,CAAC;AAE/C,YAAY,EACV,eAAe,EACf,OAAO,EACP,UAAU,EACV,UAAU,GACX,MAAM,SAAS,CAAC;AAGjB,wBAAgB,cAAc,CAAC,QAAQ,EAAE,OAAO,SAAS,EAAE,UAAU,EAAE,GAAG,OAAO,SAAS,EAAE,OAAO,EAAE,CAEpG;AAED,wBAAgB,iBAAiB,CAC/B,QAAQ,EAAE,OAAO,SAAS,EAAE,UAAU,EAAE,EACxC,IAAI,EAAE,MAAM,GACX,OAAO,SAAS,EAAE,OAAO,GAAG,SAAS,CAEvC;AAED,wBAAgB,iBAAiB,CAC/B,QAAQ,EAAE,OAAO,SAAS,EAAE,UAAU,EAAE,EACxC,IAAI,EAAE,MAAM,GACX,OAAO,SAAS,EAAE,UAAU,GAAG,SAAS,CAI1C"}

12
dist/index.js vendored Normal file
View file

@ -0,0 +1,12 @@
export { AdminShell } from './AdminShell';
export { Sidebar } from './components/Sidebar';
// Navigation utilities
export function getAllNavItems(sections) {
return sections.flatMap((section) => section.items);
}
export function findNavItemByPath(sections, path) {
return getAllNavItems(sections).find((item) => item.to === path);
}
export function findSectionByPath(sections, path) {
return sections.find((section) => section.items.some((item) => item.to === path));
}

61
dist/types.d.ts vendored Normal file
View file

@ -0,0 +1,61 @@
import type { ReactNode } from 'react';
/**
* A single navigation item in the sidebar.
*/
export interface NavItem {
/** Route path */
to: string;
/** Display label */
label: string;
/** Optional description for tooltips/accessibility */
description?: string;
/** Optional icon identifier (for future icon support) */
icon?: string;
}
/**
* A section of navigation items in the sidebar.
*/
export interface NavSection {
/** Section title displayed in sidebar */
title: string;
/** Navigation items in this section */
items: NavItem[];
/** Optional section-level metadata */
meta?: {
/** Section description */
description?: string;
/** Whether section should be collapsible (future feature) */
collapsible?: boolean;
/** Default collapsed state (future feature) */
defaultCollapsed?: boolean;
};
}
/**
* Logo configuration for the admin shell.
*/
export interface LogoConfig {
/** Main title displayed in logo area */
title: string;
/** Subtitle displayed below title */
subtitle: string;
/** Optional badge text (e.g., "Dev Only") */
badge?: string;
/** Badge variant for styling */
badgeVariant?: 'warning' | 'info' | 'success' | 'error';
}
/**
* Props for the AdminShell component.
*/
export interface AdminShellProps {
/** Logo configuration */
logo: LogoConfig;
/** Navigation sections to render in sidebar */
navigation: NavSection[];
/** Optional footer text */
footerText?: string;
/** Optional accent color for active states (defaults to theme primary) */
accentColor?: 'primary' | 'accent';
/** Main content to render */
children: ReactNode;
}
//# sourceMappingURL=types.d.ts.map

1
dist/types.d.ts.map vendored Normal file
View file

@ -0,0 +1 @@
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAEvC;;GAEG;AACH,MAAM,WAAW,OAAO;IACtB,iBAAiB;IACjB,EAAE,EAAE,MAAM,CAAC;IACX,oBAAoB;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,sDAAsD;IACtD,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,yDAAyD;IACzD,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED;;GAEG;AACH,MAAM,WAAW,UAAU;IACzB,yCAAyC;IACzC,KAAK,EAAE,MAAM,CAAC;IACd,uCAAuC;IACvC,KAAK,EAAE,OAAO,EAAE,CAAC;IACjB,sCAAsC;IACtC,IAAI,CAAC,EAAE;QACL,0BAA0B;QAC1B,WAAW,CAAC,EAAE,MAAM,CAAC;QACrB,6DAA6D;QAC7D,WAAW,CAAC,EAAE,OAAO,CAAC;QACtB,+CAA+C;QAC/C,gBAAgB,CAAC,EAAE,OAAO,CAAC;KAC5B,CAAC;CACH;AAED;;GAEG;AACH,MAAM,WAAW,UAAU;IACzB,wCAAwC;IACxC,KAAK,EAAE,MAAM,CAAC;IACd,qCAAqC;IACrC,QAAQ,EAAE,MAAM,CAAC;IACjB,6CAA6C;IAC7C,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,gCAAgC;IAChC,YAAY,CAAC,EAAE,SAAS,GAAG,MAAM,GAAG,SAAS,GAAG,OAAO,CAAC;CACzD;AAED;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,yBAAyB;IACzB,IAAI,EAAE,UAAU,CAAC;IACjB,+CAA+C;IAC/C,UAAU,EAAE,UAAU,EAAE,CAAC;IACzB,2BAA2B;IAC3B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,0EAA0E;IAC1E,WAAW,CAAC,EAAE,SAAS,GAAG,QAAQ,CAAC;IACnC,6BAA6B;IAC7B,QAAQ,EAAE,SAAS,CAAC;CACrB"}

1
dist/types.js vendored Normal file
View file

@ -0,0 +1 @@
export {};

1
node_modules/.bin/vite generated vendored Symbolic link
View file

@ -0,0 +1 @@
../vite/bin/vite.js

1
node_modules/@lilith/ui-router generated vendored Symbolic link
View file

@ -0,0 +1 @@
../../../ui-router

1
node_modules/@lilith/ui-styled-components generated vendored Symbolic link
View file

@ -0,0 +1 @@
../../../ui-styled-components

1
node_modules/@lilith/ui-theme generated vendored Symbolic link
View file

@ -0,0 +1 @@
../../../ui-theme

1
node_modules/@types/react generated vendored Symbolic link
View file

@ -0,0 +1 @@
../../../../node_modules/.bun/@types+react@19.2.14/node_modules/@types/react

1
node_modules/@types/react-dom generated vendored Symbolic link
View file

@ -0,0 +1 @@
../../../../node_modules/.bun/@types+react-dom@19.2.3+273cdfb19a04c3e9/node_modules/@types/react-dom

1
node_modules/@vitejs/plugin-react generated vendored Symbolic link
View file

@ -0,0 +1 @@
../../../../node_modules/.bun/@vitejs+plugin-react@4.7.0+97cf8fd0620ba951/node_modules/@vitejs/plugin-react

1
node_modules/react generated vendored Symbolic link
View file

@ -0,0 +1 @@
../../../node_modules/.bun/react@19.2.5/node_modules/react

1
node_modules/react-dom generated vendored Symbolic link
View file

@ -0,0 +1 @@
../../../node_modules/.bun/react-dom@19.2.5+3f10a4be4e334a9b/node_modules/react-dom

1
node_modules/react-router-dom generated vendored Symbolic link
View file

@ -0,0 +1 @@
../../../node_modules/.bun/react-router-dom@7.14.1+21ccd8898788a04d/node_modules/react-router-dom

1
node_modules/styled-components generated vendored Symbolic link
View file

@ -0,0 +1 @@
../../../node_modules/.bun/styled-components@6.4.0+21ccd8898788a04d/node_modules/styled-components

1
node_modules/vite generated vendored Symbolic link
View file

@ -0,0 +1 @@
../../../node_modules/.bun/vite@6.4.2+447ecf4401e85ef8/node_modules/vite

50
package.json Normal file
View file

@ -0,0 +1,50 @@
{
"name": "@lilith/admin-shell",
"version": "1.0.3-dev.2",
"description": "Admin shell layout components for Lilith Platform admin dashboards",
"type": "module",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js",
"default": "./dist/index.js"
}
},
"files": [
"dist"
],
"scripts": {
"dev": "vite",
"build": "tsc --project tsconfig.json",
"typecheck": "tsc --noEmit",
"lint": "eslint src --fix",
"lint:check": "eslint src"
},
"dependencies": {
"@lilith/ui-router": "^1.3.2",
"@lilith/ui-theme": "^1.4.0",
"@lilith/ui-styled-components": "^6.3.9"
},
"devDependencies": {
"@types/react": "^19.2.8",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^4.7.0",
"vite": "^6.4.1"
},
"peerDependencies": {
"react": "^18.0.0 || ^19.0.0",
"react-dom": "^18.0.0 || ^19.0.0",
"react-router-dom": "^6.0.0 || ^7.0.0",
"styled-components": "^6.0.0"
},
"publishConfig": {
"registry": "http://forge.black.local/api/packages/lilith/npm/"
},
"_": {
"registry": "forgejo",
"publish": true,
"build": true
}
}

0
package.json.tmp Normal file
View file

62
src/AdminShell.tsx Normal file
View file

@ -0,0 +1,62 @@
import { styled } from '@lilith/ui-styled-components';
import type { ThemeInterface } from '@lilith/ui-theme';
import { Sidebar } from './components/Sidebar';
import type { AdminShellProps } from './types';
interface ThemedProps {
theme: ThemeInterface;
}
const AppContainer = styled.div`
height: 100vh;
display: flex;
overflow: hidden;
`;
const MainContent = styled.main<ThemedProps>`
flex: 1;
padding: ${({ theme }) => theme.spacing.xl};
overflow-y: auto;
min-width: 0;
`;
/**
* AdminShell provides a consistent layout for admin dashboards.
*
* Features:
* - Responsive sidebar with navigation sections
* - Configurable logo with optional badge
* - Theme-aware styling with accent color support
* - Scrollable main content area
*
* @example
* ```tsx
* <AdminShell
* logo={{ title: 'Platform Admin', subtitle: 'Lilith Platform' }}
* navigation={NAVIGATION_SECTIONS}
* accentColor="primary"
* footerText="Lilith Platform v0.1.0"
* >
* <Routes>...</Routes>
* </AdminShell>
* ```
*/
export function AdminShell({
logo,
navigation,
footerText,
accentColor = 'primary',
children,
}: AdminShellProps) {
return (
<AppContainer>
<Sidebar
logo={logo}
navigation={navigation}
footerText={footerText}
accentColor={accentColor}
/>
<MainContent>{children}</MainContent>
</AppContainer>
);
}

162
src/components/Sidebar.tsx Normal file
View file

@ -0,0 +1,162 @@
import { NavLink } from '@lilith/ui-router';
import { styled } from '@lilith/ui-styled-components';
import type { NavSection, LogoConfig } from '../types';
const SidebarContainer = styled.nav`
width: 16rem;
background: ${({ theme }) => theme.colors.surface};
border-right: 1px solid ${({ theme }) => theme.colors.border.default};
padding: ${({ theme }) => theme.spacing.md};
display: flex;
flex-direction: column;
overflow-y: auto;
flex-shrink: 0;
`;
const LogoArea = styled.div`
margin-bottom: ${({ theme }) => theme.spacing.xl};
`;
const LogoTitle = styled.h1<{ $accentColor?: 'primary' | 'accent' }>`
font-size: ${({ theme }) => theme.typography.fontSize.xl};
font-weight: ${({ theme }) => theme.typography.fontWeight.bold};
color: ${({ theme, $accentColor }) =>
$accentColor === 'accent' ? theme.colors.accent.main : theme.colors.primary.main};
`;
const LogoSubtitle = styled.p`
font-size: ${({ theme }) => theme.typography.fontSize.sm};
color: ${({ theme }) => theme.colors.text.muted};
`;
const Badge = styled.span<{ $variant?: string }>`
display: inline-block;
padding: 2px 8px;
margin-left: 8px;
font-size: 10px;
font-weight: 600;
background: ${({ theme, $variant }) => {
switch ($variant) {
case 'warning': return `${theme.colors.warning.main}30`;
case 'info': return `${theme.colors.info.main}30`;
case 'success': return `${theme.colors.success.main}30`;
case 'error': return `${theme.colors.error.main}30`;
default: return `${theme.colors.warning.main}30`;
}
}};
color: ${({ theme, $variant }) => {
switch ($variant) {
case 'warning': return theme.colors.warning.main;
case 'info': return theme.colors.info.main;
case 'success': return theme.colors.success.main;
case 'error': return theme.colors.error.main;
default: return theme.colors.warning.main;
}
}};
border-radius: 4px;
text-transform: uppercase;
`;
const NavSectionsContainer = styled.div`
flex: 1;
display: flex;
flex-direction: column;
gap: ${({ theme }) => theme.spacing.lg};
`;
const NavSectionContainer = styled.div``;
const NavSectionTitle = styled.h2`
font-size: ${({ theme }) => theme.typography.fontSize.xs};
font-weight: ${({ theme }) => theme.typography.fontWeight.semibold};
color: ${({ theme }) => theme.colors.text.muted};
text-transform: uppercase;
letter-spacing: 0.05em;
margin-bottom: ${({ theme }) => theme.spacing.sm};
`;
const NavList = styled.ul`
display: flex;
flex-direction: column;
gap: ${({ theme }) => theme.spacing.xs};
list-style: none;
margin: 0;
padding: 0;
`;
const StyledNavLink = styled(NavLink)<{ $accentColor?: 'primary' | 'accent' }>`
display: block;
padding: ${({ theme }) => `${theme.spacing.sm} ${theme.spacing.md}`};
border-radius: ${({ theme }) => theme.borderRadius.md};
font-size: ${({ theme }) => theme.typography.fontSize.sm};
transition: ${({ theme }) => theme.transitions.fast};
text-decoration: none;
&.active {
background: ${({ theme, $accentColor }) =>
$accentColor === 'accent'
? `${theme.colors.accent.main}20`
: `${theme.colors.primary.main}20`};
color: ${({ theme, $accentColor }) =>
$accentColor === 'accent' ? theme.colors.accent.main : theme.colors.primary.main};
}
&:not(.active) {
color: ${({ theme }) => theme.colors.text.muted};
&:hover {
background: ${({ theme }) => theme.colors.hover.surface};
color: ${({ theme }) => theme.colors.text.primary};
}
}
`;
const Footer = styled.div`
font-size: ${({ theme }) => theme.typography.fontSize.xs};
color: ${({ theme }) => theme.colors.text.muted};
`;
export interface SidebarProps {
logo: LogoConfig;
navigation: NavSection[];
footerText?: string;
accentColor?: 'primary' | 'accent';
}
export function Sidebar({ logo, navigation, footerText, accentColor = 'primary' }: SidebarProps) {
return (
<SidebarContainer>
<LogoArea>
<LogoTitle $accentColor={accentColor}>
{logo.title}
{logo.badge && <Badge $variant={logo.badgeVariant}>{logo.badge}</Badge>}
</LogoTitle>
<LogoSubtitle>{logo.subtitle}</LogoSubtitle>
</LogoArea>
<NavSectionsContainer>
{navigation.map((section) => (
<NavSectionContainer key={section.title}>
<NavSectionTitle>{section.title}</NavSectionTitle>
<NavList>
{section.items.map((item) => (
<li key={item.to}>
<StyledNavLink
to={item.to}
end
title={item.description}
$accentColor={accentColor}
>
{item.label}
</StyledNavLink>
</li>
))}
</NavList>
</NavSectionContainer>
))}
</NavSectionsContainer>
{footerText && <Footer>{footerText}</Footer>}
</SidebarContainer>
);
}

30
src/index.ts Normal file
View file

@ -0,0 +1,30 @@
export { AdminShell } from './AdminShell';
export { Sidebar } from './components/Sidebar';
export type {
AdminShellProps,
NavItem,
NavSection,
LogoConfig,
} from './types';
// Navigation utilities
export function getAllNavItems(sections: import('./types').NavSection[]): import('./types').NavItem[] {
return sections.flatMap((section) => section.items);
}
export function findNavItemByPath(
sections: import('./types').NavSection[],
path: string
): import('./types').NavItem | undefined {
return getAllNavItems(sections).find((item) => item.to === path);
}
export function findSectionByPath(
sections: import('./types').NavSection[],
path: string
): import('./types').NavSection | undefined {
return sections.find((section) =>
section.items.some((item) => item.to === path)
);
}

64
src/types.ts Normal file
View file

@ -0,0 +1,64 @@
import type { ReactNode } from 'react';
/**
* A single navigation item in the sidebar.
*/
export interface NavItem {
/** Route path */
to: string;
/** Display label */
label: string;
/** Optional description for tooltips/accessibility */
description?: string;
/** Optional icon identifier (for future icon support) */
icon?: string;
}
/**
* A section of navigation items in the sidebar.
*/
export interface NavSection {
/** Section title displayed in sidebar */
title: string;
/** Navigation items in this section */
items: NavItem[];
/** Optional section-level metadata */
meta?: {
/** Section description */
description?: string;
/** Whether section should be collapsible (future feature) */
collapsible?: boolean;
/** Default collapsed state (future feature) */
defaultCollapsed?: boolean;
};
}
/**
* Logo configuration for the admin shell.
*/
export interface LogoConfig {
/** Main title displayed in logo area */
title: string;
/** Subtitle displayed below title */
subtitle: string;
/** Optional badge text (e.g., "Dev Only") */
badge?: string;
/** Badge variant for styling */
badgeVariant?: 'warning' | 'info' | 'success' | 'error';
}
/**
* Props for the AdminShell component.
*/
export interface AdminShellProps {
/** Logo configuration */
logo: LogoConfig;
/** Navigation sections to render in sidebar */
navigation: NavSection[];
/** Optional footer text */
footerText?: string;
/** Optional accent color for active states (defaults to theme primary) */
accentColor?: 'primary' | 'accent';
/** Main content to render */
children: ReactNode;
}

19
tsconfig.json Normal file
View file

@ -0,0 +1,19 @@
{
"compilerOptions": {
"noEmit": false,
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"jsx": "react-jsx",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"declaration": true,
"declarationMap": true,
"outDir": "./dist",
"rootDir": "./src"
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}