feat(ui): visual polish primitives + token additions (Phase A)
Adds the design tokens the polish PRs (10a-e) will draw from:
shadow-xs/sm/md/lg/glow, radius scale tuned to spec, gradient utilities,
spring/smooth eases, and fast/base/slow durations. Introduces
StatusPill, KPITile, and EmptyState primitives plus a polished
PageHeader variant ('gradient') with optional eyebrow + KPI sub-line —
existing PageHeader callers stay on the plain variant.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -6,22 +6,54 @@ interface PageHeaderProps {
|
|||||||
description?: string;
|
description?: string;
|
||||||
actions?: ReactNode;
|
actions?: ReactNode;
|
||||||
className?: string;
|
className?: string;
|
||||||
|
/** Optional small uppercase label above the title. */
|
||||||
|
eyebrow?: string;
|
||||||
|
/** Optional one-line stats / KPI summary under the description. */
|
||||||
|
kpiLine?: ReactNode;
|
||||||
|
/** Render with the polished gradient-brand-soft background strip. */
|
||||||
|
variant?: 'plain' | 'gradient';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Consistent page-level header: title, optional description, and an action
|
* Consistent page-level header: title, optional description, KPI sub-line,
|
||||||
* slot (typically buttons — e.g. "New Client", "Export").
|
* eyebrow, and an action slot. Use `variant="gradient"` for hero strips on
|
||||||
|
* landing pages and detail headers; the plain variant remains the default so
|
||||||
|
* existing call-sites stay unchanged.
|
||||||
*/
|
*/
|
||||||
export function PageHeader({ title, description, actions, className }: PageHeaderProps) {
|
export function PageHeader({
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
actions,
|
||||||
|
className,
|
||||||
|
eyebrow,
|
||||||
|
kpiLine,
|
||||||
|
variant = 'plain',
|
||||||
|
}: PageHeaderProps) {
|
||||||
|
const isGradient = variant === 'gradient';
|
||||||
return (
|
return (
|
||||||
<div className={cn('flex items-start justify-between gap-4 mb-6', className)}>
|
<div
|
||||||
<div className="min-w-0">
|
className={cn(
|
||||||
<h1 className="text-2xl font-bold text-foreground tracking-tight truncate">{title}</h1>
|
'mb-6 flex items-start justify-between gap-4',
|
||||||
{description && (
|
isGradient &&
|
||||||
<p className="mt-1 text-sm text-muted-foreground">{description}</p>
|
'rounded-xl border border-slate-200 bg-gradient-brand-soft px-5 py-4 shadow-xs',
|
||||||
|
className,
|
||||||
)}
|
)}
|
||||||
|
>
|
||||||
|
<div className="min-w-0">
|
||||||
|
{eyebrow ? (
|
||||||
|
<div className="mb-1 text-xs font-semibold uppercase tracking-wide text-brand">
|
||||||
|
{eyebrow}
|
||||||
</div>
|
</div>
|
||||||
{actions && <div className="flex items-center gap-2 shrink-0">{actions}</div>}
|
) : null}
|
||||||
|
<h1 className="truncate text-2xl font-bold tracking-tight text-foreground">{title}</h1>
|
||||||
|
{description && <p className="mt-1 text-sm text-muted-foreground">{description}</p>}
|
||||||
|
{kpiLine ? (
|
||||||
|
<div className="mt-2 flex flex-wrap items-center gap-x-4 gap-y-1 text-sm text-muted-foreground">
|
||||||
|
{kpiLine}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
{actions && <div className="flex shrink-0 items-center gap-2">{actions}</div>}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
33
src/components/ui/empty-state.tsx
Normal file
33
src/components/ui/empty-state.tsx
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
|
interface EmptyStateProps {
|
||||||
|
icon?: React.ReactNode;
|
||||||
|
title: string;
|
||||||
|
body?: React.ReactNode;
|
||||||
|
actions?: React.ReactNode;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function EmptyState({ icon, title, body, actions, className }: EmptyStateProps) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'flex flex-col items-center justify-center gap-3 rounded-lg border border-dashed border-slate-200 bg-white px-6 py-12 text-center',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{icon ? (
|
||||||
|
<div className="flex h-14 w-14 items-center justify-center rounded-full bg-gradient-brand-soft text-brand">
|
||||||
|
{icon}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
<div className="text-base font-semibold text-foreground">{title}</div>
|
||||||
|
{body ? <div className="max-w-md text-sm text-muted-foreground">{body}</div> : null}
|
||||||
|
{actions ? (
|
||||||
|
<div className="mt-2 flex flex-wrap items-center justify-center gap-2">{actions}</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
70
src/components/ui/kpi-tile.tsx
Normal file
70
src/components/ui/kpi-tile.tsx
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
|
interface KPITileProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||||
|
title: string;
|
||||||
|
value: React.ReactNode;
|
||||||
|
/** Signed delta vs. prior period; positive = green, negative = red, undefined = no chip. */
|
||||||
|
delta?: number;
|
||||||
|
/** Pre-rendered sparkline (recharts) — caller decides shape. */
|
||||||
|
sparkline?: React.ReactNode;
|
||||||
|
/** Optional accent stripe colour token; defaults to brand. */
|
||||||
|
accent?: 'brand' | 'success' | 'warning' | 'mint' | 'teal' | 'purple';
|
||||||
|
}
|
||||||
|
|
||||||
|
const ACCENT_STRIPES: Record<NonNullable<KPITileProps['accent']>, string> = {
|
||||||
|
brand: 'bg-gradient-brand',
|
||||||
|
success: 'bg-success',
|
||||||
|
warning: 'bg-warning',
|
||||||
|
mint: 'bg-mint',
|
||||||
|
teal: 'bg-teal',
|
||||||
|
purple: 'bg-purple',
|
||||||
|
};
|
||||||
|
|
||||||
|
export function KPITile({
|
||||||
|
title,
|
||||||
|
value,
|
||||||
|
delta,
|
||||||
|
sparkline,
|
||||||
|
accent = 'brand',
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: KPITileProps) {
|
||||||
|
const deltaClass =
|
||||||
|
typeof delta === 'number'
|
||||||
|
? delta > 0
|
||||||
|
? 'text-success'
|
||||||
|
: delta < 0
|
||||||
|
? 'text-error'
|
||||||
|
: 'text-muted-foreground'
|
||||||
|
: '';
|
||||||
|
const deltaPrefix = typeof delta === 'number' ? (delta > 0 ? '+' : '') : '';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'group relative overflow-hidden rounded-xl border border-slate-200 bg-gradient-brand-soft p-5 shadow-sm transition-all duration-base ease-smooth hover:shadow-md',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<div className={cn('absolute inset-x-0 top-0 h-1', ACCENT_STRIPES[accent])} aria-hidden />
|
||||||
|
<div className="flex items-start justify-between gap-4">
|
||||||
|
<div className="min-w-0">
|
||||||
|
<div className="text-xs font-medium uppercase tracking-wide text-muted-foreground">
|
||||||
|
{title}
|
||||||
|
</div>
|
||||||
|
<div className="mt-2 text-2xl font-semibold tabular-nums text-foreground">{value}</div>
|
||||||
|
{typeof delta === 'number' ? (
|
||||||
|
<div className={cn('mt-1 text-xs font-medium', deltaClass)}>
|
||||||
|
{deltaPrefix}
|
||||||
|
{delta}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
{sparkline ? <div className="h-12 w-24 shrink-0 opacity-80">{sparkline}</div> : null}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
55
src/components/ui/status-pill.tsx
Normal file
55
src/components/ui/status-pill.tsx
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
import { cva, type VariantProps } from 'class-variance-authority';
|
||||||
|
import * as React from 'react';
|
||||||
|
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Status pill — a single visual primitive for "this thing is in state X" across
|
||||||
|
* documents, signers, reservations, interests. Replaces ad-hoc Badge variants
|
||||||
|
* sprinkled through detail pages so the colour mapping stays consistent.
|
||||||
|
*/
|
||||||
|
const statusPillVariants = cva(
|
||||||
|
'inline-flex items-center gap-1.5 rounded-md border px-2 py-0.5 text-xs font-medium transition-colors',
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
status: {
|
||||||
|
// Document/signer lifecycle
|
||||||
|
pending: 'border-slate-200 bg-slate-100 text-slate-700',
|
||||||
|
sent: 'border-brand-100 bg-brand-50 text-brand-700',
|
||||||
|
partial: 'border-teal-light bg-teal-light/40 text-teal-dark',
|
||||||
|
signed: 'border-success-border bg-success-bg text-success',
|
||||||
|
completed: 'border-success-border bg-success-bg text-success',
|
||||||
|
expired: 'border-warning-border bg-warning-bg text-warning',
|
||||||
|
rejected: 'border-error-border bg-error-bg text-error',
|
||||||
|
cancelled: 'border-slate-300 bg-slate-200 text-slate-600',
|
||||||
|
declined: 'border-error-border bg-error-bg text-error',
|
||||||
|
// Reservation / interest lifecycle
|
||||||
|
active: 'border-success-border bg-success-bg text-success',
|
||||||
|
archived: 'border-slate-200 bg-slate-100 text-slate-500',
|
||||||
|
// Delivered (non-signature docs in hub)
|
||||||
|
delivered: 'border-purple-light bg-purple-light/40 text-purple-dark',
|
||||||
|
draft: 'border-slate-200 bg-white text-slate-600',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
status: 'pending',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
export type StatusPillStatus = NonNullable<VariantProps<typeof statusPillVariants>['status']>;
|
||||||
|
|
||||||
|
interface StatusPillProps
|
||||||
|
extends React.HTMLAttributes<HTMLSpanElement>, VariantProps<typeof statusPillVariants> {
|
||||||
|
/** Optional leading dot — useful for "in-progress" style indicators. */
|
||||||
|
withDot?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function StatusPill({ status, withDot, className, children, ...props }: StatusPillProps) {
|
||||||
|
return (
|
||||||
|
<span className={cn(statusPillVariants({ status }), className)} {...props}>
|
||||||
|
{withDot ? <span className="h-1.5 w-1.5 rounded-full bg-current" aria-hidden /> : null}
|
||||||
|
{children}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,43 +1,44 @@
|
|||||||
import type { Config } from "tailwindcss";
|
import type { Config } from 'tailwindcss';
|
||||||
|
import tailwindcssAnimate from 'tailwindcss-animate';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
darkMode: ["class", "class"],
|
darkMode: ['class', 'class'],
|
||||||
content: ["./src/**/*.{ts,tsx}"],
|
content: ['./src/**/*.{ts,tsx}'],
|
||||||
theme: {
|
theme: {
|
||||||
extend: {
|
extend: {
|
||||||
colors: {
|
colors: {
|
||||||
border: "hsl(var(--border))",
|
border: 'hsl(var(--border))',
|
||||||
input: "hsl(var(--input))",
|
input: 'hsl(var(--input))',
|
||||||
ring: "hsl(var(--ring))",
|
ring: 'hsl(var(--ring))',
|
||||||
background: "hsl(var(--background))",
|
background: 'hsl(var(--background))',
|
||||||
foreground: "hsl(var(--foreground))",
|
foreground: 'hsl(var(--foreground))',
|
||||||
primary: {
|
primary: {
|
||||||
DEFAULT: "hsl(var(--primary))",
|
DEFAULT: 'hsl(var(--primary))',
|
||||||
foreground: "hsl(var(--primary-foreground))",
|
foreground: 'hsl(var(--primary-foreground))',
|
||||||
},
|
},
|
||||||
secondary: {
|
secondary: {
|
||||||
DEFAULT: "hsl(var(--secondary))",
|
DEFAULT: 'hsl(var(--secondary))',
|
||||||
foreground: "hsl(var(--secondary-foreground))",
|
foreground: 'hsl(var(--secondary-foreground))',
|
||||||
},
|
},
|
||||||
destructive: {
|
destructive: {
|
||||||
DEFAULT: "hsl(var(--destructive))",
|
DEFAULT: 'hsl(var(--destructive))',
|
||||||
foreground: "hsl(var(--destructive-foreground))",
|
foreground: 'hsl(var(--destructive-foreground))',
|
||||||
},
|
},
|
||||||
muted: {
|
muted: {
|
||||||
DEFAULT: "hsl(var(--muted))",
|
DEFAULT: 'hsl(var(--muted))',
|
||||||
foreground: "hsl(var(--muted-foreground))",
|
foreground: 'hsl(var(--muted-foreground))',
|
||||||
},
|
},
|
||||||
accent: {
|
accent: {
|
||||||
DEFAULT: "hsl(var(--accent))",
|
DEFAULT: 'hsl(var(--accent))',
|
||||||
foreground: "hsl(var(--accent-foreground))",
|
foreground: 'hsl(var(--accent-foreground))',
|
||||||
},
|
},
|
||||||
popover: {
|
popover: {
|
||||||
DEFAULT: "hsl(var(--popover))",
|
DEFAULT: 'hsl(var(--popover))',
|
||||||
foreground: "hsl(var(--popover-foreground))",
|
foreground: 'hsl(var(--popover-foreground))',
|
||||||
},
|
},
|
||||||
card: {
|
card: {
|
||||||
DEFAULT: "hsl(var(--card))",
|
DEFAULT: 'hsl(var(--card))',
|
||||||
foreground: "hsl(var(--card-foreground))",
|
foreground: 'hsl(var(--card-foreground))',
|
||||||
},
|
},
|
||||||
brand: {
|
brand: {
|
||||||
'50': '#d8e5f4',
|
'50': '#d8e5f4',
|
||||||
@@ -49,7 +50,7 @@ export default {
|
|||||||
'600': '#255a9e',
|
'600': '#255a9e',
|
||||||
'700': '#1c4a87',
|
'700': '#1c4a87',
|
||||||
DEFAULT: '#3a7bc8',
|
DEFAULT: '#3a7bc8',
|
||||||
dark: '#1e2844'
|
dark: '#1e2844',
|
||||||
},
|
},
|
||||||
navy: {
|
navy: {
|
||||||
'50': '#cdcfd6',
|
'50': '#cdcfd6',
|
||||||
@@ -59,114 +60,115 @@ export default {
|
|||||||
'400': '#1e2844',
|
'400': '#1e2844',
|
||||||
'500': '#171f35',
|
'500': '#171f35',
|
||||||
'600': '#101625',
|
'600': '#101625',
|
||||||
DEFAULT: '#1e2844'
|
DEFAULT: '#1e2844',
|
||||||
},
|
},
|
||||||
sage: {
|
sage: {
|
||||||
DEFAULT: '#dae3c1',
|
DEFAULT: '#dae3c1',
|
||||||
light: '#edf1e2',
|
light: '#edf1e2',
|
||||||
dark: '#b8c49e'
|
dark: '#b8c49e',
|
||||||
},
|
},
|
||||||
mint: {
|
mint: {
|
||||||
DEFAULT: '#add5b3',
|
DEFAULT: '#add5b3',
|
||||||
light: '#d6ead9',
|
light: '#d6ead9',
|
||||||
dark: '#7dba85'
|
dark: '#7dba85',
|
||||||
},
|
},
|
||||||
teal: {
|
teal: {
|
||||||
DEFAULT: '#83aab1',
|
DEFAULT: '#83aab1',
|
||||||
light: '#b1cdd2',
|
light: '#b1cdd2',
|
||||||
dark: '#5a8a92'
|
dark: '#5a8a92',
|
||||||
},
|
},
|
||||||
purple: {
|
purple: {
|
||||||
DEFAULT: '#685aa3',
|
DEFAULT: '#685aa3',
|
||||||
light: '#a49ac6',
|
light: '#a49ac6',
|
||||||
dark: '#4d4280'
|
dark: '#4d4280',
|
||||||
},
|
},
|
||||||
success: {
|
success: {
|
||||||
DEFAULT: '#2d8a4e',
|
DEFAULT: '#2d8a4e',
|
||||||
bg: '#e8f5e9',
|
bg: '#e8f5e9',
|
||||||
border: '#a5d6a7'
|
border: '#a5d6a7',
|
||||||
},
|
},
|
||||||
warning: {
|
warning: {
|
||||||
DEFAULT: '#e6a817',
|
DEFAULT: '#e6a817',
|
||||||
bg: '#fff8e1',
|
bg: '#fff8e1',
|
||||||
border: '#ffe082'
|
border: '#ffe082',
|
||||||
},
|
},
|
||||||
error: {
|
error: {
|
||||||
DEFAULT: '#d32f2f',
|
DEFAULT: '#d32f2f',
|
||||||
bg: '#ffebee',
|
bg: '#ffebee',
|
||||||
border: '#ef9a9a'
|
border: '#ef9a9a',
|
||||||
},
|
},
|
||||||
sidebar: {
|
sidebar: {
|
||||||
DEFAULT: '#1e2844',
|
DEFAULT: '#1e2844',
|
||||||
text: '#cdcfd6',
|
text: '#cdcfd6',
|
||||||
hover: '#171f35',
|
hover: '#171f35',
|
||||||
active: '#3a7bc8',
|
active: '#3a7bc8',
|
||||||
divider: '#474e66'
|
divider: '#474e66',
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
fontFamily: {
|
fontFamily: {
|
||||||
sans: [
|
sans: ['Inter', 'system-ui', '-apple-system', 'Arial', 'sans-serif'],
|
||||||
'Inter',
|
mono: ['JetBrains Mono', 'ui-monospace', 'monospace'],
|
||||||
'system-ui',
|
serif: ['Georgia', 'Times New Roman', 'serif'],
|
||||||
'-apple-system',
|
|
||||||
'Arial',
|
|
||||||
'sans-serif'
|
|
||||||
],
|
|
||||||
mono: [
|
|
||||||
'JetBrains Mono',
|
|
||||||
'ui-monospace',
|
|
||||||
'monospace'
|
|
||||||
],
|
|
||||||
serif: [
|
|
||||||
'Georgia',
|
|
||||||
'Times New Roman',
|
|
||||||
'serif'
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
boxShadow: {
|
boxShadow: {
|
||||||
sm: '0 1px 2px rgba(30, 40, 68, 0.06)',
|
xs: '0 1px 2px 0 rgb(15 23 42 / 0.04)',
|
||||||
|
sm: '0 2px 4px -1px rgb(15 23 42 / 0.06)',
|
||||||
DEFAULT: '0 1px 3px rgba(30, 40, 68, 0.10), 0 1px 2px rgba(30, 40, 68, 0.06)',
|
DEFAULT: '0 1px 3px rgba(30, 40, 68, 0.10), 0 1px 2px rgba(30, 40, 68, 0.06)',
|
||||||
md: '0 4px 6px rgba(30, 40, 68, 0.10), 0 2px 4px rgba(30, 40, 68, 0.06)',
|
md: '0 4px 12px -2px rgb(15 23 42 / 0.08)',
|
||||||
lg: '0 10px 15px rgba(30, 40, 68, 0.10), 0 4px 6px rgba(30, 40, 68, 0.05)',
|
lg: '0 12px 32px -8px rgb(15 23 42 / 0.12)',
|
||||||
xl: '0 20px 25px rgba(30, 40, 68, 0.10), 0 8px 10px rgba(30, 40, 68, 0.04)'
|
xl: '0 20px 25px rgba(30, 40, 68, 0.10), 0 8px 10px rgba(30, 40, 68, 0.04)',
|
||||||
|
glow: '0 0 0 4px rgb(58 123 200 / 0.12)',
|
||||||
},
|
},
|
||||||
borderRadius: {
|
borderRadius: {
|
||||||
sm: '0.25rem',
|
sm: '0.375rem',
|
||||||
DEFAULT: '0.375rem',
|
DEFAULT: '0.375rem',
|
||||||
md: '0.5rem',
|
md: '0.5rem',
|
||||||
lg: '0.75rem',
|
lg: '0.625rem',
|
||||||
xl: '1rem'
|
xl: '0.875rem',
|
||||||
|
},
|
||||||
|
backgroundImage: {
|
||||||
|
'gradient-brand': 'linear-gradient(135deg, #3a7bc8 0%, #2f6ab5 100%)',
|
||||||
|
'gradient-brand-soft': 'linear-gradient(135deg, #d8e5f4 0%, #ffffff 100%)',
|
||||||
|
'gradient-success': 'linear-gradient(135deg, #e8f5e9 0%, #ffffff 100%)',
|
||||||
|
'gradient-warning': 'linear-gradient(135deg, #fef3c7 0%, #ffffff 100%)',
|
||||||
|
},
|
||||||
|
transitionTimingFunction: {
|
||||||
|
spring: 'cubic-bezier(0.34, 1.56, 0.64, 1)',
|
||||||
|
smooth: 'cubic-bezier(0.4, 0, 0.2, 1)',
|
||||||
},
|
},
|
||||||
width: {
|
width: {
|
||||||
sidebar: '256px',
|
sidebar: '256px',
|
||||||
'sidebar-collapsed': '64px'
|
'sidebar-collapsed': '64px',
|
||||||
},
|
},
|
||||||
transitionDuration: {
|
transitionDuration: {
|
||||||
sidebar: '200ms'
|
sidebar: '200ms',
|
||||||
|
fast: '150ms',
|
||||||
|
base: '200ms',
|
||||||
|
slow: '300ms',
|
||||||
},
|
},
|
||||||
keyframes: {
|
keyframes: {
|
||||||
'accordion-down': {
|
'accordion-down': {
|
||||||
from: {
|
from: {
|
||||||
height: '0'
|
height: '0',
|
||||||
},
|
},
|
||||||
to: {
|
to: {
|
||||||
height: 'var(--radix-accordion-content-height)'
|
height: 'var(--radix-accordion-content-height)',
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
'accordion-up': {
|
'accordion-up': {
|
||||||
from: {
|
from: {
|
||||||
height: 'var(--radix-accordion-content-height)'
|
height: 'var(--radix-accordion-content-height)',
|
||||||
},
|
},
|
||||||
to: {
|
to: {
|
||||||
height: '0'
|
height: '0',
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
animation: {
|
animation: {
|
||||||
'accordion-down': 'accordion-down 0.2s ease-out',
|
'accordion-down': 'accordion-down 0.2s ease-out',
|
||||||
'accordion-up': 'accordion-up 0.2s ease-out'
|
'accordion-up': 'accordion-up 0.2s ease-out',
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
plugins: [require("tailwindcss-animate")],
|
},
|
||||||
|
},
|
||||||
|
plugins: [tailwindcssAnimate],
|
||||||
} satisfies Config;
|
} satisfies Config;
|
||||||
|
|||||||
Reference in New Issue
Block a user