Skip to content

Commit 1c1f6ef

Browse files
authored
Merge pull request #106 from codervisor/copilot/implement-spec-185
feat: Create @leanspec/ui-components package (Spec 185 Phase 1-2)
2 parents 77b045a + 215186c commit 1c1f6ef

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

71 files changed

+6337
-159
lines changed
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import type { StorybookConfig } from '@storybook/react-vite';
2+
import { resolve } from 'path';
3+
4+
const config: StorybookConfig = {
5+
stories: ['../stories/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
6+
addons: [
7+
'@storybook/addon-links',
8+
'@storybook/addon-essentials',
9+
'@storybook/addon-interactions',
10+
],
11+
framework: {
12+
name: '@storybook/react-vite',
13+
options: {},
14+
},
15+
viteFinal: async (config) => {
16+
config.resolve = config.resolve || {};
17+
config.resolve.alias = {
18+
...config.resolve.alias,
19+
'@': resolve(__dirname, '../src'),
20+
};
21+
return config;
22+
},
23+
};
24+
25+
export default config;
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import type { Preview } from '@storybook/react';
2+
import '../src/styles.css';
3+
4+
const preview: Preview = {
5+
parameters: {
6+
controls: {
7+
matchers: {
8+
color: /(background|color)$/i,
9+
date: /Date$/i,
10+
},
11+
},
12+
},
13+
};
14+
15+
export default preview;

packages/ui-components/README.md

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
# @leanspec/ui-components
2+
3+
Framework-agnostic, tree-shakeable UI components for LeanSpec.
4+
5+
## Installation
6+
7+
```bash
8+
npm install @leanspec/ui-components
9+
# or
10+
pnpm add @leanspec/ui-components
11+
```
12+
13+
## Usage
14+
15+
```tsx
16+
import { StatusBadge, PriorityBadge, SpecCard, EmptyState, SearchInput } from '@leanspec/ui-components';
17+
import '@leanspec/ui-components/styles.css';
18+
19+
function MyComponent() {
20+
return (
21+
<div>
22+
<SearchInput placeholder="Search specs..." onSearch={(q) => console.log(q)} />
23+
<StatusBadge status="in-progress" />
24+
<PriorityBadge priority="high" />
25+
<SpecCard
26+
spec={{
27+
specNumber: 185,
28+
specName: 'ui-components',
29+
title: 'UI Components',
30+
status: 'in-progress',
31+
priority: 'high',
32+
tags: ['ui', 'components'],
33+
updatedAt: new Date().toISOString(),
34+
}}
35+
/>
36+
</div>
37+
);
38+
}
39+
```
40+
41+
## Components
42+
43+
### Spec Components
44+
45+
- `StatusBadge` - Display spec status with icon
46+
- `PriorityBadge` - Display spec priority with icon
47+
- `SpecCard` - Compact spec card for lists
48+
- `SpecMetadata` - Metadata display card with all spec details
49+
- `TagBadge` - Display a single tag
50+
- `TagList` - Display multiple tags with truncation
51+
52+
### Project Components
53+
54+
- `ProjectAvatar` - Avatar with initials and color from project name
55+
- `ProjectCard` - Project card with avatar, description, stats, and tags
56+
57+
### Stats Components
58+
59+
- `StatsCard` - Single stat card with icon and trend indicator
60+
- `StatsOverview` - Grid of stats cards for project overview
61+
- `ProgressBar` - Horizontal progress bar with variants
62+
63+
### Search & Filter Components
64+
65+
- `SearchInput` - Search input with keyboard shortcut hint
66+
- `FilterSelect` - Dropdown filter component
67+
68+
### Navigation Components
69+
70+
- `ThemeToggle` - Light/dark theme toggle button
71+
- `BackToTop` - Floating scroll-to-top button
72+
73+
### UI Components
74+
75+
- `Avatar` - Avatar with image and fallback
76+
- `Badge` - Base badge component with variants
77+
- `Button` - Button with variants (default, destructive, outline, secondary, ghost, link)
78+
- `Card` - Card container with header, content, footer
79+
- `Input` - Form input field
80+
- `Separator` - Horizontal or vertical divider
81+
- `Skeleton` - Loading placeholder
82+
83+
### Layout Components
84+
85+
- `EmptyState` - Empty state placeholder with icon, title, description, action
86+
- `SpecListSkeleton` - Loading skeleton for spec list
87+
- `SpecDetailSkeleton` - Loading skeleton for spec detail
88+
- `StatsCardSkeleton` - Loading skeleton for stats card
89+
- `KanbanBoardSkeleton` - Loading skeleton for kanban board
90+
- `ProjectCardSkeleton` - Loading skeleton for project card
91+
- `SidebarSkeleton` - Loading skeleton for sidebar
92+
- `ContentSkeleton` - Generic content skeleton
93+
94+
## Hooks
95+
96+
- `useLocalStorage` - Persist state in localStorage
97+
- `useDebounce` - Debounce a value
98+
- `useDebouncedCallback` - Debounce a callback function
99+
- `useTheme` - Theme state management with localStorage persistence
100+
101+
## Utilities
102+
103+
- `cn` - Merge Tailwind CSS classes
104+
- `formatDate` - Format date in readable format
105+
- `formatDateTime` - Format date with time
106+
- `formatRelativeTime` - Format relative time (e.g., "2 days ago")
107+
- `formatDuration` - Format duration between dates
108+
- `getColorFromString` - Generate consistent color from string
109+
- `getContrastColor` - Get contrasting text color for background
110+
- `getInitials` - Get initials from name string
111+
- `PROJECT_COLORS` - Predefined color palette
112+
113+
## Types
114+
115+
All spec-related TypeScript types are exported:
116+
117+
- `Spec`, `LightweightSpec`, `SidebarSpec`
118+
- `SpecStatus`, `SpecPriority`
119+
- `StatsResult`, `DependencyGraph`, etc.
120+
121+
## Development
122+
123+
```bash
124+
# Install dependencies
125+
pnpm install
126+
127+
# Build the library
128+
pnpm build
129+
130+
# Run Storybook
131+
pnpm storybook
132+
133+
# Run tests
134+
pnpm test
135+
```
136+
137+
## License
138+
139+
MIT
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
{
2+
"name": "@leanspec/ui-components",
3+
"version": "0.2.10",
4+
"description": "Framework-agnostic, tree-shakeable UI components for LeanSpec",
5+
"type": "module",
6+
"main": "./dist/index.cjs",
7+
"module": "./dist/index.js",
8+
"types": "./dist/index.d.ts",
9+
"exports": {
10+
".": {
11+
"types": "./dist/index.d.ts",
12+
"import": "./dist/index.js",
13+
"require": "./dist/index.cjs"
14+
},
15+
"./styles.css": "./dist/ui-components.css"
16+
},
17+
"sideEffects": [
18+
"*.css"
19+
],
20+
"files": [
21+
"dist",
22+
"README.md",
23+
"LICENSE"
24+
],
25+
"scripts": {
26+
"dev": "vite build --watch",
27+
"build": "vite build && pnpm build:types",
28+
"build:types": "tsc --emitDeclarationOnly --outDir dist",
29+
"typecheck": "tsc --noEmit",
30+
"test": "vitest run",
31+
"test:watch": "vitest",
32+
"storybook": "storybook dev -p 6006",
33+
"build-storybook": "storybook build"
34+
},
35+
"keywords": [
36+
"leanspec",
37+
"ui",
38+
"components",
39+
"react",
40+
"typescript"
41+
],
42+
"author": "Marvin Zhang",
43+
"license": "MIT",
44+
"repository": {
45+
"type": "git",
46+
"url": "https://github.com/codervisor/lean-spec.git"
47+
},
48+
"bugs": {
49+
"url": "https://github.com/codervisor/lean-spec/issues"
50+
},
51+
"homepage": "https://lean-spec.dev",
52+
"publishConfig": {
53+
"access": "public"
54+
},
55+
"engines": {
56+
"node": ">=20"
57+
},
58+
"peerDependencies": {
59+
"react": ">=18",
60+
"react-dom": ">=18"
61+
},
62+
"dependencies": {
63+
"@radix-ui/react-slot": "^1.2.4",
64+
"class-variance-authority": "^0.7.1",
65+
"clsx": "^2.0.0",
66+
"dayjs": "^1.11.19",
67+
"lucide-react": "^0.553.0",
68+
"tailwind-merge": "^3.4.0"
69+
},
70+
"devDependencies": {
71+
"@storybook/addon-essentials": "^8.6.3",
72+
"@storybook/addon-interactions": "^8.6.3",
73+
"@storybook/addon-links": "^8.6.3",
74+
"@storybook/react": "^8.6.3",
75+
"@storybook/react-vite": "^8.6.3",
76+
"@storybook/test": "^8.6.3",
77+
"@tailwindcss/typography": "^0.5.15",
78+
"@types/node": "^20",
79+
"@types/react": "^19",
80+
"@types/react-dom": "^19",
81+
"@vitejs/plugin-react": "^4.3.4",
82+
"autoprefixer": "^10.4.20",
83+
"postcss": "^8.4.49",
84+
"react": "19.2.0",
85+
"react-dom": "19.2.0",
86+
"storybook": "^8.6.3",
87+
"tailwindcss": "^3.4.17",
88+
"typescript": "^5",
89+
"vite": "^6.0.1",
90+
"vite-plugin-dts": "^4.5.3",
91+
"vitest": "^4.0.6"
92+
}
93+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
export default {
2+
plugins: {
3+
tailwindcss: {},
4+
autoprefixer: {},
5+
},
6+
};
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export * from './ui';
2+
export * from './spec';
3+
export * from './layout';
4+
export * from './project';
5+
export * from './navigation';
6+
export * from './search';
7+
export * from './stats';
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
/**
2+
* EmptyState component for displaying placeholder content when no data is available
3+
*/
4+
5+
import { type LucideIcon } from 'lucide-react';
6+
import { Button } from '@/components/ui/button';
7+
import { cn } from '@/lib/utils';
8+
9+
export interface EmptyStateAction {
10+
label: string;
11+
onClick?: () => void;
12+
href?: string;
13+
}
14+
15+
export interface EmptyStateProps {
16+
/** Icon to display */
17+
icon: LucideIcon;
18+
/** Title text */
19+
title: string;
20+
/** Description text */
21+
description: string;
22+
/** Optional action button */
23+
action?: EmptyStateAction;
24+
/** Additional CSS classes */
25+
className?: string;
26+
}
27+
28+
/**
29+
* Validate that a URL is safe (relative or http/https)
30+
*/
31+
function isSafeUrl(url: string): boolean {
32+
// Allow relative URLs
33+
if (url.startsWith('/') || url.startsWith('#') || url.startsWith('.')) {
34+
return true;
35+
}
36+
// Allow http and https URLs
37+
try {
38+
const parsed = new URL(url);
39+
return parsed.protocol === 'http:' || parsed.protocol === 'https:';
40+
} catch {
41+
return false;
42+
}
43+
}
44+
45+
export function EmptyState({ icon: Icon, title, description, action, className }: EmptyStateProps) {
46+
const safeHref = action?.href && isSafeUrl(action.href) ? action.href : undefined;
47+
48+
return (
49+
<div
50+
className={cn(
51+
'flex flex-col items-center justify-center py-12 px-4 text-center',
52+
className
53+
)}
54+
>
55+
<div className="rounded-full bg-muted p-6 mb-4">
56+
<Icon className="h-12 w-12 text-muted-foreground" />
57+
</div>
58+
<h3 className="text-lg font-semibold mb-2">{title}</h3>
59+
<p className="text-sm text-muted-foreground max-w-md mb-6">{description}</p>
60+
{action && (
61+
<Button onClick={action.onClick} {...(safeHref ? { asChild: true } : {})}>
62+
{safeHref ? (
63+
<a href={safeHref} rel="noopener noreferrer">
64+
{action.label}
65+
</a>
66+
) : (
67+
action.label
68+
)}
69+
</Button>
70+
)}
71+
</div>
72+
);
73+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
export { EmptyState, type EmptyStateProps, type EmptyStateAction } from './empty-state';
2+
export {
3+
SpecListSkeleton,
4+
SpecDetailSkeleton,
5+
StatsCardSkeleton,
6+
KanbanBoardSkeleton,
7+
ProjectCardSkeleton,
8+
SidebarSkeleton,
9+
ContentSkeleton,
10+
type ContentSkeletonProps,
11+
} from './loading-skeletons';

0 commit comments

Comments
 (0)