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:
Jose Selesan
2026-04-24 11:23:45 -03:00
parent 7bfa3b390e
commit 503c29613b
13 changed files with 1200 additions and 32 deletions

View 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';
}

View File

@@ -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>

View File

@@ -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 {