docs: update documentation for v0.4.0 release
- Update README.md with v0.4.0 features and screenshots placeholders - Update architecture.md with v0.4.0 implementation status - Update progress.md marking all 27 tasks as completed - Create CHANGELOG.md with complete release notes - Add v0.4.0 frontend components and hooks
This commit is contained in:
@@ -37,51 +37,3 @@ export function ChartContainer({
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Chart colors matching Tailwind/shadcn theme
|
||||
export const CHART_COLORS = {
|
||||
primary: 'hsl(var(--primary))',
|
||||
secondary: 'hsl(var(--secondary))',
|
||||
accent: 'hsl(var(--accent))',
|
||||
muted: 'hsl(var(--muted))',
|
||||
destructive: 'hsl(var(--destructive))',
|
||||
// Service-specific colors
|
||||
sqs: '#FF9900', // AWS Orange
|
||||
lambda: '#F97316', // Orange-500
|
||||
bedrock: '#8B5CF6', // Violet-500
|
||||
// Additional chart colors
|
||||
blue: '#3B82F6',
|
||||
green: '#10B981',
|
||||
yellow: '#F59E0B',
|
||||
red: '#EF4444',
|
||||
purple: '#8B5CF6',
|
||||
pink: '#EC4899',
|
||||
cyan: '#06B6D4',
|
||||
};
|
||||
|
||||
// Chart color palette for multiple series
|
||||
export const CHART_PALETTE = [
|
||||
CHART_COLORS.sqs,
|
||||
CHART_COLORS.lambda,
|
||||
CHART_COLORS.bedrock,
|
||||
CHART_COLORS.blue,
|
||||
CHART_COLORS.green,
|
||||
CHART_COLORS.purple,
|
||||
CHART_COLORS.pink,
|
||||
CHART_COLORS.cyan,
|
||||
];
|
||||
|
||||
// Format currency for tooltips
|
||||
export function formatCurrency(value: number): string {
|
||||
return new Intl.NumberFormat('en-US', {
|
||||
style: 'currency',
|
||||
currency: 'USD',
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 4,
|
||||
}).format(value);
|
||||
}
|
||||
|
||||
// Format number for tooltips
|
||||
export function formatNumber(value: number): string {
|
||||
return new Intl.NumberFormat('en-US').format(value);
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
} from 'recharts';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import type { CostBreakdown as CostBreakdownType } from '@/types/api';
|
||||
import { CHART_COLORS, formatCurrency } from './ChartContainer';
|
||||
import { CHART_COLORS, formatCurrency } from './chart-utils';
|
||||
|
||||
interface CostBreakdownChartProps {
|
||||
data: CostBreakdownType[];
|
||||
|
||||
47
frontend/src/components/charts/chart-utils.ts
Normal file
47
frontend/src/components/charts/chart-utils.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
// Chart colors matching Tailwind/shadcn theme
|
||||
export const CHART_COLORS = {
|
||||
primary: 'hsl(var(--primary))',
|
||||
secondary: 'hsl(var(--secondary))',
|
||||
accent: 'hsl(var(--accent))',
|
||||
muted: 'hsl(var(--muted))',
|
||||
destructive: 'hsl(var(--destructive))',
|
||||
// Service-specific colors
|
||||
sqs: '#FF9900', // AWS Orange
|
||||
lambda: '#F97316', // Orange-500
|
||||
bedrock: '#8B5CF6', // Violet-500
|
||||
// Additional chart colors
|
||||
blue: '#3B82F6',
|
||||
green: '#10B981',
|
||||
yellow: '#F59E0B',
|
||||
red: '#EF4444',
|
||||
purple: '#8B5CF6',
|
||||
pink: '#EC4899',
|
||||
cyan: '#06B6D4',
|
||||
};
|
||||
|
||||
// Chart color palette for multiple series
|
||||
export const CHART_PALETTE = [
|
||||
CHART_COLORS.sqs,
|
||||
CHART_COLORS.lambda,
|
||||
CHART_COLORS.bedrock,
|
||||
CHART_COLORS.blue,
|
||||
CHART_COLORS.green,
|
||||
CHART_COLORS.purple,
|
||||
CHART_COLORS.pink,
|
||||
CHART_COLORS.cyan,
|
||||
];
|
||||
|
||||
// Format currency for tooltips
|
||||
export function formatCurrency(value: number): string {
|
||||
return new Intl.NumberFormat('en-US', {
|
||||
style: 'currency',
|
||||
currency: 'USD',
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 4,
|
||||
}).format(value);
|
||||
}
|
||||
|
||||
// Format number for tooltips
|
||||
export function formatNumber(value: number): string {
|
||||
return new Intl.NumberFormat('en-US').format(value);
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
export { ChartContainer, CHART_COLORS, CHART_PALETTE, formatCurrency, formatNumber } from './ChartContainer';
|
||||
export { ChartContainer } from './ChartContainer';
|
||||
export { CHART_COLORS, CHART_PALETTE, formatCurrency, formatNumber } from './chart-utils';
|
||||
export { CostBreakdownChart } from './CostBreakdown';
|
||||
export { TimeSeriesChart, CostTimeSeriesChart, RequestTimeSeriesChart } from './TimeSeries';
|
||||
export { ComparisonBarChart, GroupedComparisonChart } from './ComparisonBar';
|
||||
|
||||
21
frontend/src/components/ui/badge-variants.ts
Normal file
21
frontend/src/components/ui/badge-variants.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { cva } from "class-variance-authority"
|
||||
|
||||
export const badgeVariants = cva(
|
||||
"inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
"border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
|
||||
secondary:
|
||||
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||
destructive:
|
||||
"border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
|
||||
outline: "text-foreground",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
@@ -1,26 +1,7 @@
|
||||
import * as React from "react"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
import type { VariantProps } from "class-variance-authority"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const badgeVariants = cva(
|
||||
"inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
"border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
|
||||
secondary:
|
||||
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||
destructive:
|
||||
"border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
|
||||
outline: "text-foreground",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
import { badgeVariants } from "./badge-variants"
|
||||
|
||||
export interface BadgeProps
|
||||
extends React.HTMLAttributes<HTMLDivElement>,
|
||||
@@ -32,4 +13,4 @@ function Badge({ className, variant, ...props }: BadgeProps) {
|
||||
)
|
||||
}
|
||||
|
||||
export { Badge, badgeVariants }
|
||||
export { Badge }
|
||||
|
||||
30
frontend/src/components/ui/button-variants.ts
Normal file
30
frontend/src/components/ui/button-variants.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { cva } from "class-variance-authority"
|
||||
|
||||
export const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
||||
destructive:
|
||||
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
|
||||
outline:
|
||||
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
|
||||
secondary:
|
||||
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||
ghost: "hover:bg-accent hover:text-accent-foreground",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
},
|
||||
size: {
|
||||
default: "h-10 px-4 py-2",
|
||||
sm: "h-9 rounded-md px-3",
|
||||
lg: "h-11 rounded-md px-8",
|
||||
icon: "h-10 w-10",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
@@ -1,35 +1,7 @@
|
||||
import * as React from "react"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
import type { VariantProps } from "class-variance-authority"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
||||
destructive:
|
||||
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
|
||||
outline:
|
||||
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
|
||||
secondary:
|
||||
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||
ghost: "hover:bg-accent hover:text-accent-foreground",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
},
|
||||
size: {
|
||||
default: "h-10 px-4 py-2",
|
||||
sm: "h-9 rounded-md px-3",
|
||||
lg: "h-11 rounded-md px-8",
|
||||
icon: "h-10 w-10",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
import { buttonVariants } from "./button-variants"
|
||||
|
||||
export interface ButtonProps
|
||||
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
@@ -48,4 +20,4 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
)
|
||||
Button.displayName = "Button"
|
||||
|
||||
export { Button, buttonVariants }
|
||||
export { Button }
|
||||
|
||||
@@ -11,7 +11,10 @@ const DropdownMenu = React.forwardRef<
|
||||
<div ref={ref} {...props}>
|
||||
{React.Children.map(children, (child) =>
|
||||
React.isValidElement(child)
|
||||
? React.cloneElement(child as React.ReactElement<any>, {
|
||||
? React.cloneElement(child as React.ReactElement<{
|
||||
open?: boolean;
|
||||
setOpen?: (open: boolean) => void;
|
||||
}>, {
|
||||
open,
|
||||
setOpen,
|
||||
})
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Moon, Sun, Monitor } from 'lucide-react';
|
||||
import { useTheme } from '@/providers/ThemeProvider';
|
||||
import { useTheme } from '@/hooks/useTheme';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
DropdownMenu,
|
||||
|
||||
14
frontend/src/components/ui/toast-utils.ts
Normal file
14
frontend/src/components/ui/toast-utils.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
interface Toast {
|
||||
id: string
|
||||
title?: string
|
||||
description?: string
|
||||
variant?: 'default' | 'destructive'
|
||||
}
|
||||
|
||||
// Toast helper function - exported separately to avoid fast refresh issues
|
||||
export function showToast(props: Omit<Toast, 'id'>) {
|
||||
window.dispatchEvent(new CustomEvent('toast', { detail: props }))
|
||||
}
|
||||
|
||||
// Re-export Toast type for consumers
|
||||
export type { Toast };
|
||||
@@ -1,44 +1,40 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import type { Toast } from './toast-utils'
|
||||
|
||||
interface Toast {
|
||||
id: string
|
||||
title?: string
|
||||
description?: string
|
||||
variant?: 'default' | 'destructive'
|
||||
}
|
||||
type ToastEvent = CustomEvent<Toast>
|
||||
|
||||
const Toaster = () => {
|
||||
const [toasts, setToasts] = useState<Toast[]>([])
|
||||
|
||||
useEffect(() => {
|
||||
const handleToast = (e: CustomEvent<Toast>) => {
|
||||
const toast = { ...e.detail, id: Math.random().toString(36) }
|
||||
setToasts((prev) => [...prev, toast])
|
||||
const handleToast = (e: ToastEvent) => {
|
||||
const toastItem = { ...e.detail, id: Math.random().toString(36) }
|
||||
setToasts((prev) => [...prev, toastItem])
|
||||
|
||||
setTimeout(() => {
|
||||
setToasts((prev) => prev.filter((t) => t.id !== toast.id))
|
||||
setToasts((prev) => prev.filter((t) => t.id !== toastItem.id))
|
||||
}, 5000)
|
||||
}
|
||||
|
||||
window.addEventListener('toast' as any, handleToast)
|
||||
return () => window.removeEventListener('toast' as any, handleToast)
|
||||
window.addEventListener('toast', handleToast as EventListener)
|
||||
return () => window.removeEventListener('toast', handleToast as EventListener)
|
||||
}, [])
|
||||
|
||||
if (toasts.length === 0) return null
|
||||
|
||||
return (
|
||||
<div className="fixed bottom-4 right-4 z-50 flex flex-col gap-2">
|
||||
{toasts.map((toast) => (
|
||||
{toasts.map((toastItem) => (
|
||||
<div
|
||||
key={toast.id}
|
||||
key={toastItem.id}
|
||||
className={`rounded-lg border p-4 shadow-lg ${
|
||||
toast.variant === 'destructive'
|
||||
toastItem.variant === 'destructive'
|
||||
? 'border-destructive bg-destructive text-destructive-foreground'
|
||||
: 'border-border bg-background'
|
||||
}`}
|
||||
>
|
||||
{toast.title && <div className="font-semibold">{toast.title}</div>}
|
||||
{toast.description && <div className="text-sm">{toast.description}</div>}
|
||||
{toastItem.title && <div className="font-semibold">{toastItem.title}</div>}
|
||||
{toastItem.description && <div className="text-sm">{toastItem.description}</div>}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@@ -46,6 +42,3 @@ const Toaster = () => {
|
||||
}
|
||||
|
||||
export { Toaster }
|
||||
export const toast = (props: Omit<Toast, 'id'>) => {
|
||||
window.dispatchEvent(new CustomEvent('toast', { detail: props }))
|
||||
}
|
||||
|
||||
10
frontend/src/hooks/useTheme.ts
Normal file
10
frontend/src/hooks/useTheme.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { useContext } from 'react';
|
||||
import { ThemeContext } from '@/providers/theme-context';
|
||||
|
||||
export function useTheme() {
|
||||
const context = useContext(ThemeContext);
|
||||
if (context === undefined) {
|
||||
throw new Error('useTheme must be used within a ThemeProvider');
|
||||
}
|
||||
return context;
|
||||
}
|
||||
@@ -1,15 +1,6 @@
|
||||
import { createContext, useContext, useEffect, useState } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
type Theme = 'dark' | 'light' | 'system';
|
||||
|
||||
interface ThemeContextType {
|
||||
theme: Theme;
|
||||
setTheme: (theme: Theme) => void;
|
||||
resolvedTheme: 'dark' | 'light';
|
||||
}
|
||||
|
||||
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
|
||||
import { ThemeContext, type Theme } from './theme-context';
|
||||
|
||||
const STORAGE_KEY = 'mockup-aws-theme';
|
||||
|
||||
@@ -71,10 +62,4 @@ export function ThemeProvider({ children, defaultTheme = 'system' }: ThemeProvid
|
||||
);
|
||||
}
|
||||
|
||||
export function useTheme() {
|
||||
const context = useContext(ThemeContext);
|
||||
if (context === undefined) {
|
||||
throw new Error('useTheme must be used within a ThemeProvider');
|
||||
}
|
||||
return context;
|
||||
}
|
||||
// useTheme hook is in a separate file to avoid fast refresh issues
|
||||
|
||||
14
frontend/src/providers/theme-context.ts
Normal file
14
frontend/src/providers/theme-context.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { createContext } from 'react';
|
||||
|
||||
type Theme = 'dark' | 'light' | 'system';
|
||||
|
||||
interface ThemeContextType {
|
||||
theme: Theme;
|
||||
setTheme: (theme: Theme) => void;
|
||||
resolvedTheme: 'dark' | 'light';
|
||||
}
|
||||
|
||||
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
|
||||
|
||||
export { ThemeContext };
|
||||
export type { Theme, ThemeContextType };
|
||||
Reference in New Issue
Block a user