Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions libs/ui/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
storybook-static/
*.storybook.log
8 changes: 8 additions & 0 deletions libs/ui/.storybook/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import type { StorybookConfig } from '@storybook/react-vite';

const config: StorybookConfig = {
framework: '@storybook/react-vite',
stories: ['../src/**/*.stories.tsx'],
};

export default config;
41 changes: 41 additions & 0 deletions libs/ui/.storybook/preview.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import type { Preview } from '@storybook/react-vite';
import '../src/tokens/tokens.css';

const preview: Preview = {
globalTypes: {
theme: {
description: 'Design-token color theme',
toolbar: {
title: 'Theme',
icon: 'mirror',
items: ['light', 'dark'],
dynamicTitle: true,
},
},
},
initialGlobals: {
theme: 'light',
},
decorators: [
(Story, context) => {
// Same mechanism as ThemeProvider: tokens.css keys dark overrides
// off <html data-theme="dark">.
document.documentElement.setAttribute('data-theme', context.globals.theme);
return (
<div
style={{
background: 'var(--bg)',
color: 'var(--ink)',
padding: 16,
minHeight: '100vh',
boxSizing: 'border-box',
}}
>
<Story />
</div>
);
},
],
};

export default preview;
9 changes: 7 additions & 2 deletions libs/ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,19 @@
"*.css"
],
"scripts": {
"typecheck": "tsc --noEmit -p tsconfig.json"
"typecheck": "tsc --noEmit -p tsconfig.json",
"storybook": "storybook dev -p 6006",
"build-storybook": "storybook build"
},
"peerDependencies": {
"react": "^19.0.0",
"react-dom": "^19.0.0"
},
"devDependencies": {
"@storybook/react-vite": "^10.5.0",
"@types/react": "^19.2.7",
"typescript": "^6.0.2"
"storybook": "^10.5.0",
"typescript": "^6.0.2",
"vite": "^7.3.6"
}
}
4 changes: 4 additions & 0 deletions libs/ui/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
// Primitives (U0.5)
export { Badge } from './primitives/Badge';
export type { BadgeProps } from './primitives/Badge';
export { Button } from './primitives/Button';
export type { ButtonProps } from './primitives/Button';
export { Input } from './primitives/Input';
export type { InputProps } from './primitives/Input';
export { Kbd } from './primitives/Kbd';
export type { KbdProps } from './primitives/Kbd';
export { ThemeProvider, useTheme } from './theme/ThemeProvider';
Expand Down
42 changes: 42 additions & 0 deletions libs/ui/src/primitives/Badge.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import type { Meta, StoryObj } from '@storybook/react-vite';
import { Badge } from './Badge';

const meta = {
title: 'Primitives/Badge',
component: Badge,
args: { children: 'Coder', tone: 'info', size: 'md' },
} satisfies Meta<typeof Badge>;

export default meta;
type Story = StoryObj<typeof meta>;

export const Playground: Story = {};

export const AllTones: Story = {
render: () => (
<div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
<Badge tone="good">QA passed</Badge>
<Badge tone="info">Coder</Badge>
<Badge tone="warn">Recovery</Badge>
<Badge tone="bad">Error</Badge>
<Badge tone="neutral">Draft</Badge>
</div>
),
};

export const CardChipSize: Story = {
name: 'Size sm (card chip)',
render: () => (
<div style={{ display: 'flex', gap: 6, alignItems: 'center' }}>
<Badge size="sm" tone="neutral">
Draft
</Badge>
<Badge size="sm" tone="info">
Coder
</Badge>
<Badge size="sm" tone="good">
Merged
</Badge>
</div>
),
};
31 changes: 31 additions & 0 deletions libs/ui/src/primitives/Button.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
.ac-button {
display: inline-flex;
align-items: center;
gap: 6px;
min-height: 34px;
padding: 0 12px;
border: 1px solid var(--line-strong);
border-radius: 8px;
background: var(--panel);
color: var(--ink);
font-family: var(--font-sans);
font-size: 12.5px;
font-weight: 720;
cursor: pointer;
}

.ac-button:disabled {
cursor: default;
opacity: 0.55;
}

.ac-button:focus-visible {
outline: 2px solid var(--blue);
outline-offset: -1px;
}

.ac-button--primary {
border-color: var(--ink);
background: var(--ink);
color: var(--bg);
}
25 changes: 25 additions & 0 deletions libs/ui/src/primitives/Button.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import type { Meta, StoryObj } from '@storybook/react-vite';
import { Button } from './Button';

const meta = {
title: 'Primitives/Button',
component: Button,
args: { children: 'Import spec', variant: 'default' },
} satisfies Meta<typeof Button>;

export default meta;
type Story = StoryObj<typeof meta>;

export const Playground: Story = {};

export const HeaderActions: Story = {
name: 'Header actions (mock)',
render: () => (
<div style={{ display: 'flex', gap: 8 }}>
<Button>Import spec</Button>
<Button>Bulk QA</Button>
<Button variant="primary">+ New spec</Button>
<Button disabled>Disabled</Button>
</div>
),
};
28 changes: 28 additions & 0 deletions libs/ui/src/primitives/Button.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import type { ButtonHTMLAttributes } from 'react';
import './Button.css';

export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
/** 'primary' for the main call-to-action; 'default' for everything else. */
variant?: 'default' | 'primary';
}

/**
* Standard action button from the `.lazyweb` design language (header
* actions like "Import spec" / "+ New spec"). Defaults to type="button";
* remaining button attributes are forwarded.
*/
export function Button({
variant = 'default',
className,
children,
...rest
}: Readonly<ButtonProps>) {
const classes = `ac-button${
variant === 'primary' ? ' ac-button--primary' : ''
}${className ? ` ${className}` : ''}`;

Check warning on line 22 in libs/ui/src/primitives/Button.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this code to not use nested template literals.

See more on https://sonarcloud.io/project/issues?id=OBenner_Auto-Coding&issues=AZ9U6gPnemFbw1K7eTWn&open=AZ9U6gPnemFbw1K7eTWn&pullRequest=413
return (
<button type="button" className={classes} {...rest}>
{children}
</button>
);
}
24 changes: 24 additions & 0 deletions libs/ui/src/primitives/Input.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
.ac-input {
min-height: 34px;
padding: 0 10px;
border: 1px solid var(--line);
border-radius: 8px;
background: var(--soft);
color: var(--ink);
font-family: var(--font-sans);
font-size: 12.5px;
}

.ac-input::placeholder {
color: var(--muted);
}

.ac-input:focus-visible {
border-color: var(--blue);
outline: 2px solid var(--blue);
outline-offset: -1px;
}

.ac-input:disabled {
opacity: 0.55;
}
17 changes: 17 additions & 0 deletions libs/ui/src/primitives/Input.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import type { Meta, StoryObj } from '@storybook/react-vite';
import { Input } from './Input';

const meta = {
title: 'Primitives/Input',
component: Input,
args: { placeholder: 'Describe your task…' },
} satisfies Meta<typeof Input>;

export default meta;
type Story = StoryObj<typeof meta>;

export const Playground: Story = {};

export const Disabled: Story = {
args: { disabled: true, value: 'Read-only value' },
};
17 changes: 17 additions & 0 deletions libs/ui/src/primitives/Input.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import type { InputHTMLAttributes } from 'react';
import './Input.css';

export type InputProps = InputHTMLAttributes<HTMLInputElement>;

/**
* Single-line text input in the `.lazyweb` field style (soft surface,
* hairline border, visible focus ring). All input attributes forward.
*/
export function Input({ className, ...rest }: Readonly<InputProps>) {
return (
<input
className={className ? `ac-input ${className}` : 'ac-input'}
{...rest}
/>
);
}
13 changes: 13 additions & 0 deletions libs/ui/src/primitives/Kbd.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import type { Meta, StoryObj } from '@storybook/react-vite';
import { Kbd } from './Kbd';

const meta = {
title: 'Primitives/Kbd',
component: Kbd,
args: { children: '⌘K' },
} satisfies Meta<typeof Kbd>;

export default meta;
type Story = StoryObj<typeof meta>;

export const Playground: Story = {};
14 changes: 14 additions & 0 deletions libs/ui/src/screens/BoardSkeleton.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import type { Meta, StoryObj } from '@storybook/react-vite';
import { BoardSkeleton } from './BoardSkeleton';

const meta = {
title: 'Screens/BoardSkeleton',
component: BoardSkeleton,
parameters: { layout: 'fullscreen' },
args: { label: 'Loading tasks…' },
} satisfies Meta<typeof BoardSkeleton>;

export default meta;
type Story = StoryObj<typeof meta>;

export const Loading: Story = {};
43 changes: 43 additions & 0 deletions libs/ui/src/screens/BoardView.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import type { Meta, StoryObj } from '@storybook/react-vite';
import { BoardView, buildBoardViewLabels } from './BoardView';
import { SAMPLE_TASKS } from './__fixtures__/sample-tasks';

const LABELS = buildBoardViewLabels((key, opts) => {
const map: Record<string, string> = {
'tb.searchPlaceholder': 'Search specs, files, agents…',
'tb.searchLabel': 'Search specs',
'tb.filtersLabel': 'Filter tasks',
'tb.viewsLabel': 'Select view',
'tb.noMatches': 'No tasks match your search or filter',
'tb.filters.all': 'All projects',
'tb.filters.running': 'Running',
'tb.filters.review': 'Needs review',
'tb.views.board': 'Board',
'tb.views.table': 'Table',
'tb.views.timeline': 'Timeline',
};
if (key === 'tb.matchCount') return `${opts?.count} matching tasks`;
return map[key] ?? key;
}, 'tb');

const meta = {
title: 'Screens/BoardView',
component: BoardView,
parameters: { layout: 'fullscreen' },
args: {
tasks: SAMPLE_TASKS,
labels: LABELS,
onSelectTask: () => {},
emptyTitle: 'No tasks yet',
emptyDescription: 'Create your first spec to see it on the board.',
},
} satisfies Meta<typeof BoardView>;

export default meta;
type Story = StoryObj<typeof meta>;

export const FilterableBoard: Story = {};

export const EmptyState: Story = {
args: { tasks: [] },
};
19 changes: 19 additions & 0 deletions libs/ui/src/screens/KanbanBoard.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import type { Meta, StoryObj } from '@storybook/react-vite';
import { KanbanBoard } from './KanbanBoard';
import { SAMPLE_TASKS } from './__fixtures__/sample-tasks';

const meta = {
title: 'Screens/KanbanBoard',
component: KanbanBoard,
parameters: { layout: 'fullscreen' },
args: { tasks: SAMPLE_TASKS },
} satisfies Meta<typeof KanbanBoard>;

export default meta;
type Story = StoryObj<typeof meta>;

export const Board: Story = {};

export const EmptyColumns: Story = {
args: { tasks: [] },
};
Loading
Loading