Files
ewa-ui/docs/ARCHITECTURE.md
T
Kaloyan Danchev cdc8829ce7 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>
2026-03-18 19:34:11 +02:00

10 KiB

Architecture

Technical internals of the @ampeco/ewa-ui package.

Design Philosophy

  1. 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 :root level.

  2. 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.

  3. Accessibility first. Interactive Card gets role="button", tabIndex, and Enter/Space key handlers. Loader uses role="status" + aria-label. Toast uses role="alert". BottomSheet uses role="dialog" + aria-modal. Focus rings use focus-visible to avoid visual clutter on mouse clicks.

  4. 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-1 or text-body-medium resolve 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:

  1. Solid color -- var(--status-accepted) etc. -- used for text via text-[var(--status-accepted)] in StatusBadge.

  2. 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.