Merge pull request #20 from johndoe6345789/copilot/refactor-ui-boilerplate

Implement atomic component library with features.json-driven Storybook stories
This commit is contained in:
2026-01-08 15:12:16 +00:00
committed by GitHub
20 changed files with 702 additions and 20 deletions

View File

@@ -0,0 +1,43 @@
import type { Meta, StoryObj } from '@storybook/react';
import featuresConfig from '@/config/features.json';
import ConfirmDialog from './ConfirmDialog';
const meta = {
title: 'Admin/ConfirmDialog (From JSON)',
component: ConfirmDialog,
parameters: {
layout: 'centered',
docs: {
description: {
component: 'ConfirmDialog component with stories dynamically loaded from features.json',
},
},
},
tags: ['autodocs'],
} satisfies Meta<typeof ConfirmDialog>;
export default meta;
type Story = StoryObj<typeof meta>;
// Dynamically generate stories from features.json
const confirmDialogStories = featuresConfig.storybookStories.ConfirmDialog;
// Default Story
export const Default: Story = {
name: confirmDialogStories.default.name,
args: {
...confirmDialogStories.default.args,
onConfirm: () => console.log('Confirmed'),
onCancel: () => console.log('Cancelled'),
},
};
// Delete Warning Story
export const DeleteWarning: Story = {
name: confirmDialogStories.deleteWarning.name,
args: {
...confirmDialogStories.deleteWarning.args,
onConfirm: () => console.log('Confirmed delete'),
onCancel: () => console.log('Cancelled delete'),
},
};

View File

@@ -0,0 +1,41 @@
import type { Meta, StoryObj } from '@storybook/react';
import ConfirmDialog from './ConfirmDialog';
const meta = {
title: 'Admin/ConfirmDialog',
component: ConfirmDialog,
parameters: {
layout: 'centered',
},
tags: ['autodocs'],
} satisfies Meta<typeof ConfirmDialog>;
export default meta;
type Story = StoryObj<typeof meta>;
// Stories based on features.json storybookStories.ConfirmDialog
export const Default: Story = {
name: 'Default',
args: {
open: true,
title: 'Confirm Action',
message: 'Are you sure you want to proceed?',
confirmLabel: 'Confirm',
cancelLabel: 'Cancel',
onConfirm: () => console.log('Confirmed'),
onCancel: () => console.log('Cancelled'),
},
};
export const DeleteWarning: Story = {
name: 'Delete Warning',
args: {
open: true,
title: 'Delete Item',
message: 'This action cannot be undone. Are you sure you want to delete this item?',
confirmLabel: 'Delete',
cancelLabel: 'Cancel',
onConfirm: () => console.log('Confirmed delete'),
onCancel: () => console.log('Cancelled delete'),
},
};

View File

@@ -1,13 +1,13 @@
'use client'; 'use client';
import { import {
Button,
Dialog, Dialog,
DialogActions, DialogActions,
DialogContent, DialogContent,
DialogContentText, DialogContentText,
DialogTitle, DialogTitle,
} from '@mui/material'; } from '@mui/material';
import Button from '../atoms/Button';
type ConfirmDialogProps = { type ConfirmDialogProps = {
open: boolean; open: boolean;
@@ -35,10 +35,8 @@ export default function ConfirmDialog({
<DialogContentText>{message}</DialogContentText> <DialogContentText>{message}</DialogContentText>
</DialogContent> </DialogContent>
<DialogActions> <DialogActions>
<Button onClick={onCancel}>{cancelLabel}</Button> <Button onClick={onCancel} text={cancelLabel} />
<Button onClick={onConfirm} color="error" variant="contained"> <Button onClick={onConfirm} color="error" variant="contained" text={confirmLabel} />
{confirmLabel}
</Button>
</DialogActions> </DialogActions>
</Dialog> </Dialog>
); );

View File

@@ -0,0 +1,45 @@
import type { Meta, StoryObj } from '@storybook/react';
import featuresConfig from '@/config/features.json';
import DataGrid from './DataGrid';
const meta = {
title: 'Admin/DataGrid (From JSON)',
component: DataGrid,
parameters: {
layout: 'padded',
docs: {
description: {
component: 'DataGrid component with stories dynamically loaded from features.json',
},
},
},
tags: ['autodocs'],
} satisfies Meta<typeof DataGrid>;
export default meta;
type Story = StoryObj<typeof meta>;
// Dynamically generate stories from features.json
const dataGridStories = featuresConfig.storybookStories.DataGrid;
// Default Story
export const Default: Story = {
name: dataGridStories.default.name,
args: dataGridStories.default.args,
};
// With Actions Story
export const WithActions: Story = {
name: dataGridStories.withActions.name,
args: {
...dataGridStories.withActions.args,
onEdit: () => console.log('Edit clicked'),
onDelete: () => console.log('Delete clicked'),
},
};
// Empty State
export const Empty: Story = {
name: dataGridStories.empty.name,
args: dataGridStories.empty.args,
};

View File

@@ -0,0 +1,61 @@
import type { Meta, StoryObj } from '@storybook/react';
import DataGrid from './DataGrid';
const meta = {
title: 'Admin/DataGrid',
component: DataGrid,
parameters: {
layout: 'padded',
},
tags: ['autodocs'],
} satisfies Meta<typeof DataGrid>;
export default meta;
type Story = StoryObj<typeof meta>;
// Stories based on features.json storybookStories.DataGrid
export const Default: Story = {
name: 'Default',
args: {
columns: [
{ name: 'id', label: 'ID' },
{ name: 'name', label: 'Name' },
{ name: 'email', label: 'Email' },
],
rows: [
{ id: 1, name: 'John Doe', email: 'john@example.com' },
{ id: 2, name: 'Jane Smith', email: 'jane@example.com' },
{ id: 3, name: 'Bob Johnson', email: 'bob@example.com' },
],
primaryKey: 'id',
},
};
export const WithActions: Story = {
name: 'With Edit/Delete Actions',
args: {
columns: [
{ name: 'id', label: 'ID' },
{ name: 'name', label: 'Name' },
{ name: 'status', label: 'Status' },
],
rows: [
{ id: 1, name: 'Active User', status: 'active' },
{ id: 2, name: 'Pending User', status: 'pending' },
],
onEdit: () => console.log('Edit clicked'),
onDelete: () => console.log('Delete clicked'),
primaryKey: 'id',
},
};
export const Empty: Story = {
name: 'Empty State',
args: {
columns: [
{ name: 'id', label: 'ID' },
{ name: 'name', label: 'Name' },
],
rows: [],
},
};

View File

@@ -1,9 +1,6 @@
'use client'; 'use client';
import DeleteIcon from '@mui/icons-material/Delete';
import EditIcon from '@mui/icons-material/Edit';
import { import {
IconButton,
Paper, Paper,
Table, Table,
TableBody, TableBody,
@@ -13,6 +10,7 @@ import {
TableRow, TableRow,
Tooltip, Tooltip,
} from '@mui/material'; } from '@mui/material';
import IconButton from '../atoms/IconButton';
type DataGridProps = { type DataGridProps = {
columns: Array<{ name: string; label?: string }>; columns: Array<{ name: string; label?: string }>;
@@ -54,16 +52,12 @@ export default function DataGrid({ columns, rows, onEdit, onDelete, primaryKey =
<TableCell> <TableCell>
{onEdit && ( {onEdit && (
<Tooltip title="Edit"> <Tooltip title="Edit">
<IconButton size="small" onClick={() => onEdit(row)}> <IconButton size="small" onClick={() => onEdit(row)} icon="Edit" />
<EditIcon fontSize="small" />
</IconButton>
</Tooltip> </Tooltip>
)} )}
{onDelete && ( {onDelete && (
<Tooltip title="Delete"> <Tooltip title="Delete">
<IconButton size="small" color="error" onClick={() => onDelete(row)}> <IconButton size="small" color="error" onClick={() => onDelete(row)} icon="Delete" />
<DeleteIcon fontSize="small" />
</IconButton>
</Tooltip> </Tooltip>
)} )}
</TableCell> </TableCell>

View File

@@ -0,0 +1,47 @@
import type { Meta, StoryObj } from '@storybook/react';
import featuresConfig from '@/config/features.json';
import Button, { type ButtonProps } from './Button';
const meta = {
title: 'Atoms/Button (From JSON)',
component: Button,
parameters: {
layout: 'centered',
docs: {
description: {
component: 'Button component with stories dynamically loaded from features.json',
},
},
},
tags: ['autodocs'],
} satisfies Meta<typeof Button>;
export default meta;
type Story = StoryObj<typeof meta>;
// Dynamically generate stories from features.json
const buttonStories = featuresConfig.storybookStories.Button;
// Primary Button
export const Primary: Story = {
name: buttonStories.primary.name,
args: buttonStories.primary.args as Partial<ButtonProps>,
};
// Secondary Button
export const Secondary: Story = {
name: buttonStories.secondary.name,
args: buttonStories.secondary.args as Partial<ButtonProps>,
};
// Button with Icon
export const WithIcon: Story = {
name: buttonStories.withIcon.name,
args: buttonStories.withIcon.args as Partial<ButtonProps>,
};
// Loading State
export const Loading: Story = {
name: buttonStories.loading.name,
args: buttonStories.loading.args as Partial<ButtonProps>,
};

View File

@@ -0,0 +1,65 @@
import type { Meta, StoryObj } from '@storybook/react';
import Button from './Button';
const meta = {
title: 'Atoms/Button',
component: Button,
parameters: {
layout: 'centered',
},
tags: ['autodocs'],
argTypes: {
variant: {
control: 'select',
options: ['text', 'outlined', 'contained'],
},
color: {
control: 'select',
options: ['default', 'primary', 'secondary', 'error', 'warning', 'info', 'success'],
},
size: {
control: 'select',
options: ['small', 'medium', 'large'],
},
},
} satisfies Meta<typeof Button>;
export default meta;
type Story = StoryObj<typeof meta>;
// Stories based on features.json storybookStories
export const Primary: Story = {
name: 'Primary Button',
args: {
variant: 'contained',
color: 'primary',
text: 'Click Me',
},
};
export const Secondary: Story = {
name: 'Secondary Button',
args: {
variant: 'outlined',
color: 'secondary',
text: 'Cancel',
},
};
export const WithIcon: Story = {
name: 'With Icon',
args: {
variant: 'contained',
startIcon: 'Add',
text: 'Add Item',
},
};
export const Loading: Story = {
name: 'Loading State',
args: {
variant: 'contained',
disabled: true,
text: 'Loading...',
},
};

View File

@@ -0,0 +1,35 @@
'use client';
import { Button as MuiButton, ButtonProps as MuiButtonProps } from '@mui/material';
import * as Icons from '@mui/icons-material';
export type ButtonProps = Omit<MuiButtonProps, 'startIcon' | 'endIcon'> & {
text?: string;
startIcon?: keyof typeof Icons;
endIcon?: keyof typeof Icons;
};
/**
* Atomic Button component
* Wraps Material-UI Button with icon support from features.json
*/
export default function Button({
text,
children,
startIcon,
endIcon,
...props
}: ButtonProps) {
const StartIconComponent = startIcon ? Icons[startIcon] : null;
const EndIconComponent = endIcon ? Icons[endIcon] : null;
return (
<MuiButton
{...props}
startIcon={StartIconComponent ? <StartIconComponent /> : undefined}
endIcon={EndIconComponent ? <EndIconComponent /> : undefined}
>
{text || children}
</MuiButton>
);
}

View File

@@ -0,0 +1,53 @@
import type { Meta, StoryObj } from '@storybook/react';
import Icon from './Icon';
const meta = {
title: 'Atoms/Icon',
component: Icon,
parameters: {
layout: 'centered',
},
tags: ['autodocs'],
argTypes: {
color: {
control: 'select',
options: ['inherit', 'primary', 'secondary', 'action', 'disabled', 'error'],
},
fontSize: {
control: 'select',
options: ['small', 'medium', 'large', 'inherit'],
},
},
} satisfies Meta<typeof Icon>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Storage: Story = {
args: {
name: 'Storage',
color: 'primary',
},
};
export const Code: Story = {
args: {
name: 'Code',
fontSize: 'large',
},
};
export const AccountTree: Story = {
args: {
name: 'AccountTree',
color: 'secondary',
},
};
export const Speed: Story = {
args: {
name: 'Speed',
color: 'error',
fontSize: 'large',
},
};

View File

@@ -0,0 +1,23 @@
'use client';
import { SvgIconProps } from '@mui/material';
import * as Icons from '@mui/icons-material';
export type IconProps = SvgIconProps & {
name: keyof typeof Icons;
};
/**
* Atomic Icon component
* Renders Material-UI icons by name from features.json
*/
export default function Icon({ name, ...props }: IconProps) {
const IconComponent = Icons[name];
if (!IconComponent) {
console.warn(`Icon "${name}" not found in Material Icons`);
return null;
}
return <IconComponent {...props} />;
}

View File

@@ -0,0 +1,52 @@
import type { Meta, StoryObj } from '@storybook/react';
import IconButton from './IconButton';
const meta = {
title: 'Atoms/IconButton',
component: IconButton,
parameters: {
layout: 'centered',
},
tags: ['autodocs'],
argTypes: {
color: {
control: 'select',
options: ['default', 'primary', 'secondary', 'error', 'warning', 'info', 'success'],
},
size: {
control: 'select',
options: ['small', 'medium', 'large'],
},
},
} satisfies Meta<typeof IconButton>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Edit: Story = {
args: {
icon: 'Edit',
color: 'primary',
},
};
export const Delete: Story = {
args: {
icon: 'Delete',
color: 'error',
},
};
export const Add: Story = {
args: {
icon: 'Add',
color: 'primary',
},
};
export const Settings: Story = {
args: {
icon: 'Settings',
size: 'large',
},
};

View File

@@ -0,0 +1,27 @@
'use client';
import { IconButton as MuiIconButton, IconButtonProps as MuiIconButtonProps } from '@mui/material';
import * as Icons from '@mui/icons-material';
export type IconButtonProps = Omit<MuiIconButtonProps, 'children'> & {
icon: keyof typeof Icons;
};
/**
* Atomic IconButton component
* Wraps Material-UI IconButton with icon name from features.json
*/
export default function IconButton({ icon, ...props }: IconButtonProps) {
const IconComponent = Icons[icon];
if (!IconComponent) {
console.warn(`Icon "${icon}" not found in Material Icons`);
return null;
}
return (
<MuiIconButton {...props}>
<IconComponent />
</MuiIconButton>
);
}

View File

@@ -0,0 +1,64 @@
import type { Meta, StoryObj } from '@storybook/react';
import TextField from './TextField';
const meta = {
title: 'Atoms/TextField',
component: TextField,
parameters: {
layout: 'centered',
},
tags: ['autodocs'],
argTypes: {
variant: {
control: 'select',
options: ['standard', 'outlined', 'filled'],
},
type: {
control: 'select',
options: ['text', 'email', 'password', 'number', 'tel', 'url'],
},
size: {
control: 'select',
options: ['small', 'medium'],
},
},
} satisfies Meta<typeof TextField>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {
args: {
label: 'Text Field',
placeholder: 'Enter text...',
variant: 'outlined',
},
};
export const Email: Story = {
args: {
label: 'Email',
type: 'email',
placeholder: 'user@example.com',
variant: 'outlined',
},
};
export const WithError: Story = {
args: {
label: 'Name',
error: true,
helperText: 'This field is required',
variant: 'outlined',
},
};
export const Multiline: Story = {
args: {
label: 'Description',
multiline: true,
rows: 4,
placeholder: 'Enter description...',
variant: 'outlined',
},
};

View File

@@ -0,0 +1,15 @@
'use client';
import { TextField as MuiTextField, TextFieldProps as MuiTextFieldProps } from '@mui/material';
export type TextFieldProps = MuiTextFieldProps & {
// Additional props from features.json
};
/**
* Atomic TextField component
* Wraps Material-UI TextField with features.json configuration
*/
export default function TextField(props: TextFieldProps) {
return <MuiTextField {...props} />;
}

View File

@@ -0,0 +1,53 @@
import type { Meta, StoryObj } from '@storybook/react';
import Typography from './Typography';
const meta = {
title: 'Atoms/Typography',
component: Typography,
parameters: {
layout: 'centered',
},
tags: ['autodocs'],
argTypes: {
variant: {
control: 'select',
options: ['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'subtitle1', 'subtitle2', 'body1', 'body2', 'caption', 'button', 'overline'],
},
align: {
control: 'select',
options: ['left', 'center', 'right', 'justify'],
},
},
} satisfies Meta<typeof Typography>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Heading1: Story = {
args: {
variant: 'h1',
text: 'Heading 1',
},
};
export const Heading4: Story = {
args: {
variant: 'h4',
text: 'Heading 4',
},
};
export const Body: Story = {
args: {
variant: 'body1',
text: 'This is body text with regular weight and size.',
},
};
export const Caption: Story = {
args: {
variant: 'caption',
color: 'text.secondary',
text: 'Caption text - smaller and secondary color',
},
};

View File

@@ -0,0 +1,16 @@
'use client';
import { Typography as MuiTypography, TypographyProps as MuiTypographyProps } from '@mui/material';
export type TypographyProps = Omit<MuiTypographyProps, 'children'> & {
text?: string;
children?: React.ReactNode;
};
/**
* Atomic Typography component
* Wraps Material-UI Typography with text prop from features.json
*/
export default function Typography({ text, children, ...props }: TypographyProps) {
return <MuiTypography {...props}>{text || children}</MuiTypography>;
}

View File

@@ -0,0 +1,47 @@
// Atomic component library - exported from features.json componentProps
export { default as Button } from './Button';
export { default as TextField } from './TextField';
export { default as Typography } from './Typography';
export { default as IconButton } from './IconButton';
export { default as Icon } from './Icon';
// Re-export commonly used Material-UI components for consistency
export {
Box,
Grid,
Paper,
Card,
CardContent,
Dialog,
DialogTitle,
DialogContent,
DialogActions,
Select,
MenuItem,
FormControl,
InputLabel,
Checkbox,
FormControlLabel,
AppBar,
Toolbar,
Drawer,
Alert,
CircularProgress,
Pagination,
Tabs,
Tab,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
List,
ListItem,
ListItemText,
ListItemIcon,
Accordion,
AccordionSummary,
AccordionDetails,
Chip,
} from '@mui/material';

View File

@@ -2944,6 +2944,7 @@
"type": "integer", "type": "integer",
"description": "Query result limit", "description": "Query result limit",
"validation": "^[0-9]+$", "validation": "^[0-9]+$",
"sanitize": "integer",
"min": 1, "min": 1,
"max": 10000, "max": 10000,
"default": 100 "default": 100

View File

@@ -236,7 +236,7 @@ function interpolateValue(value: any, data: Record<string, any>): any {
const [, operation, argsStr] = mathOp; const [, operation, argsStr] = mathOp;
const safeOps = ['abs', 'ceil', 'floor', 'round', 'max', 'min']; const safeOps = ['abs', 'ceil', 'floor', 'round', 'max', 'min'];
if (safeOps.includes(operation)) { if (operation && argsStr && safeOps.includes(operation)) {
try { try {
// Parse arguments safely // Parse arguments safely
const args = argsStr.split(',').map(arg => { const args = argsStr.split(',').map(arg => {
@@ -258,11 +258,13 @@ function interpolateValue(value: any, data: Record<string, any>): any {
const ternaryMatch = expression.match(/^(.+?)\s*\?\s*(.+?)\s*:\s*(.+)$/); const ternaryMatch = expression.match(/^(.+?)\s*\?\s*(.+?)\s*:\s*(.+)$/);
if (ternaryMatch) { if (ternaryMatch) {
const [, condition, trueValue, falseValue] = ternaryMatch; const [, condition, trueValue, falseValue] = ternaryMatch;
const conditionResult = evaluateCondition(condition.trim(), data); if (condition && trueValue !== undefined && falseValue !== undefined) {
const targetValue = conditionResult ? trueValue.trim() : falseValue.trim(); const conditionResult = evaluateCondition(condition.trim(), data);
const targetValue = conditionResult ? trueValue.trim() : falseValue.trim();
// Recursively interpolate the result
return interpolateValue(`{{${targetValue}}}`, data); // Recursively interpolate the result
return interpolateValue(`{{${targetValue}}}`, data);
}
} }
// Simple property access // Simple property access