@ronnydrori/jupiter-ui
v1.0.5
Published
Jupiter Design System - Plug and play UI components with Mantine and Emotion
Readme
Jupiter UI Design System
A comprehensive design system built with React, TypeScript, Mantine v7, and Storybook. This library provides a complete set of design tokens, themes, and reusable components for building consistent user interfaces.
🏗️ Architecture
Directory Structure
libs/jupiter-ui/
├── .storybook/ # Storybook configuration
│ ├── main.ts # Storybook main config
│ ├── preview.tsx # Global decorators & theme setup
│ └── preview.css # Preview styling
├── src/
│ ├── design-system/
│ │ ├── tokens/ # Design tokens (spacing, typography, etc.)
│ │ ├── theme/ # Theme system & providers
│ │ ├── components/ # React components
│ │ ├── hooks/ # Custom React hooks
│ │ └── utils/ # Utility functions
│ └── index.ts # Main export file
└── package.json🛠️ Technology Choices
This design system was built with the following technologies, chosen for their balance of ease of use, customization, and team familiarity:
UI Foundation: Mantine v7
- Why Mantine? Easy integration with 100+ pre-built components, flexible theme API with CSS variables, and 40+ included hooks. Provides good customization without fighting defaults, and excellent TypeScript support.
- Benefits: Faster development with ready-to-use components and extensible theming.
CSS Strategy: Emotion
- Why Emotion? Zero learning curve for our CSS-in-JS experienced team. Works seamlessly with Mantine, provides type-safe styling, dynamic theming capabilities, and co-located styles with components.
- Benefits: Full JavaScript power for styling, theme access via props, and easy refactoring.
File Organization: Feature-Based Structure
- Why Feature-Based? Components are self-contained in their own folders, making it easy to add, modify, or remove features without affecting others. Scales well for medium-sized component libraries.
- Benefits: Intuitive organization, easy maintenance, and clear boundaries between components.
Icons: Lucide + Phosphor
- Why These Libraries? Both provide excellent style control (size, color, stroke), MIT license, and tree-shaking support. Lucide for general UI icons, Phosphor for weight variants when needed.
- Benefits: Consistent iconography, small bundle size (~200-250b per icon), and flexible styling.
Overall Stack Rationale
- Balanced Approach: Prioritizes speed of development while maintaining design control and customization.
- Team Fit: Leverages existing CSS-in-JS expertise to minimize learning curve.
- Scalability: Feature-based organization supports growth from 20-40 components to more.
- Modern Standards: TypeScript-first, accessible components, and responsive design.
- Dynamic Theming: Supports runtime palette loading via environment variables for flexible theming (e.g., yanai.json, rotem.json).
� Design Tokens
All design tokens are defined in src/design-system/tokens/:
- Spacing (
spacing.ts) - Consistent spacing scale (0.25rem - 20rem) - Typography (
typography.ts) - Font families, sizes, weights, line heights - Radius (
radius.ts) - Border radius values (xs, sm, md, lg, xl) - Borders (
borders.ts) - Border width values - Shadows (
shadows.ts) - Box shadow definitions (xs, sm, md, lg, xl) - Breakpoints (
breakpoints.ts) - Responsive breakpoints (xs, sm, md, lg, xl)
Example Token File: spacing.ts
import type { TokenScaleKey } from './types';
export const spacing = {
0: '0px',
1: '4px',
2: '8px',
3: '12px',
4: '16px',
5: '20px',
6: '24px',
7: '28px',
8: '32px',
9: '36px',
} as const;
export const semanticSpacing: Partial<Record<TokenScaleKey, string>> = {
xs: spacing[1], // 4px
sm: spacing[2], // 8px
md: spacing[4], // 16px
lg: spacing[6], // 24px
xl: spacing[8], // 32px
} as const;
export type SpacingToken = keyof typeof spacing;
export type SemanticSpacingToken = keyof typeof semanticSpacing;🎭 Theme System
Theme Structure
The theme system is located in src/design-system/theme/:
- types.ts - TypeScript types for theme structure
- theme.ts - Core theme creator using design tokens
- mantine.ts - Mantine theme configuration
- ThemeProvider.tsx - React context provider for theme management
Dynamic Palette Loading
The design system supports dynamic color palette loading at runtime:
- usePalette Hook: Loads color palettes asynchronously from JSON files
- Environment Variable: Set
VITE_PROJECT_NAMEto specify the project name (e.g.,yanaiorrotem) - Palette Path: Fetches from
/color-palettes/${VITE_PROJECT_NAME}.json - Fallback: Defaults to
defaultPalette.tsif no environment variable is set or loading fails
// Example: Loading a palette
const colors = usePalette(); // Loads from /color-palettes/${VITE_PROJECT_NAME}.json or defaults to defaultPaletteUsing Themes
import { JupiterThemeProvider } from '@libraries/jupiter-ui';
function App() {
return (
<JupiterThemeProvider>
{/* Your app */}
</JupiterThemeProvider>
);
}Theme Context Hook
import { useThemeContext } from '@libraries/jupiter-ui';
function MyComponent() {
const { mode, toggleMode, setMode } = useThemeContext();
// mode: 'light' | 'dark'
// toggleMode: () => void
// setMode: (mode: ThemeMode) => void
}🧩 Components
Component Guidelines
All components are located in src/design-system/components/[ComponentName]/:
- No index.ts files - Import directly from component files
- Component file:
ComponentName.tsx - Test file:
ComponentName.test.tsx- REQUIRED (enforced by ESLint) - Stories file:
ComponentName.stories.tsx - Export from main index: Update
src/index.ts - Set
displayNameon exported React components and higher-order components (e.g.,ComponentName.displayName = 'ComponentName'; internal helper components don't need this)
⚠️ Testing Requirement: Every component must have a corresponding test file. This is enforced by ESLint's
require-test-filerule and will prevent commits without tests.
Component Structure
// JpButton.tsx
import { Button as MantineButton } from '@mantine/core';
import type { ButtonProps as MantineButtonProps } from '@mantine/core';
import { buttonVariants, type JupiterVariant } from '../utils/variant-factory';
export interface JpButtonProps extends Omit<MantineButtonProps, 'variant'> {
variant?: JupiterVariant;
}
export function JpButton({ variant = 'primary', ...props }: JpButtonProps) {
return (
<MantineButton
variant={buttonVariants[variant] ?? buttonVariants.primary}
{...props}
/>
);
}
// Required: set the display name for better DX in React DevTools, Storybook, and logs
JpButton.displayName = 'JpButton';Custom Styling with Emotion (Function Syntax)
Always use Emotion's function form for styled components, not tagged template strings. This ensures better type safety, prop filtering, and consistent patterns across the library.
import styled from '@emotion/styled';
// Optional: filter invalid DOM props when styling host elements
// import isPropValid from '@emotion/is-prop-valid';
import { JpButton, useThemeContext } from '@libraries/jupiter-ui';
// Create a styled button with custom styles (function approach)
const StyledButton = styled(JpButton, { label: 'styled-button' })(() => ({
backgroundColor: 'hotpink',
color: 'white',
'&:hover': {
backgroundColor: 'deeppink',
},
// Use static selectors to style inner elements
'& .mantine-Button-label': {
fontWeight: 'bold',
textTransform: 'uppercase',
},
}));
// Create a themed button using design tokens (function approach)
const ThemedButton = styled(JpButton, { label: 'themed-button' })(({ theme }) => ({
backgroundColor: '#3b82f6',
color: 'white',
borderRadius: theme?.radius?.md || '0.5rem',
padding: `${theme?.spacing?.md || '0.75rem'} ${theme?.spacing?.lg || '1rem'}`,
fontWeight: theme?.typography?.fontWeights?.semibold || 600,
'&:hover': {
backgroundColor: '#2563eb',
transform: 'translateY(-1px)',
boxShadow: theme?.shadows?.md || '0 4px 6px rgba(0,0,0,0.1)',
},
// Style inner elements
'& .mantine-Button-label': {
fontFamily: theme?.typography?.fontFamilies?.body || 'system-ui',
},
}));
function Demo() {
return (
<div>
<StyledButton>Styled with Emotion</StyledButton>
<ThemedButton>
Themed Button
</ThemedButton>
</div>
);
}Key Points:
- Use Emotion's function syntax:
styled(Component, options)(props => styles) - Do not use tagged template strings (e.g.,
styled.div`...,styled(Button)`...) - Use Mantine's class selectors (e.g.,
.mantine-Button-label) to target inner elements - Access theme tokens via
props.theme(pass theme fromuseThemeContext()) - Use transient props (
$propName) to avoid passing custom props to DOM - All Mantine component props still work on styled components
- Styled components can be used in Storybook stories
Storybook Stories
// ComponentName.stories.tsx
import type { Meta, StoryObj } from '@storybook/react';
import { ComponentName } from './ComponentName';
const meta = {
title: 'Components/ComponentName',
component: ComponentName,
parameters: {
layout: 'centered',
},
tags: ['autodocs'],
argTypes: {
variant: {
control: 'select',
options: ['primary', 'secondary', 'outline', 'ghost'],
},
},
} satisfies Meta<typeof ComponentName>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Primary: Story = {
args: {
variant: 'primary',
children: 'Component Text',
},
};📚 Storybook
Configuration
- Version: v8.6.15
- Framework: React + Vite
- Addons:
storybook-addon-mantine- Mantine theme integration- Standard Storybook addons (essentials, interactions, etc.)
Running Storybook
npm run storybookStorybook will be available at http://localhost:6006
🔧 Technology Stack
- React v18.3.1
- TypeScript v5.5.4 (strict mode)
- Mantine v7.15.1 (UI component library)
- Emotion v11.13.5 (CSS-in-JS styling)
- Storybook v8.6.14
- Vite v5.4.11 (build tool)
- Vitest v3.2.4 (testing framework)
- Lucide + Phosphor (icons)
📝 Coding Conventions
Imports
- Use named exports (not default exports except for Storybook meta)
- Import components directly:
import { Button } from './components/Button/Button' - No intermediate index files in component folders
TypeScript
- Use explicit types for all public APIs
- Export interface definitions alongside components
- Use
typefor type aliases,interfacefor object shapes - No
anytypes allowed
File Naming
- Components: PascalCase (e.g.,
Button.tsx) - Stories:
ComponentName.stories.tsx - Utils/Tokens: camelCase (e.g.,
colors.ts)
Component Variants
Use semantic variant names that describe intent, not implementation:
- ✅
primary,secondary,outline,ghost - ❌
filled,light,subtle(Mantine internal variants)
Map semantic variants to Mantine variants inside components.
🎯 Best Practices
Theme Provider
- Always wrap components in
JupiterThemeProviderwhen usinguseThemeContext() - ThemeProvider provides theme context and token access to all components
Mantine Integration
- Extend Mantine components, don't replace them
- Override Mantine's theme through
createAppTheme()function - Use Mantine's utility functions (e.g.,
rem(),em())
State Management
- Avoid
useEffectfor syncing props to state - Use render-time state updates:
if (prop !== state) setState(prop) - This prevents cascading renders and React act() warnings
🚀 Development Workflow
- Add new tokens in
src/design-system/tokens/ - Update theme in
src/design-system/theme/if needed - Create component in
src/design-system/components/[Name]/ - Add stories in same folder as component
- Export from main in
src/index.ts - Test in Storybook with
npm run storybook - Commit using conventional commits format
📦 Export Pattern
All exports go through src/index.ts:
// Tokens
export * from './design-system/tokens/spacing';
export * from './design-system/tokens/radius';
export * from './design-system/tokens/borders';
export * from './design-system/tokens/typography';
export * from './design-system/tokens/shadows';
export * from './design-system/tokens/breakpoints';
// Theme
export * from './design-system/theme/types';
export * from './design-system/theme/mantine';
export {
JupiterThemeProvider,
useThemeContext,
} from './design-system/theme/ThemeProvider';
export type { JupiterThemeProviderProps } from './design-system/theme/ThemeProvider';
// Utils
export {
buttonVariants,
inputVariants,
selectVariants,
checkboxVariants,
} from './design-system/utils/variant-factory';
export type { JupiterVariant } from './design-system/utils/variant-factory';
// Components
export { JpButton } from './design-system/components/Button/JpButton';
export type { JpButtonProps } from './design-system/components/Button/JpButton';
// Hooks
export * from './design-system/hooks/usePalette';Scripts
# Development
npm run storybook # Start Storybook on localhost:6006
npm run build-storybook # Build static Storybook
# Linting
npm run lint # Run ESLint
npm run lint:fix # Fix ESLint issues
# Type Checking
npm run typecheck # Run TypeScript compiler check
# Testing
npm run test # Run unit tests with Vitest
npm run test-storybook # Run storybook tests in the cli📖 Official Documentation
Storybook Interaction Tests
https://storybook.js.org/docs/writing-tests/interaction-testingStorybook Vitest Addon
https://storybook.js.org/docs/8/writing-tests/test-addon
🔒 ESLint Rules
This library enforces strict rules to maintain quality and plug-and-play architecture:
TypeScript Rules
- ❌
@typescript-eslint/no-explicit-any: Noanytypes allowed - ✅
@typescript-eslint/consistent-type-imports: Prefer type imports for better tree-shaking - ✅
@typescript-eslint/prefer-nullish-coalescing: Use??over||for null/undefined checks - ✅
@typescript-eslint/prefer-optional-chain: Use optional chaining (?.) instead of manual checks - ⚠️
@typescript-eslint/no-non-null-assertion: Warn on non-null assertions (!)
Import Restrictions
- ❌ No imports from other monorepo libraries (
@libraries/*except@libraries/jupiter-ui) - ❌ No imports from apps (
apps/*) - ❌ No imports from other libs (
libs/*) - ❌ No relative imports outside
src/directory - ✅ Only allowed external dependencies:
@mantine/*,@emotion/*,@phosphor-icons/react,lucide-react,vitest,react,react-dom,@testing-library/*,@storybook/*,@types/*
Component Requirements
- ✅ Every component must have a corresponding
.test.tsxfile (enforced bytest-file/require-test-filerule) - ✅ Set
displayNameon exported React components for better DX in DevTools and logs
React Rules
- ✅ All React recommended rules
- ✅ React Hooks recommended rules
- ✅ JSX runtime rules
Other
- ✅ Fully type-safe
- ✅ Plug-and-play architecture
🤝 Contributing
When adding components:
- Create component folder in
src/design-system/components/[Name]/ - Create
Name.tsxwith component implementation - Create
Name.stories.tsxwith comprehensive stories - Export component and types from
src/index.ts - Ensure no external monorepo dependencies
- Follow semantic variant naming
- Add proper TypeScript types
