Shared React component library with Tailwind plugin for AMPECO EWAs. 8 components: Button, Card, Input, StatusBadge, Loader, EmptyState, Toast, BottomSheet. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
6.1 KiB
@ampeco/ewa-ui
Shared React component library and Tailwind CSS plugin for AMPECO EWA (Embedded Web App) projects. Provides themed, accessible UI primitives driven entirely by CSS custom properties.
Install
npm install @ampeco/ewa-ui
Peer dependencies: react >= 18, react-dom >= 18.
Tailwind Plugin Setup
The package ships a Tailwind plugin at @ampeco/ewa-ui/plugin that registers design tokens (colors, spacing, radii, typography, animations) and status-color utilities.
// tailwind.config.js
import { ewaUiPlugin } from '@ampeco/ewa-ui/plugin';
export default {
content: [
'./src/**/*.{ts,tsx}',
'./node_modules/@ampeco/ewa-ui/dist/**/*.{js,cjs}', // scan library classes
],
plugins: [ewaUiPlugin],
};
You must define the CSS custom properties the plugin references. See docs/ARCHITECTURE.md for the full variable list.
Components
All components are named exports from @ampeco/ewa-ui:
import { Button, Card, Input, StatusBadge, Loader, EmptyState, Toast, BottomSheet } from '@ampeco/ewa-ui';
Type-only imports are also available (e.g. import type { ButtonProps } from '@ampeco/ewa-ui').
Button
Themed button with four variants and a built-in loading spinner.
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
variant?: 'primary' | 'secondary' | 'danger' | 'text';
fullWidth?: boolean;
loading?: boolean;
}
<Button variant="primary" onClick={handleSave}>Save Booking</Button>
<Button variant="secondary" fullWidth>Cancel</Button>
<Button variant="danger" loading={isDeleting}>Delete</Button>
<Button variant="text">Learn more</Button>
- Height is fixed at
h-11(44px) for touch-target compliance. loadingshows an animated spinner overlay and hides children viainvisible.disabledandloadingboth setopacity-50 cursor-not-allowed.- Focus ring uses
focus-visible:ring-2with the primary button color.
Card
Surface container with optional click interactivity.
interface CardProps {
className?: string;
onClick?: () => void;
children: React.ReactNode;
}
<Card>
<p>Static content</p>
</Card>
<Card onClick={() => navigate(`/bookings/${id}`)}>
<p>Clickable card with hover scale and keyboard support</p>
</Card>
- When
onClickis provided, the card renders withrole="button",tabIndex={0}, andEnter/Spacekey handlers. - Hover applies
scale-[1.01], active appliesscale-[0.99].
Input
Labeled text input with error and hint support. Forwards refs.
interface InputProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'className'> {
label: string;
hint?: string;
error?: string;
className?: string;
}
<Input label="Connector ID" placeholder="e.g. CPI-001" />
<Input label="Email" error="Invalid email address" />
<Input label="Duration" hint="In minutes" type="number" />
- Auto-generates
idfrom label text if none is provided. - Error state turns the border red; hint is hidden when error is present.
- Height is
h-11(44px touch target).
StatusBadge
Colored pill for booking/reservation status display.
interface StatusBadgeProps {
status: string;
label: string;
}
<StatusBadge status="accepted" label="Accepted" />
<StatusBadge status="no_show" label="No Show" />
<StatusBadge status="completed" label="Completed" />
Recognized statuses: accepted, reserved, completed, cancelled, no_show / no-show, failed. Text color uses the corresponding --status-* variable; background uses a 15% opacity color-mix() utility (see architecture docs).
Loader
Spinning ring indicator with optional full-screen centering.
interface LoaderProps {
fullScreen?: boolean;
}
<Loader />
<Loader fullScreen />
fullScreenwraps the spinner in amin-h-[60dvh]flex container.- Uses
role="status"andaria-label="Loading".
EmptyState
Centered placeholder for empty lists or zero-data screens.
interface EmptyStateProps {
icon?: React.ReactNode;
title: string;
description?: string;
action?: { label: string; onClick: () => void };
}
<EmptyState
icon={<CalendarIcon />}
title="No bookings yet"
description="Create your first booking to get started."
action={{ label: 'New Booking', onClick: () => navigate('/create') }}
/>
- Internally renders a
Buttonwithvariant="primary"for the action.
Toast
Auto-dismissing notification banner pinned to the top of the viewport.
interface ToastProps {
message: string;
type: 'success' | 'error';
onDismiss: () => void;
duration?: number; // ms, default 3000
}
<Toast message="Booking created" type="success" onDismiss={() => setToast(null)} />
<Toast message="Failed to save" type="error" onDismiss={() => setToast(null)} duration={5000} />
- Uses
animate-slide-infor entry animation. - Dismiss button is 44px square for touch accessibility.
- Auto-clears via
setTimeout; cleans up on unmount.
BottomSheet
Modal drawer that slides up from the bottom with a backdrop overlay.
interface BottomSheetProps {
open: boolean;
onClose: () => void;
children: React.ReactNode;
}
<BottomSheet open={isOpen} onClose={() => setIsOpen(false)}>
<h2>Select a time slot</h2>
{/* ... */}
</BottomSheet>
- Locks body scroll when open (
overflow: hidden), restores on close. - Overlay click triggers
onClose. - Bottom padding accounts for
safe-area-inset-bottom(notched devices). - Includes a drag-handle bar at the top.
- Uses
role="dialog"andaria-modal="true".
Build
npm run build # Production build via tsup (ESM + CJS + .d.ts)
npm run dev # Watch mode
npm run typecheck # Type-check without emitting
Source Files
src/
index.ts # Public re-exports
plugin.ts # Tailwind CSS plugin (design tokens + utilities)
components/
Button.tsx
Card.tsx
Input.tsx
StatusBadge.tsx
Loader.tsx
EmptyState.tsx
Toast.tsx
BottomSheet.tsx