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>
10 KiB
Architecture
Technical internals of the @ampeco/ewa-ui package.
Design Philosophy
-
CSS-variable-driven theming. Components never hardcode colors. Every visual property maps to a CSS custom property (
--btn-primary-bg,--surface-bg, etc.), enabling white-labeling by swapping variable values at the:rootlevel. -
44px touch targets. All interactive elements (buttons, inputs, toast dismiss) use
h-11(44px) minimum height, meeting WCAG 2.5.8 and Apple HIG touch-target guidelines. -
Accessibility first. Interactive
Cardgetsrole="button",tabIndex, andEnter/Spacekey handlers.Loaderusesrole="status"+aria-label.Toastusesrole="alert".BottomSheetusesrole="dialog"+aria-modal. Focus rings usefocus-visibleto avoid visual clutter on mouse clicks. -
Tailwind-native styling. Components compose Tailwind utility classes directly -- no CSS files, no CSS-in-JS runtime. The Tailwind plugin registers the design tokens so standard utilities like
bg-surface-elevation-1ortext-body-mediumresolve to the correct CSS variable values.
Package Entry Points
Defined in package.json exports:
| Import path | Resolves to | Purpose |
|---|---|---|
@ampeco/ewa-ui |
dist/index.js / dist/index.cjs |
React components + type exports |
@ampeco/ewa-ui/plugin |
dist/plugin.js / dist/plugin.cjs |
Tailwind CSS plugin (no React dependency) |
Both entry points ship ESM and CJS formats via tsup, with .d.ts and .d.cts type declarations.
Build config: tsup.config.ts -- two entry points, tree-shakeable, React externalized.
Tailwind Plugin Internals
Source: src/plugin.ts
The plugin is built with tailwindcss/plugin and does three things:
1. Base Styles
addBase({
'*, *::before, *::after': { 'box-sizing': 'border-box' },
'body': {
'font-family': "'Inter', system-ui, -apple-system, sans-serif",
'-webkit-font-smoothing': 'antialiased',
'-moz-osx-font-smoothing': 'grayscale',
},
});
Sets universal box-sizing and the Inter font stack with subpixel antialiasing.
2. Status Background Utilities
Generates .bg-status-{name}-subtle utilities for each status using color-mix():
const statusColors = ['accepted', 'reserved', 'completed', 'cancelled', 'no-show', 'failed'];
// Produces:
// .bg-status-accepted-subtle { background-color: color-mix(in srgb, var(--status-accepted) 15%, transparent); }
// .bg-status-reserved-subtle { background-color: color-mix(in srgb, var(--status-reserved) 15%, transparent); }
// ...
This is implemented via addUtilities() rather than theme extension because it requires color-mix() with a CSS variable -- something Tailwind's color system cannot express natively.
3. Theme Extension
Extends Tailwind's default theme with tokens that resolve to CSS custom properties:
Colors
| Tailwind class prefix | CSS variable | Usage |
|---|---|---|
bg-btn-primary-bg |
--btn-primary-bg |
Primary button background |
text-btn-primary-text |
--btn-primary-text |
Primary button text |
bg-btn-secondary-bg |
--btn-secondary-bg |
Secondary button hover fill |
border-btn-secondary-border |
--btn-secondary-border |
Secondary button border |
text-btn-secondary-text |
--btn-secondary-text |
Secondary button text |
bg-surface-bg |
--surface-bg |
Page/app background |
bg-surface-elevation-1 |
--surface-elevation-1 |
Card surfaces |
bg-surface-elevation-2 |
--surface-elevation-2 |
Dividers, loader track |
text-text-base-color |
--text-base |
Primary text |
text-text-support |
--text-support |
Secondary/muted text |
text-text-placeholder |
--text-placeholder |
Input placeholders |
bg-feedback-success |
--feedback-success |
Success color (solid) |
bg-feedback-error |
--feedback-error |
Error color (solid) |
bg-feedback-warning |
--feedback-warning |
Warning color (solid) |
bg-feedback-info |
--feedback-info |
Info color (solid) |
bg-feedback-success-subtle |
--feedback-success-subtle |
Success background (light) |
bg-feedback-error-subtle |
--feedback-error-subtle |
Error background (light) |
bg-feedback-warning-subtle |
--feedback-warning-subtle |
Warning background (light) |
bg-feedback-info-subtle |
--feedback-info-subtle |
Info background (light) |
bg-input-fill |
--input-fill |
Input background |
border-input-border |
--input-border |
Input border (default) |
border-input-border-active |
--input-border-active |
Input border (focus) |
text-input-text |
--input-text |
Input text |
border-separator |
--separator |
Divider lines |
text-status-accepted |
--status-accepted |
Booking status: accepted |
text-status-reserved |
--status-reserved |
Booking status: reserved |
text-status-completed |
--status-completed |
Booking status: completed |
text-status-cancelled |
--status-cancelled |
Booking status: cancelled |
text-status-no-show |
--status-no-show |
Booking status: no-show |
text-status-failed |
--status-failed |
Booking status: failed |
Spacing Scale
| Token | CSS variable | Suggested value |
|---|---|---|
xxxs |
--space-xxxs |
2px |
xxs |
--space-xxs |
4px |
xs |
--space-xs |
8px |
s |
--space-s |
12px |
m |
--space-m |
16px |
l |
--space-l |
20px |
xl |
--space-xl |
24px |
xxl |
--space-xxl |
32px |
xxxl |
--space-xxxl |
48px |
Used as: p-l, gap-xs, mt-m, mb-xxl, etc.
Border Radius
| Token | CSS variable |
|---|---|
s |
--radius-s |
m |
--radius-m |
l |
--radius-l |
xl |
--radius-xl |
Used as: rounded-s, rounded-m, rounded-l, rounded-xl.
Typography Scale
| Utility | Size | Line height | Weight |
|---|---|---|---|
text-h1 |
22px | 26px | 600 |
text-h2 |
18px | 22px | 600 |
text-h3 |
16px | 20px | 500 |
text-h4 |
14px | 17px | 500 |
text-body-large |
16px | 20px | 400 |
text-body-medium |
14px | 17px | 400 |
text-body-large-heavy |
16px | 20px | 600 |
text-body-medium-heavy |
14px | 17px | 600 |
text-buttons |
14px | 20px | 600 |
text-buttons-small |
12px | 17px | 700 |
text-label |
12px | 15px | 400 |
text-caption |
12px | 15px | 500 |
Each utility sets font-size, line-height, and font-weight simultaneously via Tailwind's fontSize tuple syntax.
Animations
| Utility | Keyframes | Duration | Timing |
|---|---|---|---|
animate-spin |
spin |
1s | linear, infinite |
animate-slide-in |
slideIn (translateY -100% to 0, fade) |
0.2s | ease-out |
animate-slide-up |
slideUp (translateY 100% to 0) |
0.3s | ease-out |
animate-fade-in |
fadeIn (opacity 0 to 1) |
0.2s | ease-out |
How Status Colors Work
Status colors use a two-layer system:
-
Solid color --
var(--status-accepted)etc. -- used for text viatext-[var(--status-accepted)]inStatusBadge. -
Subtle background -- generated by the Tailwind plugin as a custom utility class:
.bg-status-accepted-subtle {
background-color: color-mix(in srgb, var(--status-accepted) 15%, transparent);
}
color-mix(in srgb, <color> 15%, transparent) blends the status color at 15% opacity with transparency. This produces a tinted background that automatically adapts to any status color value without needing a separate --status-accepted-subtle variable.
The StatusBadge component (src/components/StatusBadge.tsx) applies both layers:
className={[
`bg-status-${mappedStatus}-subtle`, // plugin-generated utility
`text-[var(--status-${mappedStatus})]`, // arbitrary value for text color
].join(' ')}
The statusMap normalizes API values (e.g. no_show with underscore) to CSS-friendly names (no-show with hyphen).
Component Hierarchy
EmptyState
└── Button (renders the optional action CTA)
Toast
└── (self-contained, no child components)
BottomSheet
└── (renders children; scroll-locks body)
Card
└── (renders children; conditionally interactive)
Button
└── (self-contained with inline SVG spinner)
Input
└── (self-contained; forwardRef)
StatusBadge
└── (self-contained)
Loader
└── (self-contained)
EmptyState is the only component with an internal component dependency (it imports Button). All other components are leaf nodes.
CSS Variable Contract
The consuming application must define these CSS custom properties (typically on :root or a theme wrapper element). The plugin does not provide default values -- all colors, spacing, and radii are expected to be injected by the host.
Minimal example:
:root {
/* Buttons */
--btn-primary-bg: #2563eb;
--btn-primary-text: #ffffff;
--btn-secondary-bg: #f1f5f9;
--btn-secondary-border: #cbd5e1;
--btn-secondary-text: #1e293b;
/* Surfaces */
--surface-bg: #ffffff;
--surface-elevation-1: #ffffff;
--surface-elevation-2: #e2e8f0;
/* Text */
--text-base: #0f172a;
--text-support: #64748b;
--text-placeholder: #94a3b8;
/* Feedback */
--feedback-success: #16a34a;
--feedback-error: #dc2626;
--feedback-warning: #d97706;
--feedback-info: #2563eb;
--feedback-success-subtle: #f0fdf4;
--feedback-error-subtle: #fef2f2;
--feedback-warning-subtle: #fffbeb;
--feedback-info-subtle: #eff6ff;
/* Inputs */
--input-fill: #f8fafc;
--input-border: #e2e8f0;
--input-border-active: #2563eb;
--input-text: #0f172a;
/* Separator */
--separator: #e2e8f0;
/* Status */
--status-accepted: #2563eb;
--status-reserved: #7c3aed;
--status-completed: #16a34a;
--status-cancelled: #64748b;
--status-no-show: #d97706;
--status-failed: #dc2626;
/* Spacing */
--space-xxxs: 2px;
--space-xxs: 4px;
--space-xs: 8px;
--space-s: 12px;
--space-m: 16px;
--space-l: 20px;
--space-xl: 24px;
--space-xxl: 32px;
--space-xxxl: 48px;
/* Radii */
--radius-s: 6px;
--radius-m: 10px;
--radius-l: 16px;
--radius-xl: 24px;
}
Safe Area Handling
BottomSheet accounts for notched/home-bar devices via:
padding-bottom: calc(var(--space-l) + env(safe-area-inset-bottom));
Consuming apps should include <meta name="viewport" content="..., viewport-fit=cover"> for this to take effect.
Scroll Locking
BottomSheet sets document.body.style.overflow = 'hidden' while open and restores the original value on close (via useEffect cleanup). This prevents background scroll while the modal is visible.