UI
ui-step-wizard
>-
Install
mkdir -p .claude/skills/ui-step-wizard && curl -L -o skill.zip "https://agentskills.codes/api/skills/download/14938" && unzip -o skill.zip -d .claude/skills/ui-step-wizard && rm skill.zipInstalls to .claude/skills/ui-step-wizard
Activation
This is the description your AI agent reads to decide when to run this skill — the better it matches your request, the more reliably it fires.
StepWizard pentru formulare multi-step ERP: validare per step cu Zod, progress indicator, back/next navigation, submit la final. PrintLayout pentru documente printabile cu @media print. AuditTrail — timeline din audit.audit_log.228 charsno explicit “when” trigger
About this skill
StepWizard, PrintLayout, AuditTrail
StepWizard
// components/common/StepWizard/StepWizard.tsx
import { useState } from 'react';
import { CheckIcon } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { cn } from '@/lib/utils';
import type { ZodSchema } from 'zod';
import type { UseFormReturn } from 'react-hook-form';
export interface WizardStep {
id: string;
title: string;
description?: string;
schema?: ZodSchema; // validare Zod pentru acest step
component: React.ReactNode;
}
interface StepWizardProps {
steps: WizardStep[];
form: UseFormReturn<any>;
onSubmit: (data: any) => void;
isSubmitting?: boolean;
}
export function StepWizard({
steps,
form,
onSubmit,
isSubmitting,
}: StepWizardProps) {
const [current, setCurrent] = useState(0);
const isFirst = current === 0;
const isLast = current === steps.length - 1;
const goNext = async () => {
const step = steps[current];
if (step.schema) {
// Validează doar câmpurile din step-ul curent
const values = form.getValues();
const result = step.schema.safeParse(values);
if (!result.success) {
// Triggerează erorile RHF pentru câmpurile invalide
result.error.issues.forEach((issue) => {
const path = issue.path.join('.');
form.setError(path as any, { message: issue.message });
});
return;
}
}
setCurrent((c) => c + 1);
};
const goBack = () => setCurrent((c) => c - 1);
return (
<div className="space-y-6">
{/* Progress indicator */}
<div className="flex items-center">
{steps.map((step, index) => (
<div key={step.id} className="flex items-center flex-1 last:flex-none">
{/* Circle */}
<div className={cn(
'h-8 w-8 rounded-full flex items-center justify-center',
'text-xs font-semibold shrink-0 border-2 transition-colors',
index < current
? 'bg-primary-500 border-primary-500 text-white'
: index === current
? 'border-primary-500 text-primary-600'
: 'border-border-strong text-text-muted'
)}>
{index < current
? <CheckIcon className="h-4 w-4" />
: index + 1
}
</div>
{/* Label */}
<div className="ml-2 hidden sm:block">
<p className={cn(
'text-xs font-medium',
index === current ? 'text-primary-600' : 'text-text-muted'
)}>
{step.title}
</p>
</div>
{/* Connector */}
{index < steps.length - 1 && (
<div className={cn(
'flex-1 h-0.5 mx-3',
index < current ? 'bg-primary-500' : 'bg-border-default'
)} />
)}
</div>
))}
</div>
{/* Step content */}
<div className="min-h-[300px]">
<div className="mb-4">
<h3 className="text-base font-semibold">{steps[current].title}</h3>
{steps[current].description && (
<p className="text-sm text-text-muted mt-1">
{steps[current].description}
</p>
)}
</div>
{steps[current].component}
</div>
{/* Navigation */}
<div className="flex justify-between pt-4 border-t border-border-default">
<Button
type="button"
variant="outline"
onClick={goBack}
disabled={isFirst}
>
Înapoi
</Button>
{isLast ? (
<Button
type="button"
onClick={form.handleSubmit(onSubmit)}
disabled={isSubmitting}
>
{isSubmitting ? 'Se salvează...' : 'Finalizează'}
</Button>
) : (
<Button type="button" onClick={goNext}>
Continuă
</Button>
)}
</div>
</div>
);
}
PrintLayout — Document Printabil
// components/common/PrintLayout/PrintLayout.tsx
// CSS @media print — ascunde UI, afișează doar documentul
interface PrintLayoutProps {
children: React.ReactNode;
title?: string; // titlu document în header print
}
export function PrintLayout({ children, title }: PrintLayoutProps) {
return (
<>
{/* Buton print — ascuns la printare */}
<div className="print:hidden mb-4 flex gap-2">
<Button variant="outline" onClick={() => window.print()}>
Printează
</Button>
</div>
{/* Conținut document */}
<div className="print:block">
{children}
</div>
{/* Stiluri print */}
<style>{`
@media print {
/* Ascunde UI */
nav, aside, header, .print\\:hidden { display: none !important; }
/* Dimensiuni pagină A4 */
@page {
size: A4 portrait;
margin: 15mm 20mm;
}
body {
font-size: 11pt;
color: #000;
background: #fff;
}
/* Evită ruperea tabelelor între pagini */
table { page-break-inside: avoid; }
tr { page-break-inside: avoid; }
/* Header pe fiecare pagină */
thead { display: table-header-group; }
tfoot { display: table-footer-group; }
}
`}</style>
</>
);
}
// Template factură printabilă
function InvoicePrint({ invoice }: { invoice: InvoiceDetailDto }) {
return (
<PrintLayout title={`Factură ${invoice.invoiceNumber}`}>
<div className="p-8 max-w-3xl mx-auto">
{/* Header */}
<div className="flex justify-between mb-8">
<div>
<h1 className="text-2xl font-bold">FACTURĂ FISCALĂ</h1>
<p className="text-lg font-semibold">{invoice.invoiceNumber}</p>
</div>
<div className="text-right text-sm">
<p>Data: {new Date(invoice.createdAt).toLocaleDateString('ro-RO')}</p>
<p>Scadent: {new Date(invoice.dueDate).toLocaleDateString('ro-RO')}</p>
</div>
</div>
{/* Furnizor / Client */}
<div className="grid grid-cols-2 gap-8 mb-8 text-sm">
<div>
<h3 className="font-semibold mb-1">Furnizor</h3>
{/* date furnizor */}
</div>
<div>
<h3 className="font-semibold mb-1">Client</h3>
<p>{invoice.customerName}</p>
</div>
</div>
{/* Linii */}
<table className="w-full text-sm border-collapse mb-6">
<thead>
<tr className="border-b-2 border-black">
<th className="text-left py-2">Descriere</th>
<th className="text-right py-2">Cant.</th>
<th className="text-right py-2">Preț unit.</th>
<th className="text-right py-2">TVA</th>
<th className="text-right py-2">Total</th>
</tr>
</thead>
<tbody>
{invoice.lines?.map((line, i) => (
<tr key={i} className="border-b border-gray-200">
<td className="py-1">{line.description}</td>
<td className="text-right py-1">{line.quantity}</td>
<td className="text-right py-1 font-mono">
{line.unitPrice.toLocaleString('ro-RO', { minimumFractionDigits: 2 })}
</td>
<td className="text-right py-1">{line.vatRate * 100}%</td>
<td className="text-right py-1 font-mono">
{line.lineTotal.toLocaleString('ro-RO', { minimumFractionDigits: 2 })}
</td>
</tr>
))}
</tbody>
<tfoot>
<tr className="border-t-2 border-black font-semibold">
<td colSpan={4} className="text-right py-2">TOTAL:</td>
<td className="text-right py-2 font-mono">
{invoice.totalAmount.toLocaleString('ro-RO', {
minimumFractionDigits: 2
})} RON
</td>
</tr>
</tfoot>
</table>
</div>
</PrintLayout>
);
}
AuditTrail — Timeline Modificări
// components/common/AuditTrail/AuditTrail.tsx
import { useQuery } from '@tanstack/react-query';
import { api } from '@/lib/axios';
import { format } from 'date-fns';
import { ro } from 'date-fns/locale';
interface AuditEntry {
id: number;
action: string;
userName: string;
oldValues?: Record<string, unknown>;
newValues?: Record<string, unknown>;
createdAt: string;
}
interface AuditTrailProps {
entityType: string;
entityId: string;
}
export function AuditTrail({ entityType, entityId }: AuditTrailProps) {
const { data = [], isLoading } = useQuery({
queryKey: ['audit', entityType, entityId],
queryFn: () => api.get<AuditEntry[]>('/administration/audit', {
params: { entityType, entityId }
}),
staleTime: 60_000,
});
if (isLoading) {
return <div className="space-y-3">
{[1,2,3].map(i => (
<div key={i} className="h-16 bg-surface-muted rounded animate-pulse" />
))}
</div>;
}
if (data.length === 0) {
return (
<p className="text-sm text-text-muted text-center py-8">
Nu există istoric modificări.
</p>
);
}
return (
<div className="space-y-0">
{data.map((entry, index) => (
<div key={entry.id} className="flex gap-3">
{/* Timeline line */}
<div className="flex flex-col items-center">
<div className="h-2.5 w-2.5 rounded-full bg-primary-400 mt-1.5 shrink-0" />
{index < data.length - 1 && (
<div className="flex-1 w-px bg-border-default mt-1" />
)}
</div>
{/* Content */}
<div className="pb-4 min-w-0 fl
---
*Content truncated.*