Initial commit: @ampeco/ewa-ui v1.0.0
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>
This commit is contained in:
@@ -0,0 +1,261 @@
|
||||
# @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
|
||||
|
||||
```bash
|
||||
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.
|
||||
|
||||
```js
|
||||
// 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](docs/ARCHITECTURE.md) for the full variable list.
|
||||
|
||||
## Components
|
||||
|
||||
All components are named exports from `@ampeco/ewa-ui`:
|
||||
|
||||
```tsx
|
||||
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.
|
||||
|
||||
```tsx
|
||||
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
variant?: 'primary' | 'secondary' | 'danger' | 'text';
|
||||
fullWidth?: boolean;
|
||||
loading?: boolean;
|
||||
}
|
||||
```
|
||||
|
||||
```tsx
|
||||
<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.
|
||||
- `loading` shows an animated spinner overlay and hides children via `invisible`.
|
||||
- `disabled` and `loading` both set `opacity-50 cursor-not-allowed`.
|
||||
- Focus ring uses `focus-visible:ring-2` with the primary button color.
|
||||
|
||||
---
|
||||
|
||||
### Card
|
||||
|
||||
Surface container with optional click interactivity.
|
||||
|
||||
```tsx
|
||||
interface CardProps {
|
||||
className?: string;
|
||||
onClick?: () => void;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
```
|
||||
|
||||
```tsx
|
||||
<Card>
|
||||
<p>Static content</p>
|
||||
</Card>
|
||||
|
||||
<Card onClick={() => navigate(`/bookings/${id}`)}>
|
||||
<p>Clickable card with hover scale and keyboard support</p>
|
||||
</Card>
|
||||
```
|
||||
|
||||
- When `onClick` is provided, the card renders with `role="button"`, `tabIndex={0}`, and `Enter`/`Space` key handlers.
|
||||
- Hover applies `scale-[1.01]`, active applies `scale-[0.99]`.
|
||||
|
||||
---
|
||||
|
||||
### Input
|
||||
|
||||
Labeled text input with error and hint support. Forwards refs.
|
||||
|
||||
```tsx
|
||||
interface InputProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'className'> {
|
||||
label: string;
|
||||
hint?: string;
|
||||
error?: string;
|
||||
className?: string;
|
||||
}
|
||||
```
|
||||
|
||||
```tsx
|
||||
<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 `id` from 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.
|
||||
|
||||
```tsx
|
||||
interface StatusBadgeProps {
|
||||
status: string;
|
||||
label: string;
|
||||
}
|
||||
```
|
||||
|
||||
```tsx
|
||||
<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.
|
||||
|
||||
```tsx
|
||||
interface LoaderProps {
|
||||
fullScreen?: boolean;
|
||||
}
|
||||
```
|
||||
|
||||
```tsx
|
||||
<Loader />
|
||||
<Loader fullScreen />
|
||||
```
|
||||
|
||||
- `fullScreen` wraps the spinner in a `min-h-[60dvh]` flex container.
|
||||
- Uses `role="status"` and `aria-label="Loading"`.
|
||||
|
||||
---
|
||||
|
||||
### EmptyState
|
||||
|
||||
Centered placeholder for empty lists or zero-data screens.
|
||||
|
||||
```tsx
|
||||
interface EmptyStateProps {
|
||||
icon?: React.ReactNode;
|
||||
title: string;
|
||||
description?: string;
|
||||
action?: { label: string; onClick: () => void };
|
||||
}
|
||||
```
|
||||
|
||||
```tsx
|
||||
<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 `Button` with `variant="primary"` for the action.
|
||||
|
||||
---
|
||||
|
||||
### Toast
|
||||
|
||||
Auto-dismissing notification banner pinned to the top of the viewport.
|
||||
|
||||
```tsx
|
||||
interface ToastProps {
|
||||
message: string;
|
||||
type: 'success' | 'error';
|
||||
onDismiss: () => void;
|
||||
duration?: number; // ms, default 3000
|
||||
}
|
||||
```
|
||||
|
||||
```tsx
|
||||
<Toast message="Booking created" type="success" onDismiss={() => setToast(null)} />
|
||||
<Toast message="Failed to save" type="error" onDismiss={() => setToast(null)} duration={5000} />
|
||||
```
|
||||
|
||||
- Uses `animate-slide-in` for 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.
|
||||
|
||||
```tsx
|
||||
interface BottomSheetProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
```
|
||||
|
||||
```tsx
|
||||
<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"` and `aria-modal="true"`.
|
||||
|
||||
## Build
|
||||
|
||||
```bash
|
||||
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
|
||||
```
|
||||
Reference in New Issue
Block a user