# 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 ```js 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()`: ```js 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: ```css .bg-status-accepted-subtle { background-color: color-mix(in srgb, var(--status-accepted) 15%, transparent); } ``` `color-mix(in srgb, 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: ```tsx 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: ```css :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: ```css padding-bottom: calc(var(--space-l) + env(safe-area-inset-bottom)); ``` Consuming apps should include `` 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.