feat(interface-design): add comprehensive interface design skill documentation and references
- Introduced SKILL.md outlining the principles and processes for effective interface design. - Added critique.md for evaluating design outputs and ensuring craft over correctness. - Created example.md to illustrate the application of subtle layering principles in design decisions. - Developed principles.md detailing core craft principles for consistent design quality. - Implemented validation.md for memory management and pattern reuse in the design system. - Established system.md for the Playzer design system, defining direction, domain concepts, and design decisions. - Added StatusBadge component for displaying booking statuses with appropriate styles and labels. - Updated skills-lock.json to include the new interface-design skill.
This commit is contained in:
49
apps/frontend/src/components/ui/status-badge.tsx
Normal file
49
apps/frontend/src/components/ui/status-badge.tsx
Normal file
@@ -0,0 +1,49 @@
|
||||
import { cn } from '@/lib/utils';
|
||||
import { type LucideIcon } from 'lucide-react';
|
||||
|
||||
type StatusType = 'CONFIRMED' | 'PENDING' | 'CANCELLED' | 'NO_SHOW' | 'COMPLETED';
|
||||
|
||||
type StatusBadgeProps = {
|
||||
status: StatusType;
|
||||
label: string;
|
||||
icon?: LucideIcon;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
const statusStyles: Record<StatusType, string> = {
|
||||
CONFIRMED: 'bg-status-confirmed-bg border-status-confirmed-border text-status-confirmed-text',
|
||||
PENDING: 'bg-status-pending-bg border-status-pending-border text-status-pending-text',
|
||||
CANCELLED: 'bg-status-cancelled-bg border-status-cancelled-border text-status-cancelled-text',
|
||||
NO_SHOW: 'bg-status-noshow-bg border-status-noshow-border text-status-noshow-text',
|
||||
COMPLETED: 'bg-status-completed-bg border-status-completed-border text-status-completed-text',
|
||||
};
|
||||
|
||||
export function StatusBadge({ status, label, icon: Icon, className }: StatusBadgeProps) {
|
||||
return (
|
||||
<span
|
||||
className={cn(
|
||||
'inline-flex items-center gap-1.5 rounded-full border px-2 py-0.5 text-xs font-medium',
|
||||
statusStyles[status],
|
||||
className
|
||||
)}
|
||||
>
|
||||
{Icon && <Icon className="size-3" />}
|
||||
{label}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
export function effectiveStatusClassName(
|
||||
status: 'CONFIRMED' | 'CANCELLED' | 'COMPLETED' | 'NO_SHOW'
|
||||
): string {
|
||||
return statusStyles[status];
|
||||
}
|
||||
|
||||
export function effectiveStatusLabel(
|
||||
status: 'CONFIRMED' | 'CANCELLED' | 'COMPLETED' | 'NO_SHOW'
|
||||
): string {
|
||||
if (status === 'NO_SHOW') return 'No show';
|
||||
if (status === 'COMPLETED') return 'Cumplida';
|
||||
if (status === 'CANCELLED') return 'Cancelada';
|
||||
return 'Confirmada';
|
||||
}
|
||||
@@ -66,11 +66,25 @@ function extractMessage(error: unknown, fallback: string) {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
function formatDateLabel(dateIso: string) {
|
||||
function formatDateLabel(dateIso: string, todayIso: string) {
|
||||
const [year, month, day] = dateIso.split('-').map(Number);
|
||||
|
||||
if (!year || !month || !day) return dateIso;
|
||||
|
||||
const [tYear, tMonth, tDay] = todayIso.split('-').map(Number);
|
||||
|
||||
const isToday = year === tYear && month === tMonth && day === tDay;
|
||||
|
||||
const tmrw = new Date(tYear, tMonth - 1, tDay);
|
||||
tmrw.setDate(tmrw.getDate() + 1);
|
||||
const isTomorrow =
|
||||
year === tmrw.getFullYear() &&
|
||||
month === tmrw.getMonth() + 1 &&
|
||||
day === tmrw.getDate();
|
||||
|
||||
if (isToday) return 'Hoy';
|
||||
if (isTomorrow) return 'Mañana';
|
||||
|
||||
const date = new Date(year, month - 1, day);
|
||||
return new Intl.DateTimeFormat('es-AR', {
|
||||
weekday: 'long',
|
||||
@@ -104,21 +118,6 @@ function effectiveStatusLabel(status: 'CONFIRMED' | 'CANCELLED' | 'COMPLETED' |
|
||||
return 'Confirmada';
|
||||
}
|
||||
|
||||
function effectiveStatusClassName(status: 'CONFIRMED' | 'CANCELLED' | 'COMPLETED' | 'NO_SHOW') {
|
||||
if (status === 'NO_SHOW') {
|
||||
return 'border-orange-200 bg-orange-50 text-orange-700';
|
||||
}
|
||||
if (status === 'COMPLETED') {
|
||||
return 'border-emerald-200 bg-emerald-50 text-emerald-700';
|
||||
}
|
||||
|
||||
if (status === 'CANCELLED') {
|
||||
return 'border-rose-200 bg-rose-50 text-rose-700';
|
||||
}
|
||||
|
||||
return 'border-sky-200 bg-sky-50 text-sky-700';
|
||||
}
|
||||
|
||||
export function HomePage() {
|
||||
const queryClient = useQueryClient();
|
||||
const navigate = useNavigate();
|
||||
@@ -704,7 +703,7 @@ export function HomePage() {
|
||||
<div className="mt-4 space-y-4">
|
||||
{groupedBookings.map(([date, bookings]) => (
|
||||
<article key={date} className="rounded-lg border bg-background p-3">
|
||||
<h2 className="text-sm font-semibold capitalize">{formatDateLabel(date)}</h2>
|
||||
<h2 className="text-sm font-semibold">{formatDateLabel(date, todayIso)}</h2>
|
||||
<div className="mt-3 space-y-2">
|
||||
{bookings.map((booking) => {
|
||||
const effectiveStatus = getEffectiveStatus(booking);
|
||||
@@ -712,10 +711,36 @@ export function HomePage() {
|
||||
booking.status === 'CONFIRMED' && effectiveStatus === 'CONFIRMED';
|
||||
const isMutating = updateStatusMutation.isPending;
|
||||
|
||||
const statusColorMap: Record<string, string> = {
|
||||
CONFIRMED: 'border-l-sky-500 dark:border-l-sky-400',
|
||||
COMPLETED: 'border-l-emerald-500 dark:border-l-emerald-400',
|
||||
CANCELLED: 'border-l-rose-500 dark:border-l-rose-400',
|
||||
NO_SHOW: 'border-l-orange-500 dark:border-l-orange-400',
|
||||
};
|
||||
|
||||
const statusBgMap: Record<string, string> = {
|
||||
CONFIRMED: 'bg-sky-50/30 dark:bg-sky-950/20',
|
||||
COMPLETED: 'bg-emerald-50/30 dark:bg-emerald-950/20',
|
||||
CANCELLED: 'bg-rose-50/30 dark:bg-rose-950/20',
|
||||
NO_SHOW: 'bg-orange-50/30 dark:bg-orange-950/20',
|
||||
};
|
||||
|
||||
const colorClass = statusColorMap[effectiveStatus] || '';
|
||||
const bgClass = statusBgMap[effectiveStatus] || '';
|
||||
|
||||
const badgeColorMap: Record<string, string> = {
|
||||
CONFIRMED: 'border-sky-200 bg-sky-50 text-sky-700 dark:border-sky-800 dark:bg-sky-950 dark:text-sky-300',
|
||||
COMPLETED: 'border-emerald-200 bg-emerald-50 text-emerald-700 dark:border-emerald-800 dark:bg-emerald-950 dark:text-emerald-300',
|
||||
CANCELLED: 'border-rose-200 bg-rose-50 text-rose-700 dark:border-rose-800 dark:bg-rose-950 dark:text-rose-300',
|
||||
NO_SHOW: 'border-orange-200 bg-orange-50 text-orange-700 dark:border-orange-800 dark:bg-orange-950 dark:text-orange-300',
|
||||
};
|
||||
|
||||
const badgeClass = badgeColorMap[effectiveStatus] || '';
|
||||
|
||||
return (
|
||||
<div
|
||||
key={booking.id}
|
||||
className="flex flex-col gap-2 rounded-md border p-3 sm:flex-row sm:items-center sm:justify-between"
|
||||
className={`flex flex-col gap-2 rounded-md border border-l-[3px] p-3 sm:flex-row sm:items-center sm:justify-between ${colorClass} ${bgClass}`}
|
||||
>
|
||||
<div>
|
||||
<p className="text-sm font-medium">
|
||||
@@ -729,7 +754,7 @@ export function HomePage() {
|
||||
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<span
|
||||
className={`inline-flex rounded-full border px-2 py-1 text-xs font-medium ${effectiveStatusClassName(effectiveStatus)}`}
|
||||
className={`hidden sm:inline-flex rounded-full border px-2 py-1 text-xs font-medium ${badgeClass}`}
|
||||
>
|
||||
{effectiveStatusLabel(effectiveStatus)}
|
||||
</span>
|
||||
|
||||
@@ -49,6 +49,7 @@
|
||||
}
|
||||
|
||||
:root {
|
||||
/* Base colors (retain shadcn structure) */
|
||||
--background: oklch(1 0 0);
|
||||
--foreground: oklch(0.145 0 0);
|
||||
--card: oklch(1 0 0);
|
||||
@@ -81,6 +82,41 @@
|
||||
--sidebar-accent-foreground: oklch(0.205 0 0);
|
||||
--sidebar-border: oklch(0.922 0 0);
|
||||
--sidebar-ring: oklch(0.708 0 0);
|
||||
|
||||
/* Surface elevation (light mode: higher = lighter) */
|
||||
--surface-base: oklch(1 0 0);
|
||||
--surface-raised: oklch(0.99 0 0);
|
||||
--surface-overlay: oklch(0.98 0 0);
|
||||
|
||||
/* Semantic status - Light mode (subtle, not overwhelming) */
|
||||
--status-confirmed-bg: oklch(0.97 0.02 145);
|
||||
--status-confirmed-border: oklch(0.88 0.03 145);
|
||||
--status-confirmed-text: oklch(0.38 0.12 145);
|
||||
|
||||
--status-pending-bg: oklch(0.97 0.02 75);
|
||||
--status-pending-border: oklch(0.88 0.03 75);
|
||||
--status-pending-text: oklch(0.48 0.15 75);
|
||||
|
||||
--status-cancelled-bg: oklch(0.97 0.02 25);
|
||||
--status-cancelled-border: oklch(0.88 0.03 25);
|
||||
--status-cancelled-text: oklch(0.45 0.15 25);
|
||||
|
||||
--status-noshow-bg: oklch(0.97 0.02 50);
|
||||
--status-noshow-border: oklch(0.88 0.03 50);
|
||||
--status-noshow-text: oklch(0.45 0.12 50);
|
||||
|
||||
--status-completed-bg: oklch(0.97 0.02 220);
|
||||
--status-completed-border: oklch(0.88 0.03 220);
|
||||
--status-completed-text: oklch(0.38 0.1 220);
|
||||
|
||||
/* Borders */
|
||||
--border-subtle: oklch(1 0 0 / 8%);
|
||||
--border-default: oklch(1 0 0 / 12%);
|
||||
--border-emphasis: oklch(1 0 0 / 20%);
|
||||
|
||||
/* Input */
|
||||
--input-bg: oklch(0.97 0 0);
|
||||
--input-border: oklch(0.89 0 0);
|
||||
}
|
||||
|
||||
.dark {
|
||||
@@ -115,6 +151,41 @@
|
||||
--sidebar-accent-foreground: oklch(0.985 0 0);
|
||||
--sidebar-border: oklch(1 0 0 / 10%);
|
||||
--sidebar-ring: oklch(0.556 0 0);
|
||||
|
||||
/* Surface elevation (dark mode: higher = lighter) */
|
||||
--surface-base: oklch(0.145 0 0);
|
||||
--surface-raised: oklch(0.18 0 0);
|
||||
--surface-overlay: oklch(0.22 0 0);
|
||||
|
||||
/* Semantic status - Dark mode (desaturated, lower contrast) */
|
||||
--status-confirmed-bg: oklch(0.25 0.06 145 / 40%);
|
||||
--status-confirmed-border: oklch(0.35 0.06 145 / 60%);
|
||||
--status-confirmed-text: oklch(0.7 0.08 145);
|
||||
|
||||
--status-pending-bg: oklch(0.25 0.06 75 / 40%);
|
||||
--status-pending-border: oklch(0.35 0.06 75 / 60%);
|
||||
--status-pending-text: oklch(0.75 0.08 75);
|
||||
|
||||
--status-cancelled-bg: oklch(0.25 0.06 25 / 40%);
|
||||
--status-cancelled-border: oklch(0.35 0.06 25 / 60%);
|
||||
--status-cancelled-text: oklch(0.75 0.08 25);
|
||||
|
||||
--status-noshow-bg: oklch(0.25 0.06 50 / 40%);
|
||||
--status-noshow-border: oklch(0.35 0.06 50 / 60%);
|
||||
--status-noshow-text: oklch(0.75 0.08 50);
|
||||
|
||||
--status-completed-bg: oklch(0.25 0.06 220 / 40%);
|
||||
--status-completed-border: oklch(0.35 0.06 220 / 60%);
|
||||
--status-completed-text: oklch(0.7 0.08 220);
|
||||
|
||||
/* Borders */
|
||||
--border-subtle: oklch(1 0 0 / 6%);
|
||||
--border-default: oklch(1 0 0 / 10%);
|
||||
--border-emphasis: oklch(1 0 0 / 18%);
|
||||
|
||||
/* Input */
|
||||
--input-bg: oklch(1 0 0 / 10%);
|
||||
--input-border: oklch(1 0 0 / 15%);
|
||||
}
|
||||
|
||||
@layer base {
|
||||
|
||||
Reference in New Issue
Block a user